feat(html/minifier): Merge script tags (#6273)

This commit is contained in:
Alexander Akait 2022-10-28 05:39:10 +03:00 committed by GitHub
parent 30b3596779
commit 02f8d31ef3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 225 additions and 66 deletions

View File

@ -578,6 +578,36 @@ impl Minifier<'_> {
})
}
fn is_type_text_javascript(&self, value: &JsWord) -> bool {
let value = value.trim().to_ascii_lowercase();
let value = if let Some(next) = value.split(';').next() {
next
} else {
&value
};
match value {
// Legacy JavaScript MIME types
"application/javascript"
| "application/ecmascript"
| "application/x-ecmascript"
| "application/x-javascript"
| "text/ecmascript"
| "text/javascript1.0"
| "text/javascript1.1"
| "text/javascript1.2"
| "text/javascript1.3"
| "text/javascript1.4"
| "text/javascript1.5"
| "text/jscript"
| "text/livescript"
| "text/x-ecmascript"
| "text/x-javascript" => true,
"text/javascript" => true,
_ => false,
}
}
fn is_default_attribute_value(
&self,
namespace: Namespace,
@ -606,47 +636,22 @@ impl Minifier<'_> {
}
_ => {}
},
js_word!("script") => {
match attribute.name {
js_word!("type") => {
let value = if let Some(next) = attribute_value.split(';').next() {
next
} else {
attribute_value
};
match value {
// Legacy JavaScript MIME types
"application/javascript"
| "application/ecmascript"
| "application/x-ecmascript"
| "application/x-javascript"
| "text/ecmascript"
| "text/javascript1.0"
| "text/javascript1.1"
| "text/javascript1.2"
| "text/javascript1.3"
| "text/javascript1.4"
| "text/javascript1.5"
| "text/jscript"
| "text/livescript"
| "text/x-ecmascript"
| "text/x-javascript" => return true,
"text/javascript" => return true,
_ => {}
}
js_word!("script") => match attribute.name {
js_word!("type") => {
if self.is_type_text_javascript(attribute_value) {
return true;
}
js_word!("language") => {
match &*attribute_value.trim().to_ascii_lowercase() {
"javascript" | "javascript1.2" | "javascript1.3"
| "javascript1.4" | "javascript1.5" | "javascript1.6"
| "javascript1.7" => return true,
_ => {}
}
}
_ => {}
}
}
js_word!("language") => {
match &*attribute_value.trim().to_ascii_lowercase() {
"javascript" | "javascript1.2" | "javascript1.3"
| "javascript1.4" | "javascript1.5" | "javascript1.6"
| "javascript1.7" => return true,
_ => {}
}
}
_ => {}
},
js_word!("link") => {
if attribute.name == js_word!("type")
&& &*attribute_value.trim().to_ascii_lowercase() == "text/css"
@ -1336,11 +1341,16 @@ impl Minifier<'_> {
fn allow_elements_to_merge(&self, left: Option<&Child>, right: &Element) -> bool {
if let Some(Child::Element(left)) = left {
if matches!(left.namespace, Namespace::HTML | Namespace::SVG)
let is_style_tag = matches!(left.namespace, Namespace::HTML | Namespace::SVG)
&& left.tag_name == js_word!("style")
&& matches!(right.namespace, Namespace::HTML | Namespace::SVG)
&& right.tag_name == js_word!("style")
{
&& right.tag_name == js_word!("style");
let is_script_tag = matches!(left.namespace, Namespace::HTML | Namespace::SVG)
&& left.tag_name == js_word!("script")
&& matches!(right.namespace, Namespace::HTML | Namespace::SVG)
&& right.tag_name == js_word!("script");
if is_style_tag || is_script_tag {
let mut need_skip = false;
let mut left_attributes = left
@ -1348,9 +1358,16 @@ impl Minifier<'_> {
.clone()
.into_iter()
.filter(|attribute| match attribute.name {
js_word!("src") if is_script_tag => {
need_skip = true;
true
}
js_word!("type") => {
if let Some(value) = &attribute.value {
if value.trim().to_ascii_lowercase() == "text/css" {
if (is_style_tag && value.trim().to_ascii_lowercase() == "text/css")
|| is_script_tag && self.is_type_text_javascript(value)
{
false
} else {
need_skip = true;
@ -1374,9 +1391,16 @@ impl Minifier<'_> {
.clone()
.into_iter()
.filter(|attribute| match attribute.name {
js_word!("src") if is_script_tag => {
need_skip = true;
true
}
js_word!("type") => {
if let Some(value) = &attribute.value {
if value.trim().to_ascii_lowercase() == "text/css" {
if (is_style_tag && value.trim().to_ascii_lowercase() == "text/css")
|| (is_script_tag && self.is_type_text_javascript(value))
{
false
} else {
need_skip = true;
@ -1406,18 +1430,31 @@ impl Minifier<'_> {
}
fn merge_text_children(&self, left: &Element, right: &Element) -> Vec<Child> {
let is_script_tag = matches!(left.namespace, Namespace::HTML | Namespace::SVG)
&& left.tag_name == js_word!("script")
&& matches!(right.namespace, Namespace::HTML | Namespace::SVG)
&& right.tag_name == js_word!("script");
let data = left.children.iter().chain(right.children.iter()).fold(
String::new(),
|mut acc, child| match child {
Child::Text(text) => {
Child::Text(text) if text.data.len() > 0 => {
acc.push_str(&text.data);
if is_script_tag {
acc.push(';');
}
acc
}
_ => acc,
},
);
if data.is_empty() {
return vec![];
}
vec![Child::Text(Text {
span: DUMMY_SP,
data: data.into(),
@ -2004,7 +2041,9 @@ impl Minifier<'_> {
}
let minified = match String::from_utf8(buf) {
Ok(minified) => minified,
// Avoid generating the sequence "</script" in JS code
// TODO move it to ecma codegen under the option?
Ok(minified) => minified.replace("</script>", "<\\/script>"),
_ => return None,
};
@ -2751,19 +2790,14 @@ impl VisitMut for Minifier<'_> {
Some(js_word!("module")) if self.need_minify_js() => {
text_type = Some(MinifierType::JsModule);
}
Some(
js_word!("text/javascript")
| js_word!("text/ecmascript")
| js_word!("text/jscript")
| js_word!("application/javascript")
| js_word!("application/x-javascript")
| js_word!("application/ecmascript"),
)
| None
if self.need_minify_js() =>
Some(value)
if self.need_minify_js() && self.is_type_text_javascript(&value) =>
{
text_type = Some(MinifierType::JsScript);
}
None if self.need_minify_js() => {
text_type = Some(MinifierType::JsScript);
}
Some(
js_word!("application/json")
| js_word!("application/ld+json")

View File

@ -1,6 +1,6 @@
<!doctype html><script defer>console.log()</script><script>console.log()</script><script>console.log()</script><script type=module>console.log()</script><script type=module>console.log()</script><script>window.jQuery||document.write('<script src="jquery.js"></script>')</script><script type=text/html>
<!doctype html><script defer>console.log()</script><script>console.log();console.log()</script><script type=module>console.log()</script><script type=module>console.log()</script><script>window.jQuery||document.write('<script src="jquery.js"><\/script>')</script><script type=text/html>
<div>
test
</div>
<!-- aa -->\n
</script><script type="">alert(1)</script><script type=modules>alert(1)</script><script>alert(1)</script><script>alert(1)</script><script type=module src=app.mjs></script><script nomodule defer src=classic-app-bundle.js></script><script>alert(1)</script><script type=text/vbscript>MsgBox("foo bar")</script><script type="">MsgBox("foo bar")</script><script type=;;;;;>MsgBox("foo bar")</script><script>alert(1)</script><script>alert(1)</script><script>alert(1)</script><script>alert(1)</script><script>alert(1)</script><script>alert(1)</script><script>alert(1)</script><script></script>
</script><script type="">alert(1)</script><script type=modules>alert(1)</script><script>alert(1);alert(1)</script><script type=module src=app.mjs></script><script nomodule defer src=classic-app-bundle.js></script><script>alert(1)</script><script type=text/vbscript>MsgBox("foo bar")</script><script type="">MsgBox("foo bar")</script><script type=;;;;;>MsgBox("foo bar")</script><script>alert(1)</script><script>alert(1)</script><script>alert(1)</script><script>alert(1)</script><script>alert(1)</script><script>alert(1)</script><script>alert(1)</script><script></script>

View File

@ -8,7 +8,7 @@
<div>baz</div>
<script></script><script>alert(1)</script><style>p{color:red}</style><script>alert(8)</script>
<script>alert(1)</script><style>p{color:red}</style><script>alert(8)</script>
<script>alert(10)</script>
<!-- @preserve foo -->
<!-- @copyright foo -->

View File

@ -10,7 +10,7 @@
<!-- foo --><div>baz</div><!-- bar
moo -->
<script></script><script>alert(1)</script><style>p{color:red}</style><script>alert(8)</script>
<script>alert(1)</script><style>p{color:red}</style><script>alert(8)</script>
<script>alert(10)</script>
<!-- @preserve foo -->
<!-- @copyright foo -->

View File

@ -2,7 +2,7 @@
<div>baz</div>
<script></script><script>alert(1)</script><style>p{color:red}</style><script>alert(8)</script>
<script>alert(1)</script><style>p{color:red}</style><script>alert(8)</script>
<script>alert(10)</script>
<!--! test -->
<!-- ko if: someExpressionGoesHere --><li>test</li><!-- /ko -->

View File

@ -0,0 +1,73 @@
<!doctype html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<div>breaker</div>
<script>var a = "test";console.log(a)</script>
<script>var b = "test";console.log(b)</script>
<div>breaker</div>
<script type="text/javascript">var a = "test";console.log(a);</script>
<script type="application/javascript">var b = "test";console.log(b);</script>
<div>breaker</div>
<script type="application/javascript">var a = "test";console.log(a)</script>
<script>var b = "test";console.log(b)</script>
<div>breaker</div>
<script type="application/javascript;version=1.8">var a = "test";console.log(a)</script>
<script>var b = "test";console.log(b)</script>
<div>breaker</div>
<script>var a = "test";console.log(a)</script>
<script crossorigin="use-credentials">var b = "test";console.log(b)</script>
<div>breaker</div>
<script type="text/javascript" crossorigin="use-credentials">var a = "test";console.log(a)</script>
<script crossorigin="use-credentials">var b = "test";console.log(b)</script>
<div>breaker</div>
<script>
(function test() {
let test = "1";
console.log(test);
})();
</script>
<script>
let test = "1";
console.log(test);
</script>
<div>breaker</div>
<script type="module">var a = "test";console.log(a)</script>
<script>var b = "test";console.log(b)</script>
<div>breaker</div>
<script type="module">var a = "test";console.log(a)</script>
<script type="module">var b = "test";console.log(b)</script>
<div>breaker</div>
<script>var a = "test";console.log(a)</script>
<script type="unknown">var b = "test";console.log(b)</script>
<div>breaker</div>
<script src="test.js"></script>
<script>var b = "test";console.log(b)</script>
<div>breaker</div>
<script></script>
</body>
</html>

View File

@ -0,0 +1,44 @@
<!doctype html><html lang=en><title>Document</title><div>breaker</div>
<script>var o="test";console.log(o);var e="test";console.log(e)</script>
<div>breaker</div>
<script>var o="test";console.log(o);var e="test";console.log(e)</script>
<div>breaker</div>
<script>var o="test";console.log(o);var e="test";console.log(e)</script>
<div>breaker</div>
<script>var o="test";console.log(o);var e="test";console.log(e)</script>
<div>breaker</div>
<script>var o="test";console.log(o)</script><script crossorigin=use-credentials>var o="test";console.log(o)</script>
<div>breaker</div>
<script crossorigin=use-credentials>var o="test";console.log(o);var e="test";console.log(e)</script>
<div>breaker</div>
<script>(function o(){let o="1";console.log(o)})();let o="1";console.log(o)</script>
<div>breaker</div>
<script type=module>var o="test";console.log(o)</script><script>var o="test";console.log(o)</script>
<div>breaker</div>
<script type=module>var o="test";console.log(o)</script><script type=module>var o="test";console.log(o)</script>
<div>breaker</div>
<script>var o="test";console.log(o)</script><script type=unknown>var b = "test";console.log(b)</script>
<div>breaker</div>
<script src=test.js></script><script>var o="test";console.log(o)</script>
<div>breaker</div>

View File

@ -183,5 +183,8 @@
</math>
<script blocking="render a">console.log("block");</script>
<script>(function(test){ var test = "test" + Math.random() + test; var foo = 1; var bar = 2 + Math.random(); alert(foo + " " + bar); console.log(Math.random()) })("test")</script>
<script>window.jQuery || document.write('<script src="jquery.js"><\/script>')</script>
<div>test</div>
<script></script>
</body>
</html>

View File

@ -19,7 +19,11 @@
</script><script id=data type=application/json>{"foo":"bar"}</script><script>
broken broken
</script><script>var a=3</script><script>var a=9</script><script type=module>import t from"foo.js";let o=58800,e="testtest"</script><script>let e=new Date;alert("Today's date is "+e)</script><script src=javascript.js>
;
var a = 1 + 2;
;;
var a = 3 + 6;
;</script><script type=module>import t from"foo.js";let o=58800,e="testtest"</script><script>let e=new Date;alert("Today's date is "+e)</script><script src=javascript.js>
@ -27,7 +31,7 @@
var q = "WRONG" ;
</script><script>let t="testtest"</script><script>alert("<!--")</script><script>alert("<!-- foo -->")</script><script>alert("-->")</script><script></script><script>alert("testtest")</script><script>function a(){var a=1,n=2;alert(a+" "+n)}</script><script></script><h2>Party coffee cake recipe</h2>
</script><script>let t="testtest";alert("<!--");alert("<!-- foo -->");alert("-->");alert("testtest");function e(){var t=1,e=2;alert(t+" "+e)}</script><h2>Party coffee cake recipe</h2>
<p>
<i>by Mary Stone, 2018-03-10</i>
</p>
@ -54,4 +58,5 @@
alert('test')
</script>
</math>
<script blocking="a render">console.log("block")</script><script>(function(t){var t="test"+Math.random()+t,a=1,o=2+Math.random();alert(a+" "+o);console.log(Math.random())})("test")</script>
<script blocking="a render">console.log("block")</script><script>(function(t){var t="test"+Math.random()+t,r=1,o=2+Math.random();alert(r+" "+o);console.log(Math.random())})("test");window.jQuery||document.write('<script src="jquery.js"><\/script>')</script>
<div>test</div>

View File

@ -29,4 +29,4 @@
foo
baz
</pre> <div> a <input> c </div> <div>Empty </div> <!--[if lte IE 6]> <span>A</span> <span title=" sigificant whitespace ">blah blah</span> <![endif]--> <div> <a href=#> <span> <b> foo </b> <i> bar </i> </span> </a> </div> <div>a b</div> <div>a b c d</div> <div> text </div> <span> text </span> <span> text </span> <div> <style>a{color:red}</style> <span>test</span> <style>a{color:red}</style> </div> <div> <style>a{color:red}</style> </div> <div> <style>a{color:red}</style> <span>test</span> <span>test</span> <style>a{color:red}</style> </div> <div> <script>console.log("test")</script><script>console.log("test")</script> </div>
</pre> <div> a <input> c </div> <div>Empty </div> <!--[if lte IE 6]> <span>A</span> <span title=" sigificant whitespace ">blah blah</span> <![endif]--> <div> <a href=#> <span> <b> foo </b> <i> bar </i> </span> </a> </div> <div>a b</div> <div>a b c d</div> <div> text </div> <span> text </span> <span> text </span> <div> <style>a{color:red}</style> <span>test</span> <style>a{color:red}</style> </div> <div> <style>a{color:red}</style> </div> <div> <style>a{color:red}</style> <span>test</span> <span>test</span> <style>a{color:red}</style> </div> <div> <script>console.log("test");console.log("test")</script> </div>

View File

@ -29,4 +29,4 @@
foo
baz
</pre><div>a <input> c</div><div>Empty</div><!--[if lte IE 6]> <span>A</span> <span title=" sigificant whitespace ">blah blah</span> <![endif]--><div><a href=#> <span><b>foo </b><i> bar </i></span></a></div><div>a b</div><div>a b c d</div><div>text</div><span> text </span><span> text </span><div><span>test</span> <span>test</span></div><div><span>test</span> <command>test</command><span>test</span></div><div><span>test</span><link rel=stylesheet href=""> <span>test</span></div><div><span>test</span><meta name=content> <span>test</span></div><div><span>test</span><script>console.log("test")</script> <span>test</span></div><div><span>test</span><style>a{color:red}</style> <span>test</span></div><div><span>test</span><title>test</title> <span>test</span></div><div><meta name=test><meta name=test></div><div><link rel=stylesheet href=""><link rel=stylesheet href=""></div><div><script>console.log("test")</script><script>console.log("test")</script></div><div><script>console.log("test")</script> <span>test</span><script>console.log("test")</script></div><div><style>a{color:red}</style></div><div><script>console.log("test")</script><style>a{color:red}</style></div><div><span itemscope><meta itemprop=name content="The Castle">test</span> <span>test</span></div><div><meta name=test></div><div><style>a{color:red}</style></div><div><meta name=test><div>test</div><meta name=test></div><div><meta name=test> <span>test</span><meta name=test></div><svg> <title>test</title> <metadata>test</metadata> <desc>test</desc> </svg><svg> <a>test</a> <a>test</a> </svg><svg><text x=20 y=35><tspan font-weight=bold fill=red>This is bold and red</tspan> <tspan font-weight=bold fill=red>This is bold and red</tspan></text></svg><svg> <tspan>test</tspan><foreignObject>test</foreignObject></svg><svg><text x=20 y=35><tspan font-weight=bold fill=red>This is bold and red</tspan> <tspan font-weight=bold fill=red>This is bold and red</tspan></text></svg><svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice" style=width:100%;height:100%;position:absolute;top:0;left:0;z-index:-1> <linearGradient id=gradient><stop class=begin offset=0% /><stop class=end offset=100% /></linearGradient><rect width=100 height=100 style=fill:url(#gradient) /> <circle cx=50 cy=50 r=30 style=fill:url(#gradient) /> </svg><svg> <script>console.log("test")</script></svg><svg> <style>a{color:red}</style></svg><div><span>test</span> a b <span>test</span> <span>test</span> a b <span>test</span> <span>test</span> a b <span>test</span></div><div><foo-bar> <span>test</span> </foo-bar> <foo-bar> <span>test</span> </foo-bar></div><div><svg> <linearGradient id=gradient /> </svg><span>a</span></div>
</pre><div>a <input> c</div><div>Empty</div><!--[if lte IE 6]> <span>A</span> <span title=" sigificant whitespace ">blah blah</span> <![endif]--><div><a href=#> <span><b>foo </b><i> bar </i></span></a></div><div>a b</div><div>a b c d</div><div>text</div><span> text </span><span> text </span><div><span>test</span> <span>test</span></div><div><span>test</span> <command>test</command><span>test</span></div><div><span>test</span><link rel=stylesheet href=""> <span>test</span></div><div><span>test</span><meta name=content> <span>test</span></div><div><span>test</span><script>console.log("test")</script> <span>test</span></div><div><span>test</span><style>a{color:red}</style> <span>test</span></div><div><span>test</span><title>test</title> <span>test</span></div><div><meta name=test><meta name=test></div><div><link rel=stylesheet href=""><link rel=stylesheet href=""></div><div><script>console.log("test");console.log("test")</script></div><div><script>console.log("test")</script> <span>test</span><script>console.log("test")</script></div><div><style>a{color:red}</style></div><div><script>console.log("test")</script><style>a{color:red}</style></div><div><span itemscope><meta itemprop=name content="The Castle">test</span> <span>test</span></div><div><meta name=test></div><div><style>a{color:red}</style></div><div><meta name=test><div>test</div><meta name=test></div><div><meta name=test> <span>test</span><meta name=test></div><svg> <title>test</title> <metadata>test</metadata> <desc>test</desc> </svg><svg> <a>test</a> <a>test</a> </svg><svg><text x=20 y=35><tspan font-weight=bold fill=red>This is bold and red</tspan> <tspan font-weight=bold fill=red>This is bold and red</tspan></text></svg><svg> <tspan>test</tspan><foreignObject>test</foreignObject></svg><svg><text x=20 y=35><tspan font-weight=bold fill=red>This is bold and red</tspan> <tspan font-weight=bold fill=red>This is bold and red</tspan></text></svg><svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice" style=width:100%;height:100%;position:absolute;top:0;left:0;z-index:-1> <linearGradient id=gradient><stop class=begin offset=0% /><stop class=end offset=100% /></linearGradient><rect width=100 height=100 style=fill:url(#gradient) /> <circle cx=50 cy=50 r=30 style=fill:url(#gradient) /> </svg><svg> <script>console.log("test")</script></svg><svg> <style>a{color:red}</style></svg><div><span>test</span> a b <span>test</span> <span>test</span> a b <span>test</span> <span>test</span> a b <span>test</span></div><div><foo-bar> <span>test</span> </foo-bar> <foo-bar> <span>test</span> </foo-bar></div><div><svg> <linearGradient id=gradient /> </svg><span>a</span></div>