fix(html/minifier): Don't break unknown attributes (#5256)

This commit is contained in:
Alexander Akait 2022-07-20 10:23:58 +03:00 committed by GitHub
parent 6cca12d119
commit 4f55459b01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 576 additions and 278 deletions

View File

@ -6,7 +6,7 @@ authors = [
description = "HTML minifier" description = "HTML minifier"
documentation = "https://rustdoc.swc.rs/swc_html_minifier/" documentation = "https://rustdoc.swc.rs/swc_html_minifier/"
edition = "2021" edition = "2021"
include = ["Cargo.toml", "src/**/*.rs"] include = ["Cargo.toml", "src/**/*.rs", "data/**/*.json"]
license = "Apache-2.0" license = "Apache-2.0"
name = "swc_html_minifier" name = "swc_html_minifier"
repository = "https://github.com/swc-project/swc.git" repository = "https://github.com/swc-project/swc.git"

View File

@ -14,7 +14,7 @@ use swc_common::{
}; };
use swc_html_ast::*; use swc_html_ast::*;
use swc_html_parser::parser::ParserConfig; use swc_html_parser::parser::ParserConfig;
use swc_html_utils::{HTML_DEFAULT_ATTRIBUTES, SVG_ELEMENTS_AND_ATTRIBUTES}; use swc_html_utils::{HTML_ELEMENTS_AND_ATTRIBUTES, SVG_ELEMENTS_AND_ATTRIBUTES};
use swc_html_visit::{VisitMut, VisitMutWith}; use swc_html_visit::{VisitMut, VisitMutWith};
use crate::option::{ use crate::option::{
@ -23,53 +23,6 @@ use crate::option::{
}; };
pub mod option; pub mod option;
static HTML_BOOLEAN_ATTRIBUTES: &[&str] = &[
"allowfullscreen",
"async",
"autofocus",
"autoplay",
"checked",
"controls",
"default",
"defer",
"disabled",
"formnovalidate",
"hidden",
"inert",
"ismap",
"itemscope",
"loop",
"multiple",
"muted",
"nomodule",
"novalidate",
"open",
"playsinline",
"readonly",
"required",
"reversed",
"selected",
// Legacy
"declare",
"defaultchecked",
"defaultmuted",
"defaultselected",
"enabled",
"compact",
"indeterminate",
"sortable",
"nohref",
"noresize",
"noshade",
"truespeed",
"typemustmatch",
"nowrap",
"visible",
"pauseonexit",
"scoped",
"seamless",
];
// Global attributes // Global attributes
static EVENT_HANDLER_ATTRIBUTES: &[&str] = &[ static EVENT_HANDLER_ATTRIBUTES: &[&str] = &[
"onabort", "onabort",
@ -410,8 +363,24 @@ impl Minifier<'_> {
EVENT_HANDLER_ATTRIBUTES.contains(&name) EVENT_HANDLER_ATTRIBUTES.contains(&name)
} }
fn is_boolean_attribute(&self, name: &str) -> bool { fn is_boolean_attribute(&self, element: &Element, name: &str) -> bool {
HTML_BOOLEAN_ATTRIBUTES.contains(&name) if let Some(global_pseudo_element) = HTML_ELEMENTS_AND_ATTRIBUTES.get("*") {
if let Some(element) = global_pseudo_element.other.get(name) {
if element.boolean.is_some() && element.boolean.unwrap() {
return true;
}
}
}
if let Some(element) = HTML_ELEMENTS_AND_ATTRIBUTES.get(&*element.tag_name) {
if let Some(element) = element.other.get(name) {
if element.boolean.is_some() && element.boolean.unwrap() {
return true;
}
}
}
false
} }
fn is_trimable_separated_attribute(&self, element: &Element, attribute_name: &str) -> bool { fn is_trimable_separated_attribute(&self, element: &Element, attribute_name: &str) -> bool {
@ -600,7 +569,7 @@ impl Minifier<'_> {
} }
let default_attributes = if namespace == Namespace::HTML { let default_attributes = if namespace == Namespace::HTML {
&HTML_DEFAULT_ATTRIBUTES &HTML_ELEMENTS_AND_ATTRIBUTES
} else { } else {
&SVG_ELEMENTS_AND_ATTRIBUTES &SVG_ELEMENTS_AND_ATTRIBUTES
}; };
@ -2030,7 +1999,7 @@ impl VisitMut for Minifier<'_> {
if self.options.collapse_boolean_attributes if self.options.collapse_boolean_attributes
&& current_element.namespace == Namespace::HTML && current_element.namespace == Namespace::HTML
&& self.is_boolean_attribute(&n.name) && self.is_boolean_attribute(current_element, &n.name)
{ {
n.value = None; n.value = None;

View File

@ -22,7 +22,12 @@
<input autofocus="autofocus"> <input autofocus="autofocus">
<input required="required"> <input required="required">
<input multiple="multiple"> <input multiple="multiple">
<div Allowfullscreen=foo Async=foo Autofocus=foo Autoplay=foo Checked=foo Compact=foo Controls=foo Declare=foo Default=foo Defaultchecked=foo Defaultmuted=foo Defaultselected=foo Defer=foo Disabled=foo Enabled=foo Formnovalidate=foo Hidden=foo Indeterminate=foo Inert=foo Ismap=foo Itemscope=foo Loop=foo Multiple=foo Muted=foo Nohref=foo Noresize=foo Noshade=foo Novalidate=foo Nowrap=foo Open=foo Pauseonexit=foo Readonly=foo Required=foo Reversed=foo Scoped=foo Seamless=foo Selected=foo Sortable=foo Truespeed=foo Typemustmatch=foo Visible=foo></div> <iframe Allowfullscreen=foo></iframe>
<ul compact="compact"></ul>
<video src="" controls="controls" autoplay="autoplay"></video>
<script async="async" defer="defer"></script>
<input type="text" autofocus="autofocus" checked="checked" disabled="disabled">
<button formnovalidate="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> <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>
<div draggable="auto"></div> <div draggable="auto"></div>

View File

@ -15,7 +15,12 @@
<input autofocus> <input autofocus>
<input required> <input required>
<input multiple> <input multiple>
<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> <iframe allowfullscreen></iframe>
<ul compact></ul>
<video src="" controls autoplay></video>
<script async defer></script>
<input 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> <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>
<div draggable=auto></div> <div draggable=auto></div>

View File

@ -0,0 +1,533 @@
{
"*": {
"autofocus": {
"boolean": true
},
"hidden": {
"boolean": true
},
"inert": {
"boolean": true
},
"itemscope": {
"boolean": true
}
},
"a": {
"referrerpolicy": {
"initial": "strict-origin-when-cross-origin"
},
"target": {
"initial": "_self"
}
},
"abbr": {},
"applet": {},
"area": {
"nohref": {
"boolean": true
},
"referrerpolicy": {
"initial": "strict-origin-when-cross-origin"
},
"shape": {
"initial": "rect"
},
"target": {
"initial": "_self"
}
},
"article": {},
"aside": {},
"audio": {
"autoplay": {
"boolean": true
},
"controls": {
"boolean": true
},
"disableremoteplayback": {
"boolean": true
},
"loop": {
"boolean": true
},
"muted": {
"boolean": true
}
},
"b": {},
"base": {
"target": {
"initial": "_self"
}
},
"basefont": {},
"bdi": {},
"bdo": {},
"bgsound": {},
"big": {},
"blockquote": {},
"body": {},
"br": {
"clear": {
"initial": "none"
}
},
"button": {
"disabled": {
"boolean": true
},
"formnovalidate": {
"boolean": true
},
"type": {
"initial": "submit"
}
},
"canvas": {
"height": {
"initial": "150"
},
"width": {
"initial": "300"
}
},
"caption": {},
"center": {},
"cite": {},
"code": {},
"col": {
"span": {
"initial": "1"
}
},
"colgroup": {
"span": {
"initial": "1"
}
},
"data": {},
"datalist": {},
"dd": {},
"del": {},
"details": {
"open": {
"boolean": true
}
},
"dfn": {},
"dialog": {
"open": {
"boolean": true
}
},
"dir": {
"compact": {
"boolean": true
}
},
"div": {},
"dl": {
"compact": {
"boolean": true
}
},
"dt": {},
"em": {},
"embed": {},
"fieldset": {
"disabled": {
"boolean": true
}
},
"font": {},
"footer": {},
"form": {
"enctype": {
"initial": "application/x-www-form-urlencoded"
},
"method": {
"initial": "get"
},
"novalidate": {
"boolean": true
},
"target": {
"initial": "_self"
}
},
"frame": {
"noresize": {
"boolean": true
}
},
"frameset": {},
"h1": {},
"h2": {},
"h3": {},
"h4": {},
"h5": {},
"h6": {},
"head": {},
"header": {},
"hgroup": {},
"hr": {
"align": {
"initial": "center"
},
"noshade": {
"booean": true
},
"width": {
"initial": "100%"
},
"size": {
"initial": "2"
}
},
"html": {
"xmlns": {
"initial": "http://www.w3.org/1999/xhtml"
},
"xmlns:xlink": {
"initial": "http://www.w3.org/1999/xlink"
}
},
"i": {},
"iframe": {
"allowtransparency": {
"boolean": true
},
"allowfullscreen": {
"boolean": true
},
"fetchpriority": {
"initial": "auto"
},
"frameborder": {
"initial": "1"
},
"height": {
"initial": "150"
},
"hspace": {
"initial": "0"
},
"loading": {
"initial": "eager"
},
"referrerpolicy": {
"initial": "strict-origin-when-cross-origin"
},
"scrolling": {
"initial": "auto"
},
"seamless": {
"boolean": true
},
"vspace": {
"initial": "0"
},
"width": {
"initial": "300"
}
},
"image": {},
"img": {
"decoding": {
"initial": "auto"
},
"fetchpriority": {
"initial": "auto"
},
"ismap": {
"boolean": true
},
"loading": {
"initial": "eager"
},
"referrerpolicy": {
"initial": "strict-origin-when-cross-origin"
}
},
"input": {
"checked": {
"boolean": true
},
"disabled": {
"boolean": true
},
"ismap": {
"boolean": true
},
"multiple": {
"boolean": true
},
"formnovalidate": {
"boolean": true
},
"indeterminate": {
"boolean": true
},
"readonly": {
"boolean": true
},
"required": {
"boolean": true
},
"size": {
"initial": "20"
},
"type": {
"initial": "text"
}
},
"ins": {},
"isindex": {},
"kbd": {},
"keygen": {},
"label": {},
"legend": {},
"li": {},
"link": {
"disabled": {
"boolean": true
},
"fetchpriority": {
"initial": "auto"
},
"media": {
"initial": "all"
},
"referrerpolicy": {
"initial": "strict-origin-when-cross-origin"
},
"type": {
"initial": "text/css"
}
},
"listing": {},
"map": {},
"mark": {},
"marquee": {
"scrollamount": {
"initial": "6"
},
"scrolldelay": {
"initial": "85"
},
"truespeed": {
"boolean": true
}
},
"menu": {
"compact": {
"boolean": true
}
},
"meta": {},
"meter": {
"min": {
"initial": "0"
}
},
"nav": {},
"nobr": {},
"noembed": {},
"noframes": {},
"noscript": {},
"object": {
"declare": {
"boolean": true
},
"typemustmatch": {
"boolean": true
}
},
"ol": {
"compact": {
"boolean": true
},
"reversed": {
"boolean": true
},
"start": {
"initial": "1"
},
"type": {
"initial": "1"
}
},
"optgroup": {
"disabled": {
"boolean": true
}
},
"option": {
"disabled": {
"boolean": true
},
"selected": {
"boolean": true
}
},
"output": {},
"p": {},
"param": {},
"picture": {},
"plaintext": {},
"pre": {},
"progress": {
"max": {
"initial": "1"
}
},
"q": {},
"rb": {},
"rp": {},
"rt": {},
"rtc": {},
"ruby": {},
"s": {},
"samp": {},
"script": {
"async": {
"boolean": true
},
"charset": {
"initial": "utf-8"
},
"defer": {
"boolean": true
},
"fetchpriority": {
"initial": "auto"
},
"language": {
"initial": "javascript"
},
"nomodule": {
"boolean": true
},
"referrerpolicy": {
"initial": "strict-origin-when-cross-origin"
},
"type": {
"initial": "text/javascript"
}
},
"section": {},
"select": {
"disabled": {
"boolean": true
},
"multiple": {
"boolean": true
},
"required": {
"boolean": true
}
},
"slot": {},
"small": {},
"source": {},
"span": {},
"strike": {},
"strong": {},
"style": {
"scoped": {
"boolean": true
},
"type": {
"initial": "text/css"
}
},
"sub": {},
"sup": {},
"table": {},
"tbody": {},
"td": {
"colspan": {
"initial": "1"
},
"nowrap": {
"boolean": true
},
"rowspan": {
"initial": "1"
}
},
"template": {},
"textarea": {
"disabled": {
"boolean": true
},
"cols": {
"initial": "20"
},
"readonly": {
"boolean": true
},
"required": {
"boolean": true
},
"rows": {
"initial": "2"
},
"wrap": {
"initial": "sort"
}
},
"tfoot": {},
"th": {
"colspan": {
"initial": "1"
},
"rowspan": {
"initial": "1"
}
},
"thead": {},
"time": {},
"title": {},
"tr": {
"nowrap": {
"boolean": true
}
},
"track": {
"default": {
"boolean": true
},
"kind": {
"initial": "subtitles"
}
},
"tt": {},
"ul": {
"compact": {
"boolean": true
}
},
"var": {},
"video": {
"autoplay": {
"boolean": true
},
"controls": {
"boolean": true
},
"loop": {
"boolean": true
},
"muted": {
"boolean": true
},
"playsinline": {
"boolean": true
}
},
"wbr": {},
"xmp": {}
}

View File

@ -1,217 +0,0 @@
{
"a": {
"referrerpolicy": {
"initial": "strict-origin-when-cross-origin"
},
"target": {
"initial": "_self"
}
},
"area": {
"referrerpolicy": {
"initial": "strict-origin-when-cross-origin"
},
"shape": {
"initial": "rect"
},
"target": {
"initial": "_self"
}
},
"base": {
"target": {
"initial": "_self"
}
},
"br": {
"clear": {
"initial": "none"
}
},
"button": {
"type": {
"initial": "submit"
}
},
"canvas": {
"height": {
"initial": "150"
},
"width": {
"initial": "300"
}
},
"col": {
"span": {
"initial": "1"
}
},
"colgroup": {
"span": {
"initial": "1"
}
},
"form": {
"enctype": {
"initial": "application/x-www-form-urlencoded"
},
"method": {
"initial": "get"
},
"target": {
"initial": "_self"
}
},
"hr": {
"align": {
"initial": "center"
},
"width": {
"initial": "100%"
}
},
"html": {
"xmlns": {
"initial": "http://www.w3.org/1999/xhtml"
},
"xmlns:xlink": {
"initial": "http://www.w3.org/1999/xlink"
}
},
"iframe": {
"fetchpriority": {
"initial": "auto"
},
"frameborder": {
"initial": "1"
},
"height": {
"initial": "150"
},
"loading": {
"initial": "eager"
},
"referrerpolicy": {
"initial": "strict-origin-when-cross-origin"
},
"width": {
"initial": "300"
}
},
"img": {
"decoding": {
"initial": "auto"
},
"fetchpriority": {
"initial": "auto"
},
"loading": {
"initial": "eager"
},
"referrerpolicy": {
"initial": "strict-origin-when-cross-origin"
}
},
"input": {
"size": {
"initial": "20"
},
"type": {
"initial": "text"
}
},
"link": {
"fetchpriority": {
"initial": "auto"
},
"media": {
"initial": "all"
},
"referrerpolicy": {
"initial": "strict-origin-when-cross-origin"
},
"type": {
"initial": "text/css"
}
},
"marquee": {
"scrollamount": {
"initial": "6"
},
"scrolldelay": {
"initial": "85"
}
},
"meter": {
"min": {
"initial": "0"
}
},
"ol": {
"type": {
"initial": "1"
},
"start": {
"initial": "1"
}
},
"progress": {
"max": {
"initial": "1"
}
},
"script": {
"charset": {
"initial": "utf-8"
},
"fetchpriority": {
"initial": "auto"
},
"language": {
"initial": "javascript"
},
"referrerpolicy": {
"initial": "strict-origin-when-cross-origin"
},
"type": {
"initial": "text/javascript"
}
},
"style": {
"type": {
"initial": "text/css"
}
},
"td": {
"colspan": {
"initial": "1"
},
"rowspan": {
"initial": "1"
}
},
"textarea": {
"cols": {
"initial": "20"
},
"rows": {
"initial": "2"
},
"wrap": {
"initial": "sort"
}
},
"th": {
"colspan": {
"initial": "1"
},
"rowspan": {
"initial": "1"
}
},
"track": {
"kind": {
"initial": "subtitles"
}
}
}

View File

@ -8,7 +8,8 @@ pub struct Entity {
} }
pub static HTML_ENTITIES: Lazy<AHashMap<String, Entity>> = Lazy::new(|| { pub static HTML_ENTITIES: Lazy<AHashMap<String, Entity>> = Lazy::new(|| {
let entities: AHashMap<String, Entity> = serde_json::from_str(include_str!("./entities.json")) let entities: AHashMap<String, Entity> =
serde_json::from_str(include_str!("../data/entities.json"))
.expect("failed to parse entities.json for html entities"); .expect("failed to parse entities.json for html entities");
entities entities
@ -21,6 +22,8 @@ pub struct AttributeInfo {
pub initial: Option<String>, pub initial: Option<String>,
#[serde(default)] #[serde(default)]
pub inherited: Option<bool>, pub inherited: Option<bool>,
#[serde(default)]
pub boolean: Option<bool>,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -30,17 +33,17 @@ pub struct Element {
pub other: AHashMap<String, AttributeInfo>, pub other: AHashMap<String, AttributeInfo>,
} }
pub static HTML_DEFAULT_ATTRIBUTES: Lazy<AHashMap<String, Element>> = Lazy::new(|| { pub static HTML_ELEMENTS_AND_ATTRIBUTES: Lazy<AHashMap<String, Element>> = Lazy::new(|| {
let default_attributes: AHashMap<String, Element> = let default_attributes: AHashMap<String, Element> =
serde_json::from_str(include_str!("./html_default_attributes.json")) serde_json::from_str(include_str!("../data/html_elements_and_attributes.json"))
.expect("failed to parse html_default_attributes.json for default attributes"); .expect("failed to parse html_elements_and_attributes.json for default attributes");
default_attributes default_attributes
}); });
pub static SVG_ELEMENTS_AND_ATTRIBUTES: Lazy<AHashMap<String, Element>> = Lazy::new(|| { pub static SVG_ELEMENTS_AND_ATTRIBUTES: Lazy<AHashMap<String, Element>> = Lazy::new(|| {
let svg_elements_and_attributes: AHashMap<String, Element> = let svg_elements_and_attributes: AHashMap<String, Element> =
serde_json::from_str(include_str!("./svg_elements_and_attributes.json")) serde_json::from_str(include_str!("../data/svg_elements_and_attributes.json"))
.expect("failed to parse svg_elements_and_attributes.json for default attributes"); .expect("failed to parse svg_elements_and_attributes.json for default attributes");
svg_elements_and_attributes svg_elements_and_attributes