Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(html/minifier): improve the remove_redundant_attributes option #6197

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
150 changes: 112 additions & 38 deletions crates/swc_html_minifier/src/lib.rs
Expand Up @@ -18,7 +18,7 @@ use swc_html_visit::{VisitMut, VisitMutWith};

use crate::option::{
CollapseWhitespaces, CssOptions, JsOptions, JsParserOptions, JsonOptions, MinifierType,
MinifyCssOption, MinifyJsOption, MinifyJsonOption, MinifyOptions,
MinifyCssOption, MinifyJsOption, MinifyJsonOption, MinifyOptions, RemoveRedundantAttributes,
};

pub mod option;
Expand Down Expand Up @@ -563,44 +563,82 @@ impl Minifier<'_> {

match namespace {
Namespace::HTML | Namespace::SVG => {
// Legacy attributes, not in spec
if *tag_name == 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,
_ => {}
match *tag_name {
js_word!("html") => match attribute.name {
js_word!("xmlns") => {
if &*attribute_value.trim().to_ascii_lowercase()
== "http://www.w3.org/1999/xhtml"
{
return true;
}
}
js_word!("xmlns:xlink") => {
if &*attribute_value.trim().to_ascii_lowercase()
== "http://www.w3.org/1999/xlink"
{
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!("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!("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"
{
return true;
}
}

js_word!("svg") => {
if attribute.name == js_word!("xmlns")
&& &*attribute_value.trim().to_ascii_lowercase()
== "http://www.w3.org/2000/svg"
{
return true;
}
}
_ => {}
}

let default_attributes = if namespace == Namespace::HTML {
Expand Down Expand Up @@ -635,7 +673,43 @@ impl Minifier<'_> {

match (attribute_info.inherited, &attribute_info.initial) {
(None, Some(initial)) | (Some(false), Some(initial)) => {
initial == normalized_value
match self.options.remove_redundant_attributes {
RemoveRedundantAttributes::None => false,
RemoveRedundantAttributes::Smart => {
if initial == normalized_value {
// It is safe to remove deprecated redundant attributes, they
// should not be used
if attribute_info.deprecated == Some(true) {
return true;
}

// It it safe to remove svg redundant attributes, they used for
// styling
if namespace == Namespace::SVG {
return true;
}

// It it safe to remove redundant attributes for metadata
// elements
if namespace == Namespace::HTML
&& matches!(
*tag_name,
js_word!("base")
| js_word!("link")
| js_word!("noscript")
| js_word!("script")
| js_word!("style")
| js_word!("title")
)
{
return true;
}
}

false
}
RemoveRedundantAttributes::All => initial == normalized_value,
}
}
_ => false,
}
Expand Down Expand Up @@ -1607,7 +1681,7 @@ impl Minifier<'_> {
let result = child_will_be_retained(&mut child, &mut new_children, children);

if result {
if self.options.remove_redundant_attributes
if self.options.remove_empty_metadata_elements
&& self.is_empty_metadata_element(&child)
{
let need_continue = {
Expand Down Expand Up @@ -2242,7 +2316,7 @@ impl VisitMut for Minifier<'_> {

for (i, i1) in n.attributes.iter().enumerate() {
if i1.value.is_some() {
if self.options.remove_redundant_attributes
if self.options.remove_redundant_attributes != RemoveRedundantAttributes::None
&& self.is_default_attribute_value(n.namespace, &n.tag_name, i1)
{
remove_list.push(i);
Expand Down
36 changes: 29 additions & 7 deletions crates/swc_html_minifier/src/option.rs
Expand Up @@ -44,6 +44,32 @@ pub enum CollapseWhitespaces {
OnlyMetadata,
}

impl Default for CollapseWhitespaces {
fn default() -> Self {
CollapseWhitespaces::OnlyMetadata
}
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[serde(rename_all = "kebab-case")]
pub enum RemoveRedundantAttributes {
Copy link
Member

Choose a reason for hiding this comment

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

Let's implement Default for this type instead of using #[serde(default = "")].

Copy link
Member

Choose a reason for hiding this comment

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

You can then change #[serde(default = "...")] to #[serde(default)]

/// Do not remove redundant attributes
None,
/// Remove all redundant attributes
All,
/// Remove deprecated and svg redundant (they used for styling) and `xmlns`
/// attributes (for example the `type` attribute for the `style` tag and
/// `xmlns` for svg)
Smart,
}

impl Default for RemoveRedundantAttributes {
fn default() -> Self {
RemoveRedundantAttributes::Smart
}
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
Expand Down Expand Up @@ -116,7 +142,7 @@ pub struct CssOptions {
pub struct MinifyOptions {
#[serde(default)]
pub force_set_html5_doctype: bool,
#[serde(default = "default_collapse_whitespaces")]
#[serde(default)]
pub collapse_whitespaces: CollapseWhitespaces,
// Remove safe empty elements with metadata content, i.e. the `script` and `style` element
// without content and attributes, `meta` and `link` elements without attributes and etc
Expand All @@ -136,8 +162,8 @@ pub struct MinifyOptions {
/// libraries
#[serde(default = "true_by_default")]
pub remove_empty_attributes: bool,
#[serde(default = "true_by_default")]
pub remove_redundant_attributes: bool,
#[serde(default)]
pub remove_redundant_attributes: RemoveRedundantAttributes,
#[serde(default = "true_by_default")]
pub collapse_boolean_attributes: bool,
/// Merge the same metadata elements into one (for example, consecutive
Expand Down Expand Up @@ -184,10 +210,6 @@ impl Default for MinifyOptions {
}
}

const fn default_collapse_whitespaces() -> CollapseWhitespaces {
CollapseWhitespaces::OnlyMetadata
}

const fn true_by_default() -> bool {
true
}
Expand Down
Expand Up @@ -19,7 +19,7 @@
<ul compact></ul>
<video src="" controls autoplay></video>
<script async defer></script>
<input autofocus checked disabled>
<input type=text autofocus checked disabled>
<button formnovalidate></button>
<div allowfullscreen async autofocus autoplay checked compact controls declare default defaultchecked defaultmuted defaultselected defer disabled enabled formnovalidate hidden indeterminate inert ismap itemscope loop multiple muted nohref noresize noshade novalidate nowrap open pauseonexit readonly required reversed scoped seamless selected sortable truespeed typemustmatch visible></div>

Expand Down
@@ -1,9 +1,9 @@
<!doctype html><html lang=en><title>Document</title><table width=200 border=1 align=center cellpadding=4 cellspacing=0>
<tr>
<th scope=col>Cell 1</th>
<th colspan=1 rowspan=1 scope=col>Cell 1</th>
</tr>
<tr>
<td class=test bgcolor=#FBF0DB>
<td class=test colspan=1 rowspan=1 bgcolor=#FBF0DB>
Cell 1
</td>
</tr>
Expand Down
@@ -1 +1 @@
<!doctype html><html lang=en><title>Document</title><input class=form-control id={{vm.formInputName}} name={{vm.formInputName}} placeholder=YYYY-MM-DD date-range-picker data-ng-model=vm.value data-ng-model-options="{ debounce: 1000 }" data-ng-pattern=vm.options.format data-options=vm.datepickerOptions>
<!doctype html><html lang=en><title>Document</title><input class=form-control type=text id={{vm.formInputName}} name={{vm.formInputName}} placeholder=YYYY-MM-DD date-range-picker data-ng-model=vm.value data-ng-model-options="{ debounce: 1000 }" data-ng-pattern=vm.options.format data-options=vm.datepickerOptions>

This file was deleted.

This file was deleted.

This file was deleted.

@@ -1,10 +1,10 @@
<!doctype html><meta charset=utf-8><title>Test</title><form action=handler.php method=post>
<input name=str>
<input type=text name=str>
<input type=submit value=send>
</form>
<form action=handler.php>
<input name=str>
<form action=handler.php method=get>
<input type=text name=str>
<input type=submit value=send>
<input value=foo>
<input type=text value=foo>
<input type=checkbox>
</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 id=test src=test.html></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 id=test src=test.html height=150 width=300 loading=eager fetchpriority=auto referrerpolicy=strict-origin-when-cross-origin></iframe>
@@ -1 +1 @@
<!doctype html><html lang=en><title>Document</title><img src=test.png alt=test>
<!doctype html><html lang=en><title>Document</title><img src=test.png alt=test decoding=auto loading=eager referrerpolicy=strict-origin-when-cross-origin>
@@ -1,4 +1,4 @@
<!doctype html><html lang=en><title>Document</title><input name=a>
<input name=b>
<input name=c>
<input name=d>
<!doctype html><html lang=en><title>Document</title><input name=a type=text>
<input name=b type=text>
<input name=c type=text>
<input name=d size=20>
@@ -1,5 +1,5 @@
<!doctype html><html lang=en><title>Document</title><form action=/test>
<input onkeydown=myFunction()>
<input type=text onkeydown=myFunction()>
</form>
<div type=text onmouseover=myFunction()>test</div>
<div type=text onmouseover=myFunction()>test</div>
Expand Down
@@ -1,3 +1,3 @@
<!doctype html><html lang=en><title>Document</title><meter id=fuel max=100 low=33 high=66 optimum=80 value=50>
<!doctype html><html lang=en><title>Document</title><meter id=fuel min=0 max=100 low=33 high=66 optimum=80 value=50>
at 50/100
</meter>
@@ -1,11 +1,11 @@
<!doctype html><html lang=en><title>Document</title><ol>
<!doctype html><html lang=en><title>Document</title><ol type=1>
<li>Mix flour, baking powder, sugar, and salt.</li>
<li>In another bowl, mix eggs, milk, and oil.</li>
<li>Stir both mixtures together.</li>
<li>Fill muffin tray 3/4 full.</li>
<li>Bake for 20 minutes.</li>
</ol>
<ol>
<ol type=1 start=1>
<li>United Kingdom
<li>Switzerland
<li>United States
Expand Down
@@ -1 +1 @@
<!doctype html><html lang=en><title>Document</title><progress></progress>
<!doctype html><html lang=en><title>Document</title><progress max=1></progress>
@@ -0,0 +1,3 @@
{
"removeRedundantAttributes": "all"
}