Skip to content

Commit

Permalink
feat(html/minifier): Sort unordered values in attributes (#5035)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-akait committed Jun 30, 2022
1 parent 3eefa63 commit 26cfeff
Show file tree
Hide file tree
Showing 21 changed files with 153 additions and 36 deletions.
78 changes: 60 additions & 18 deletions crates/swc_html_minifier/src/lib.rs
Expand Up @@ -221,10 +221,10 @@ static COMMA_SEPARATED_SVG_ATTRIBUTES: &[(&str, &str)] = &[("style", "media")];

static SPACE_SEPARATED_GLOBAL_ATTRIBUTES: &[&str] = &[
"class",
"part",
"itemtype",
"itemref",
"itemprop",
"itemref",
"itemtype",
"part",
"accesskey",
"aria-describedby",
"aria-labelledby",
Expand All @@ -238,15 +238,16 @@ static SPACE_SEPARATED_HTML_ATTRIBUTES: &[(&str, &str)] = &[
("area", "ping"),
("link", "rel"),
("link", "sizes"),
("input", "autocomplete"),
("form", "autocomplete"),
("link", "blocking"),
("iframe", "sandbox"),
("td", "headers"),
("th", "headers"),
("output", "for"),
("link", "blocking"),
("script", "blocking"),
("style", "blocking"),
("input", "autocomplete"),
("form", "rel"),
("form", "autocomplete"),
];

enum CssMinificationMode {
Expand Down Expand Up @@ -331,6 +332,8 @@ struct Minifier {
minify_css: bool,
minify_additional_attributes: Option<Vec<(CachedRegex, MinifierType)>>,
minify_additional_scripts_content: Option<Vec<(CachedRegex, MinifierType)>>,

sort_space_separated_attribute_values: bool,
}

fn get_white_space(namespace: Namespace, tag_name: &str) -> WhiteSpace {
Expand Down Expand Up @@ -418,16 +421,40 @@ impl Minifier {
}
}

fn is_additional_minifier_attribute(&self, name: &str) -> Option<MinifierType> {
if let Some(minify_additional_attributes) = &self.minify_additional_attributes {
for item in minify_additional_attributes {
if item.0.is_match(name) {
return Some(item.1.clone());
}
}
fn is_attribute_value_unordered_set(&self, element: &Element, attribute_name: &str) -> bool {
if matches!(
attribute_name,
"class" | "part" | "itemprop" | "itemref" | "itemtype"
) {
return true;
}

None
match element.namespace {
Namespace::HTML => match &*element.tag_name {
"link" if attribute_name == "blocking" => true,
"script" if attribute_name == "blocking" => true,
"style" if attribute_name == "blocking" => true,
"output" if attribute_name == "for" => true,
"td" if attribute_name == "headers" => true,
"th" if attribute_name == "headers" => true,
"form" if attribute_name == "rel" => true,
"a" if attribute_name == "rel" => true,
"area" if attribute_name == "rel" => true,
"link" if attribute_name == "rel" => true,
"iframe" if attribute_name == "sandbox" => true,
"link"
if self.element_has_attribute_with_value(
element,
"rel",
&["icon", "apple-touch-icon", "apple-touch-icon-precomposed"],
) && attribute_name == "sizes" =>
{
true
}
_ => false,
},
_ => false,
}
}

fn element_has_attribute_with_value(
Expand Down Expand Up @@ -866,6 +893,18 @@ impl Minifier {
collapsed
}

fn is_additional_minifier_attribute(&self, name: &str) -> Option<MinifierType> {
if let Some(minify_additional_attributes) = &self.minify_additional_attributes {
for item in minify_additional_attributes {
if item.0.is_match(name) {
return Some(item.1.clone());
}
}
}

None
}

fn minify_children(&mut self, children: &mut Vec<Child>) {
let (namespace, tag_name) = match &self.current_element {
Some(element) => (element.namespace, &element.tag_name),
Expand Down Expand Up @@ -1472,6 +1511,7 @@ impl Minifier {
minify_css: self.minify_css,
minify_additional_scripts_content: self.minify_additional_scripts_content.clone(),
minify_additional_attributes: self.minify_additional_attributes.clone(),
sort_space_separated_attribute_values: self.sort_space_separated_attribute_values,
};

match document_or_document_fragment {
Expand Down Expand Up @@ -1735,12 +1775,12 @@ impl VisitMut for Minifier {
}
}

if &*n.name == "class" {
if self.sort_space_separated_attribute_values
&& self.is_attribute_value_unordered_set(current_element, &n.name)
{
let mut values = value.split_whitespace().collect::<Vec<_>>();

if &*n.name == "class" {
values.sort_unstable();
}
values.sort_unstable();

value = values.join(" ");
} else if self.is_event_handler_attribute(&n.name) {
Expand Down Expand Up @@ -2025,6 +2065,8 @@ fn create_minifier(context_element: Option<&Element>, options: &MinifyOptions) -
minify_css: options.minify_css,
minify_additional_attributes: options.minify_additional_attributes.clone(),
minify_additional_scripts_content: options.minify_additional_scripts_content.clone(),

sort_space_separated_attribute_values: options.sort_space_separated_attribute_values,
}
}

Expand Down
18 changes: 10 additions & 8 deletions crates/swc_html_minifier/src/option.rs
Expand Up @@ -49,14 +49,6 @@ pub struct MinifyOptions {
pub minify_json: bool,
#[serde(default = "true_by_default")]
pub minify_css: bool,
/// Allow to compress value of custom attributes,
/// i.e. `<div data-js="myFunction(100 * 2, 'foo' + 'bar')"></div>`
///
/// The first item is tag_name
/// The second is attribute name
/// The third is type of minifier
#[serde(default)]
pub minify_additional_attributes: Option<Vec<(CachedRegex, MinifierType)>>,
// Allow to compress value of custom script elements,
// i.e. `<script type="text/html"><div><!-- text --> <div data-foo="bar> Text </div></script>`
//
Expand All @@ -65,6 +57,16 @@ pub struct MinifyOptions {
// The third is type of minifier
#[serde(default)]
pub minify_additional_scripts_content: Option<Vec<(CachedRegex, MinifierType)>>,
/// Allow to compress value of custom attributes,
/// i.e. `<div data-js="myFunction(100 * 2, 'foo' + 'bar')"></div>`
///
/// The first item is tag_name
/// The second is attribute name
/// The third is type of minifier
pub minify_additional_attributes: Option<Vec<(CachedRegex, MinifierType)>>,
/// Sorting the values of `class`, `rel`, etc. of attributes
#[serde(default = "true_by_default")]
pub sort_space_separated_attribute_values: bool,
}

/// Implement default using serde.
Expand Down
@@ -1,2 +1,2 @@
<!doctype html><html lang=en><title>Document</title><link rel=stylesheet href=test.css media="screen and (min-width:1024px)"><a rel="foo bar baz"></a>
<!doctype html><html lang=en><title>Document</title><link rel=stylesheet href=test.css media="screen and (min-width:1024px)"><a rel="bar baz foo"></a>
<div onclick='javascript:alert("test")'></div>
@@ -0,0 +1,3 @@
{
"sortSpaceSeparatedAttributeValues": false
}
@@ -0,0 +1,10 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div class="b a d"></div>
</body>
</html>
@@ -0,0 +1 @@
<!doctype html><html lang=en><meta charset=UTF-8><title>Document</title><div class="b a d"></div>
Expand Up @@ -21,6 +21,12 @@
<td headers="phone">+45342323</td>
<td headers="addr">Rosevn 56,4300 Sandnes,Norway</td>
</tr>
<tr>
<td headers="name a ">John Doe</td>
<td headers="email a">someone@example.com</td>
<td headers="phone a">+45342323</td>
<td headers="addr b">Rosevn 56,4300 Sandnes,Norway</td>
</tr>
</table>
</body>
</html>
Expand Up @@ -11,4 +11,10 @@
<td headers=phone>+45342323</td>
<td headers=addr>Rosevn 56,4300 Sandnes,Norway</td>
</tr>
<tr>
<td headers="a name">John Doe</td>
<td headers="a email">someone@example.com</td>
<td headers="a phone">+45342323</td>
<td headers="addr b">Rosevn 56,4300 Sandnes,Norway</td>
</tr>
</table>
Expand Up @@ -11,7 +11,7 @@
in excellent condition<br>
<link itemprop=availability href=http://schema.org/InStock>In stock! Order now!
</span>
<dl itemscope itemtype="https://md.example.com/loco https://md.example.com/lighting">
<dl itemscope itemtype="https://md.example.com/lighting https://md.example.com/loco">
<dt>Name:
<dd itemprop="name name">Tank Locomotive (DB 80)
<dt>Product code:
Expand Down
14 changes: 14 additions & 0 deletions crates/swc_html_minifier/tests/fixture/attribute/link/input.html
Expand Up @@ -8,6 +8,20 @@
<link rel="stylesheet" href="c.css" type=" text/css ">
<link rel="stylesheet" href="d.css" type="">
<link rel="stylesheet" href="d.css" type="unknown/unknown">
<link href="default.css" rel="stylesheet" title="Default Style">
<link href="fancy.css" rel="stylesheet alternate" title="Fancy">
<link href="basic.css" rel="stylesheet alternate" title="Basic">
<link rel="stylesheet foo bar" href="d.css">
<link rel="stylesheet foo bar" href="d.css" blocking="render a">
<link rel="icon" href="demo_icon.gif" type="image/gif" sizes="32x32 16x16">
<link rel="apple-touch-icon-precomposed" sizes="512x512 114x114"
href="apple-icon-114.png" type="image/png">
<link rel="apple-touch-icon" sizes="512x512 114x114"
href="apple-icon-114.png" type="image/png">
<link rel="icon" href="/favicon.ico" sizes="any"><!-- 32×32 -->
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png"><!-- 180×180 -->
<link rel="manifest" href="/manifest.webmanifest">
</head>
<body>
<div>test</div>
Expand Down
@@ -1 +1 @@
<!doctype html><html lang=en><title>Document</title><link rel=stylesheet href=a.css><link rel=stylesheet href=b.css><link rel=stylesheet href=b.css><link rel=stylesheet href=c.css><link rel=stylesheet href=d.css type=""><link rel=stylesheet href=d.css type=unknown/unknown><div>test</div>
<!doctype html><html lang=en><title>Document</title><link rel=stylesheet href=a.css><link rel=stylesheet href=b.css><link rel=stylesheet href=b.css><link rel=stylesheet href=c.css><link rel=stylesheet href=d.css type=""><link rel=stylesheet href=d.css type=unknown/unknown><link href=default.css rel=stylesheet title="Default Style"><link href=fancy.css rel="alternate stylesheet" title=Fancy><link href=basic.css rel="alternate stylesheet" title=Basic><link rel="bar foo stylesheet" href=d.css><link rel="bar foo stylesheet" href=d.css blocking="a render"><link rel=icon href=demo_icon.gif type=image/gif sizes="16x16 32x32"><link rel=apple-touch-icon-precomposed sizes="114x114 512x512" href=apple-icon-114.png type=image/png><link rel=apple-touch-icon sizes="114x114 512x512" href=apple-icon-114.png type=image/png><link rel=icon href=/favicon.ico sizes=any><link rel=icon href=/icon.svg type=image/svg+xml><link rel=apple-touch-icon href=/apple-touch-icon.png><link rel=manifest href=/manifest.webmanifest><div>test</div>
@@ -1,7 +1,7 @@
<!doctype html><html lang=en><title>Document</title><body><style>c-e::part(textspan){color:red}</style>

<template id=c-e-template>
<span part="textspan a b c">This text will be red</span>
<span part="a b c textspan">This text will be red</span>
</template>

<c-e></c-e>
Expand Up @@ -7,5 +7,9 @@
<a href="#" rel="value1 nofollow">Link</a>
<a href="#" rel=" value1 nofollow ">Link</a>
<a href="#" rel=" nofollow ">Link</a>
<form rel="noreferrer nofollow" action="mypage.php">
<input type="search" placeholder="search here" />
<input type="button" value="search" />
</form>
</body>
</html>
@@ -1,3 +1,7 @@
<!doctype html><html lang=en><title>Document</title><a href=# rel="value1 nofollow">Link</a>
<a href=# rel="value1 nofollow">Link</a>
<a href=# rel=nofollow>Link</a>
<!doctype html><html lang=en><title>Document</title><a href=# rel="nofollow value1">Link</a>
<a href=# rel="nofollow value1">Link</a>
<a href=# rel=nofollow>Link</a>
<form rel="nofollow noreferrer" action=mypage.php>
<input type=search placeholder="search here">
<input type=button value=search>
</form>
@@ -1 +1 @@
<!doctype html><html lang=en><meta charset=UTF-8><meta name=viewport content="width=device-width,user-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0"><meta http-equiv=X-UA-Compatible content="ie=edge"><title>Document</title><iframe sandbox="allow-same-origin allow-scripts allow-popups allow-forms" src=https://platform.twitter.com/widgets/tweet_button.html style=border:0;width:130px;height:20px></iframe>
<!doctype html><html lang=en><meta charset=UTF-8><meta name=viewport content="width=device-width,user-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0"><meta http-equiv=X-UA-Compatible content="ie=edge"><title>Document</title><iframe sandbox="allow-forms allow-popups allow-same-origin allow-scripts" src=https://platform.twitter.com/widgets/tweet_button.html style=border:0;width:130px;height:20px></iframe>
16 changes: 16 additions & 0 deletions crates/swc_html_minifier/tests/fixture/element/output/input.html
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<title>Document</title>
</head>
<body>
<form oninput="result.value=parseInt(a.value)+parseInt(b.value)">
<input type="range" id="b" name="b" value="50" /> +
<input type="number" id="a" name="a" value="10" /> =
<output name="result" for="b a">60</output>
</form>
</body>
</html>
@@ -0,0 +1,5 @@
<!doctype html><html lang=en><meta charset=UTF-8><meta name=viewport content="width=device-width,user-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0"><title>Document</title><form oninput="result.value=parseInt(a.value)+parseInt(b.value)">
<input type=range id=b name=b value=50> +
<input type=number id=a name=a value=10> =
<output name=result for="a b">60</output>
</form>
Expand Up @@ -181,5 +181,6 @@ <h2>Party coffee cake recipe</h2>
alert('test')
</script>
</math>
<script blocking="render a">console.log("block");</script>
</body>
</html>
Expand Up @@ -53,4 +53,5 @@

alert('test')
</script>
</math>
</math>
<script blocking="a render">console.log("block")</script>
Expand Up @@ -60,5 +60,6 @@
}
</style>
</math>
<style blocking="render a">a { color: red }</style>
</body>
</html>
Expand Up @@ -23,4 +23,5 @@
color: red
}
</style>
</math>
</math>
<style blocking="a render">a{color:red}</style>

1 comment on commit 26cfeff

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: 26cfeff Previous: e1aa937 Ratio
es/full/minify/libraries/antd 1673598306 ns/iter (± 41348611) 1727364091 ns/iter (± 54532047) 0.97
es/full/minify/libraries/d3 436453853 ns/iter (± 16628311) 429979035 ns/iter (± 4506701) 1.02
es/full/minify/libraries/echarts 1703856939 ns/iter (± 32568965) 1664779840 ns/iter (± 24718591) 1.02
es/full/minify/libraries/jquery 110236323 ns/iter (± 7808977) 105003017 ns/iter (± 4254857) 1.05
es/full/minify/libraries/lodash 135052625 ns/iter (± 9281647) 131544173 ns/iter (± 4528322) 1.03
es/full/minify/libraries/moment 53617625 ns/iter (± 2335592) 57582398 ns/iter (± 3173103) 0.93
es/full/minify/libraries/react 19006061 ns/iter (± 398219) 19068493 ns/iter (± 501602) 1.00
es/full/minify/libraries/terser 616252559 ns/iter (± 16910644) 618229835 ns/iter (± 22007363) 1.00
es/full/minify/libraries/three 568537856 ns/iter (± 5290300) 567282018 ns/iter (± 11626451) 1.00
es/full/minify/libraries/typescript 3541668602 ns/iter (± 114323953) 3559981447 ns/iter (± 69841661) 0.99
es/full/minify/libraries/victory 746877779 ns/iter (± 35673301) 749315401 ns/iter (± 12156272) 1.00
es/full/minify/libraries/vue 153594407 ns/iter (± 15074563) 156586585 ns/iter (± 3336991) 0.98
es/full/codegen/es3 32883 ns/iter (± 371) 32810 ns/iter (± 903) 1.00
es/full/codegen/es5 32769 ns/iter (± 769) 32809 ns/iter (± 1526) 1.00
es/full/codegen/es2015 32904 ns/iter (± 839) 32848 ns/iter (± 418) 1.00
es/full/codegen/es2016 33050 ns/iter (± 1523) 32798 ns/iter (± 572) 1.01
es/full/codegen/es2017 32886 ns/iter (± 434) 32857 ns/iter (± 563) 1.00
es/full/codegen/es2018 32797 ns/iter (± 447) 32805 ns/iter (± 1033) 1.00
es/full/codegen/es2019 33034 ns/iter (± 710) 32863 ns/iter (± 997) 1.01
es/full/codegen/es2020 32910 ns/iter (± 1300) 32858 ns/iter (± 1225) 1.00
es/full/all/es3 202551019 ns/iter (± 9276163) 197935526 ns/iter (± 11493587) 1.02
es/full/all/es5 188408470 ns/iter (± 11657720) 185281025 ns/iter (± 6656809) 1.02
es/full/all/es2015 152272720 ns/iter (± 8544818) 147599855 ns/iter (± 5394853) 1.03
es/full/all/es2016 151152173 ns/iter (± 7070423) 154102708 ns/iter (± 11544819) 0.98
es/full/all/es2017 148003386 ns/iter (± 12181427) 150505397 ns/iter (± 6463510) 0.98
es/full/all/es2018 135097622 ns/iter (± 6917501) 149309296 ns/iter (± 5816013) 0.90
es/full/all/es2019 135972348 ns/iter (± 6465223) 147273785 ns/iter (± 7370877) 0.92
es/full/all/es2020 129838367 ns/iter (± 7717834) 140181736 ns/iter (± 6566415) 0.93
es/full/parser 725033 ns/iter (± 70082) 707473 ns/iter (± 24071) 1.02
es/full/base/fixer 29722 ns/iter (± 443) 29867 ns/iter (± 486) 1.00
es/full/base/resolver_and_hygiene 90213 ns/iter (± 6323) 87353 ns/iter (± 1571) 1.03
serialization of ast node 215 ns/iter (± 8) 215 ns/iter (± 5) 1
serialization of serde 222 ns/iter (± 12) 225 ns/iter (± 7) 0.99

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.