Remove attrs with default values; create minified comparison script; remove `=` from boolean attrs; fix closing tag writing before collapsed whitespace; rebuild hyperbuild only in bench build script instead of all dependencies; conservatively collapse whitespace for html-minifier to match hyperbuild behaviour; update bench results
This commit is contained in:
parent
cf56c0c2e6
commit
27af2368ff
|
@ -6,14 +6,13 @@ pushd "$(dirname "$0")"
|
||||||
|
|
||||||
nodejs_cargo_toml="../nodejs/native/Cargo.toml"
|
nodejs_cargo_toml="../nodejs/native/Cargo.toml"
|
||||||
|
|
||||||
rm -rf node_modules
|
|
||||||
if [ -f "$nodejs_cargo_toml.orig" ]; then
|
if [ -f "$nodejs_cargo_toml.orig" ]; then
|
||||||
echo 'Not altering Node.js Cargo.toml file'
|
echo 'Not altering Node.js Cargo.toml file'
|
||||||
else
|
else
|
||||||
cp "$nodejs_cargo_toml" "$nodejs_cargo_toml.orig"
|
cp "$nodejs_cargo_toml" "$nodejs_cargo_toml.orig"
|
||||||
fi
|
fi
|
||||||
sed -i 's%^hyperbuild = .*$%hyperbuild = { path = "../.." }%' "$nodejs_cargo_toml"
|
sed -i 's%^hyperbuild = .*$%hyperbuild = { path = "../.." }%' "$nodejs_cargo_toml"
|
||||||
HYPERBUILD_NODEJS_SKIP_BIN_DOWNLOAD=1 npm i
|
HYPERBUILD_NODEJS_SKIP_BIN_DOWNLOAD=1 npm rebuild hyperbuild
|
||||||
mv "$nodejs_cargo_toml.orig" "$nodejs_cargo_toml"
|
mv "$nodejs_cargo_toml.orig" "$nodejs_cargo_toml"
|
||||||
pushd hyperbuild-bench
|
pushd hyperbuild-bench
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
git --no-pager diff --no-index --word-diff=color --word-diff-regex=. "min/html-minifier/$1.html" "min/hyperbuild-nodejs/$1.html" | less
|
|
@ -5,12 +5,12 @@
|
||||||
"relative": 1
|
"relative": 1
|
||||||
},
|
},
|
||||||
"hyperbuild-nodejs": {
|
"hyperbuild-nodejs": {
|
||||||
"absolute": 353526,
|
"absolute": 353501,
|
||||||
"relative": 0.9585430133182942
|
"relative": 0.9584752288403974
|
||||||
},
|
},
|
||||||
"html-minifier": {
|
"html-minifier": {
|
||||||
"absolute": 353730,
|
"absolute": 355185,
|
||||||
"relative": 0.9590961346579324
|
"relative": 0.9630411912715283
|
||||||
},
|
},
|
||||||
"minimize": {
|
"minimize": {
|
||||||
"absolute": 359912,
|
"absolute": 359912,
|
||||||
|
@ -23,12 +23,12 @@
|
||||||
"relative": 1
|
"relative": 1
|
||||||
},
|
},
|
||||||
"hyperbuild-nodejs": {
|
"hyperbuild-nodejs": {
|
||||||
"absolute": 231730,
|
"absolute": 231706,
|
||||||
"relative": 0.941161658212065
|
"relative": 0.9410641832204925
|
||||||
},
|
},
|
||||||
"html-minifier": {
|
"html-minifier": {
|
||||||
"absolute": 234186,
|
"absolute": 234306,
|
||||||
"relative": 0.9511365990163149
|
"relative": 0.9516239739741772
|
||||||
},
|
},
|
||||||
"minimize": {
|
"minimize": {
|
||||||
"absolute": 238547,
|
"absolute": 238547,
|
||||||
|
@ -41,8 +41,8 @@
|
||||||
"relative": 1
|
"relative": 1
|
||||||
},
|
},
|
||||||
"hyperbuild-nodejs": {
|
"hyperbuild-nodejs": {
|
||||||
"absolute": 90485,
|
"absolute": 90484,
|
||||||
"relative": 0.983222679807452
|
"relative": 0.9832118136674309
|
||||||
},
|
},
|
||||||
"html-minifier": {
|
"html-minifier": {
|
||||||
"absolute": 90599,
|
"absolute": 90599,
|
||||||
|
@ -59,12 +59,12 @@
|
||||||
"relative": 1
|
"relative": 1
|
||||||
},
|
},
|
||||||
"hyperbuild-nodejs": {
|
"hyperbuild-nodejs": {
|
||||||
"absolute": 271220,
|
"absolute": 270833,
|
||||||
"relative": 0.8756319775813419
|
"relative": 0.8743825506389188
|
||||||
},
|
},
|
||||||
"html-minifier": {
|
"html-minifier": {
|
||||||
"absolute": 270355,
|
"absolute": 277911,
|
||||||
"relative": 0.8728393307978899
|
"relative": 0.8972338268623564
|
||||||
},
|
},
|
||||||
"minimize": {
|
"minimize": {
|
||||||
"absolute": 278990,
|
"absolute": 278990,
|
||||||
|
@ -77,12 +77,12 @@
|
||||||
"relative": 1
|
"relative": 1
|
||||||
},
|
},
|
||||||
"hyperbuild-nodejs": {
|
"hyperbuild-nodejs": {
|
||||||
"absolute": 79834,
|
"absolute": 79806,
|
||||||
"relative": 0.9433520820532212
|
"relative": 0.9430212222904949
|
||||||
},
|
},
|
||||||
"html-minifier": {
|
"html-minifier": {
|
||||||
"absolute": 79273,
|
"absolute": 81446,
|
||||||
"relative": 0.9367230703785981
|
"relative": 0.9624001512501772
|
||||||
},
|
},
|
||||||
"minimize": {
|
"minimize": {
|
||||||
"absolute": 81844,
|
"absolute": 81844,
|
||||||
|
@ -95,12 +95,12 @@
|
||||||
"relative": 1
|
"relative": 1
|
||||||
},
|
},
|
||||||
"hyperbuild-nodejs": {
|
"hyperbuild-nodejs": {
|
||||||
"absolute": 5744300,
|
"absolute": 5744290,
|
||||||
"relative": 0.9094278392290278
|
"relative": 0.9094262560459782
|
||||||
},
|
},
|
||||||
"html-minifier": {
|
"html-minifier": {
|
||||||
"absolute": 5663106,
|
"absolute": 5785725,
|
||||||
"relative": 0.896573342775437
|
"relative": 0.9159861750123369
|
||||||
},
|
},
|
||||||
"minimize": {
|
"minimize": {
|
||||||
"absolute": 5799350,
|
"absolute": 5799350,
|
||||||
|
@ -113,12 +113,12 @@
|
||||||
"relative": 1
|
"relative": 1
|
||||||
},
|
},
|
||||||
"hyperbuild-nodejs": {
|
"hyperbuild-nodejs": {
|
||||||
"absolute": 196589,
|
"absolute": 196577,
|
||||||
"relative": 0.9964468548836738
|
"relative": 0.9963860307162046
|
||||||
},
|
},
|
||||||
"html-minifier": {
|
"html-minifier": {
|
||||||
"absolute": 196568,
|
"absolute": 196600,
|
||||||
"relative": 0.9963404125906027
|
"relative": 0.9965026103705206
|
||||||
},
|
},
|
||||||
"minimize": {
|
"minimize": {
|
||||||
"absolute": 196776,
|
"absolute": 196776,
|
||||||
|
@ -131,12 +131,12 @@
|
||||||
"relative": 1
|
"relative": 1
|
||||||
},
|
},
|
||||||
"hyperbuild-nodejs": {
|
"hyperbuild-nodejs": {
|
||||||
"absolute": 28189,
|
"absolute": 28154,
|
||||||
"relative": 0.8285764674759707
|
"relative": 0.8275476911319479
|
||||||
},
|
},
|
||||||
"html-minifier": {
|
"html-minifier": {
|
||||||
"absolute": 28109,
|
"absolute": 29086,
|
||||||
"relative": 0.8262249786896328
|
"relative": 0.8549425354927839
|
||||||
},
|
},
|
||||||
"minimize": {
|
"minimize": {
|
||||||
"absolute": 30318,
|
"absolute": 30318,
|
||||||
|
@ -149,12 +149,12 @@
|
||||||
"relative": 1
|
"relative": 1
|
||||||
},
|
},
|
||||||
"hyperbuild-nodejs": {
|
"hyperbuild-nodejs": {
|
||||||
"absolute": 1263698,
|
"absolute": 1263682,
|
||||||
"relative": 0.9967377432692293
|
"relative": 0.9967251233205608
|
||||||
},
|
},
|
||||||
"html-minifier": {
|
"html-minifier": {
|
||||||
"absolute": 1263082,
|
"absolute": 1263150,
|
||||||
"relative": 0.9962518752454974
|
"relative": 0.9963055100273379
|
||||||
},
|
},
|
||||||
"minimize": {
|
"minimize": {
|
||||||
"absolute": 1264354,
|
"absolute": 1264354,
|
||||||
|
@ -167,12 +167,12 @@
|
||||||
"relative": 1
|
"relative": 1
|
||||||
},
|
},
|
||||||
"hyperbuild-nodejs": {
|
"hyperbuild-nodejs": {
|
||||||
"absolute": 648036,
|
"absolute": 648004,
|
||||||
"relative": 0.9945654926432332
|
"relative": 0.9945163810263407
|
||||||
},
|
},
|
||||||
"html-minifier": {
|
"html-minifier": {
|
||||||
"absolute": 647776,
|
"absolute": 647822,
|
||||||
"relative": 0.9941664607559813
|
"relative": 0.9942370587052644
|
||||||
},
|
},
|
||||||
"minimize": {
|
"minimize": {
|
||||||
"absolute": 648422,
|
"absolute": 648422,
|
||||||
|
@ -185,12 +185,12 @@
|
||||||
"relative": 1
|
"relative": 1
|
||||||
},
|
},
|
||||||
"hyperbuild-nodejs": {
|
"hyperbuild-nodejs": {
|
||||||
"absolute": 86770,
|
"absolute": 86693,
|
||||||
"relative": 0.7731513245239644
|
"relative": 0.7724652273476552
|
||||||
},
|
},
|
||||||
"html-minifier": {
|
"html-minifier": {
|
||||||
"absolute": 86383,
|
"absolute": 88366,
|
||||||
"relative": 0.7697030179365405
|
"relative": 0.7873722478147359
|
||||||
},
|
},
|
||||||
"minimize": {
|
"minimize": {
|
||||||
"absolute": 89682,
|
"absolute": 89682,
|
||||||
|
@ -203,12 +203,12 @@
|
||||||
"relative": 1
|
"relative": 1
|
||||||
},
|
},
|
||||||
"hyperbuild-nodejs": {
|
"hyperbuild-nodejs": {
|
||||||
"absolute": 272544,
|
"absolute": 272480,
|
||||||
"relative": 0.8647962583371939
|
"relative": 0.8645931830152878
|
||||||
},
|
},
|
||||||
"html-minifier": {
|
"html-minifier": {
|
||||||
"absolute": 265242,
|
"absolute": 266639,
|
||||||
"relative": 0.841626633328468
|
"relative": 0.8460593868394499
|
||||||
},
|
},
|
||||||
"minimize": {
|
"minimize": {
|
||||||
"absolute": 307860,
|
"absolute": 307860,
|
||||||
|
@ -221,12 +221,12 @@
|
||||||
"relative": 1
|
"relative": 1
|
||||||
},
|
},
|
||||||
"hyperbuild-nodejs": {
|
"hyperbuild-nodejs": {
|
||||||
"absolute": 1319433,
|
"absolute": 1319432,
|
||||||
"relative": 0.9350021790660842
|
"relative": 0.9350014704267072
|
||||||
},
|
},
|
||||||
"html-minifier": {
|
"html-minifier": {
|
||||||
"absolute": 1308864,
|
"absolute": 1327244,
|
||||||
"relative": 0.927512569490949
|
"relative": 0.940537361239552
|
||||||
},
|
},
|
||||||
"minimize": {
|
"minimize": {
|
||||||
"absolute": 1369798,
|
"absolute": 1369798,
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 28 KiB |
|
@ -8,6 +8,10 @@ module.exports = {
|
||||||
collapseBooleanAttributes: true,
|
collapseBooleanAttributes: true,
|
||||||
collapseInlineTagWhitespace: true,
|
collapseInlineTagWhitespace: true,
|
||||||
collapseWhitespace: true,
|
collapseWhitespace: true,
|
||||||
|
// hyperbuild can do context-aware whitespace removal, which is safe when configured correctly to match how whitespace is used in the document.
|
||||||
|
// html-minifier cannot, so whitespace must be collapsed conservatively.
|
||||||
|
// Alternatively, hyperbuild can also be made to remove whitespace regardless of context.
|
||||||
|
conservativeCollapse: true,
|
||||||
customEventAttributes: [],
|
customEventAttributes: [],
|
||||||
decodeEntities: true,
|
decodeEntities: true,
|
||||||
ignoreCustomComments: [],
|
ignoreCustomComments: [],
|
||||||
|
|
|
@ -7,7 +7,7 @@ const tests = require('./tests');
|
||||||
for (const t of tests) {
|
for (const t of tests) {
|
||||||
for (const p of Object.keys(programs)) {
|
for (const p of Object.keys(programs)) {
|
||||||
try {
|
try {
|
||||||
const minPath = path.join(__dirname, 'min', p, t.name);
|
const minPath = path.join(__dirname, 'min', p, `${t.name}.html`);
|
||||||
mkdirp.sync(path.dirname(minPath));
|
mkdirp.sync(path.dirname(minPath));
|
||||||
fs.writeFileSync(minPath, programs[p](t.contentAsString, t.contentAsBuffer));
|
fs.writeFileSync(minPath, programs[p](t.contentAsString, t.contentAsBuffer));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -1,67 +1,67 @@
|
||||||
{
|
{
|
||||||
"Amazon": {
|
"Amazon": {
|
||||||
"hyperbuild-nodejs": 508.6052506017873,
|
"hyperbuild-nodejs": 499.9428905642954,
|
||||||
"html-minifier": 42.25586310792637,
|
"html-minifier": 36.3036627447208,
|
||||||
"minimize": 112.84668317196737
|
"minimize": 113.33527430279831
|
||||||
},
|
},
|
||||||
"BBC": {
|
"BBC": {
|
||||||
"hyperbuild-nodejs": 532.8102905973569,
|
"hyperbuild-nodejs": 534.5358584753244,
|
||||||
"html-minifier": 54.543166381705994,
|
"html-minifier": 49.23362197831472,
|
||||||
"minimize": 158.04711027873367
|
"minimize": 161.96803168572117
|
||||||
},
|
},
|
||||||
"Bing": {
|
"Bing": {
|
||||||
"hyperbuild-nodejs": 2163.2302078550147,
|
"hyperbuild-nodejs": 2137.27013270331,
|
||||||
"html-minifier": 226.06394079630297,
|
"html-minifier": 223.08512226985184,
|
||||||
"minimize": 545.0653574947338
|
"minimize": 550.9181761277221
|
||||||
},
|
},
|
||||||
"Bootstrap": {
|
"Bootstrap": {
|
||||||
"hyperbuild-nodejs": 276.9069916779089,
|
"hyperbuild-nodejs": 277.1391080206004,
|
||||||
"html-minifier": 9.088104379498866,
|
"html-minifier": 8.043255283692064,
|
||||||
"minimize": 23.551904759608103
|
"minimize": 22.245439492019898
|
||||||
},
|
},
|
||||||
"Coding Horror": {
|
"Coding Horror": {
|
||||||
"hyperbuild-nodejs": 1130.8127333329164,
|
"hyperbuild-nodejs": 1096.4910673601032,
|
||||||
"html-minifier": 57.409616058969526,
|
"html-minifier": 49.83595257976626,
|
||||||
"minimize": 187.28893041569714
|
"minimize": 188.32749988717788
|
||||||
},
|
},
|
||||||
"ECMA-262": {
|
"ECMA-262": {
|
||||||
"hyperbuild-nodejs": 16.78425400473028,
|
"hyperbuild-nodejs": 16.200240897950334,
|
||||||
"html-minifier": 0.5081461293026476,
|
"html-minifier": 0.45522858062374655,
|
||||||
"minimize": 1.3377251957362182
|
"minimize": 1.3356053866389666
|
||||||
},
|
},
|
||||||
"Google": {
|
"Google": {
|
||||||
"hyperbuild-nodejs": 1856.1401576820815,
|
"hyperbuild-nodejs": 1832.4626475818236,
|
||||||
"html-minifier": 326.33685434942925,
|
"html-minifier": 242.1462398878334,
|
||||||
"minimize": 555.6574376056606
|
"minimize": 564.1884364813526
|
||||||
},
|
},
|
||||||
"Hacker News": {
|
"Hacker News": {
|
||||||
"hyperbuild-nodejs": 2243.718482073889,
|
"hyperbuild-nodejs": 2127.8084431041266,
|
||||||
"html-minifier": 86.20536879655822,
|
"html-minifier": 74.78979361035866,
|
||||||
"minimize": 272.83901988612814
|
"minimize": 272.3995630011103
|
||||||
},
|
},
|
||||||
"NY Times": {
|
"NY Times": {
|
||||||
"hyperbuild-nodejs": 281.05098122387943,
|
"hyperbuild-nodejs": 265.55362689112695,
|
||||||
"html-minifier": 35.57258739419913,
|
"html-minifier": 37.146711151201565,
|
||||||
"minimize": 85.73538788107287
|
"minimize": 87.7133467873164
|
||||||
},
|
},
|
||||||
"Reddit": {
|
"Reddit": {
|
||||||
"hyperbuild-nodejs": 410.12831233429847,
|
"hyperbuild-nodejs": 391.75000439723124,
|
||||||
"html-minifier": 45.16614107393938,
|
"html-minifier": 45.067854272152125,
|
||||||
"minimize": 122.77229783626386
|
"minimize": 125.87983932864549
|
||||||
},
|
},
|
||||||
"Stack Overflow": {
|
"Stack Overflow": {
|
||||||
"hyperbuild-nodejs": 844.9492250936706,
|
"hyperbuild-nodejs": 818.3755008258345,
|
||||||
"html-minifier": 47.752958982642866,
|
"html-minifier": 41.43093414076361,
|
||||||
"minimize": 157.06321076776504
|
"minimize": 159.71387801780298
|
||||||
},
|
},
|
||||||
"Twitter": {
|
"Twitter": {
|
||||||
"hyperbuild-nodejs": 279.4114354197891,
|
"hyperbuild-nodejs": 274.57816497268476,
|
||||||
"html-minifier": 41.71822833204345,
|
"html-minifier": 36.94949014023178,
|
||||||
"minimize": 164.8527211050192
|
"minimize": 168.81796573617953
|
||||||
},
|
},
|
||||||
"Wikipedia": {
|
"Wikipedia": {
|
||||||
"hyperbuild-nodejs": 57.721598283350104,
|
"hyperbuild-nodejs": 54.852210553433345,
|
||||||
"html-minifier": 3.1325913382989596,
|
"html-minifier": 2.821530343574604,
|
||||||
"minimize": 8.741663457588
|
"minimize": 8.66394750522524
|
||||||
}
|
}
|
||||||
}
|
}
|
BIN
bench/speed.png
BIN
bench/speed.png
Binary file not shown.
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
76
build.rs
76
build.rs
|
@ -88,32 +88,67 @@ fn generate_fastrie_code(var_name: &str, value_type: &str, built: &FastrieBuild<
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_attr_map(name: &str) {
|
#[derive(Serialize, Deserialize)]
|
||||||
let name_words = name_words(name);
|
struct TagAttr {
|
||||||
let snake_case = snake_case(&name_words);
|
boolean: bool,
|
||||||
let file_name = name_words.join("_");
|
redundant_if_empty: bool,
|
||||||
let attrs: HashMap<String, Vec<String>> = read_json(file_name.as_str());
|
collapse_and_trim: bool,
|
||||||
|
default_value: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TagAttr {
|
||||||
|
fn code(&self) -> String {
|
||||||
|
format!(r"
|
||||||
|
AttributeMinification {{
|
||||||
|
boolean: {boolean},
|
||||||
|
redundant_if_empty: {redundant_if_empty},
|
||||||
|
collapse_and_trim: {collapse_and_trim},
|
||||||
|
default_value: {default_value},
|
||||||
|
}}
|
||||||
|
",
|
||||||
|
boolean = self.boolean,
|
||||||
|
redundant_if_empty = self.redundant_if_empty,
|
||||||
|
collapse_and_trim = self.collapse_and_trim,
|
||||||
|
default_value = match &self.default_value {
|
||||||
|
Some(val) => format!("Some({})", create_byte_string_literal(val.as_bytes())),
|
||||||
|
None => "None".to_string(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_attr_map() {
|
||||||
|
let attrs: HashMap<String, HashMap<String, TagAttr>> = read_json("attrs");
|
||||||
let mut code = String::new();
|
let mut code = String::new();
|
||||||
for (name, elems) in attrs.iter() {
|
for (attr_name, tags_map) in attrs.iter() {
|
||||||
if !elems.contains(&"".to_string()) {
|
if let Some(global_attr) = tags_map.get("") {
|
||||||
code.push_str(format!(
|
code.push_str(format!(
|
||||||
"static {}_{}_ATTR: &phf::Set<&'static [u8]> = &phf::phf_set!({});\n\n",
|
"static {}_ATTR: &AttrMapEntry = &AttrMapEntry::AllHtmlElements({});\n\n",
|
||||||
name.to_uppercase(),
|
attr_name.to_uppercase(),
|
||||||
snake_case,
|
global_attr.code(),
|
||||||
elems.iter().map(|e| format!("b\"{}\"", e)).collect::<Vec<String>>().join(", "),
|
).as_str());
|
||||||
|
} else {
|
||||||
|
code.push_str(format!(
|
||||||
|
"static {}_ATTR: &AttrMapEntry = &AttrMapEntry::DistinctHtmlElements(phf::phf_map! {{\n{}\n}});\n\n",
|
||||||
|
attr_name.to_uppercase(),
|
||||||
|
tags_map
|
||||||
|
.iter()
|
||||||
|
.map(|(tag_name, tag_attr)| format!(
|
||||||
|
"b\"{}\" => {}",
|
||||||
|
tag_name,
|
||||||
|
tag_attr.code(),
|
||||||
|
))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(",\n"),
|
||||||
).as_str());
|
).as_str());
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
code.push_str(format!("pub static {}: crate::pattern::AttrMap = crate::pattern::AttrMap::new(phf::phf_map!{{\n", snake_case).as_str());
|
code.push_str("pub static ATTRS: AttrMap = AttrMap::new(phf::phf_map! {\n");
|
||||||
for (name, elems) in attrs.iter() {
|
for attr_name in attrs.keys() {
|
||||||
if elems.contains(&"".to_string()) {
|
code.push_str(format!("\tb\"{}\" => {}_ATTR,\n", attr_name, attr_name.to_uppercase()).as_str());
|
||||||
code.push_str(format!("\tb\"{}\" => crate::pattern::AttrMapEntry::AllHtmlElements,\n", name).as_str());
|
|
||||||
} else {
|
|
||||||
code.push_str(format!("\tb\"{}\" => crate::pattern::AttrMapEntry::SomeHtmlElements({}_{}_ATTR),\n", name, name.to_uppercase(), snake_case).as_str());
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
code.push_str("});\n\n");
|
code.push_str("});\n\n");
|
||||||
write_rs(file_name.as_str(), code);
|
write_rs("attrs", code);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -182,8 +217,7 @@ fn generate_tries() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
generate_attr_map("boolean attrs");
|
generate_attr_map();
|
||||||
generate_attr_map("redundant if empty attrs");
|
|
||||||
generate_entities();
|
generate_entities();
|
||||||
generate_patterns();
|
generate_patterns();
|
||||||
generate_tries();
|
generate_tries();
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,113 +0,0 @@
|
||||||
{
|
|
||||||
"allowfullscreen": [
|
|
||||||
"iframe"
|
|
||||||
],
|
|
||||||
"allowtransparency": [
|
|
||||||
"iframe"
|
|
||||||
],
|
|
||||||
"async": [
|
|
||||||
"script"
|
|
||||||
],
|
|
||||||
"autofocus": [
|
|
||||||
"button",
|
|
||||||
"input",
|
|
||||||
"keygen",
|
|
||||||
"select",
|
|
||||||
"textarea"
|
|
||||||
],
|
|
||||||
"autoplay": [
|
|
||||||
"media"
|
|
||||||
],
|
|
||||||
"capture": [
|
|
||||||
"input"
|
|
||||||
],
|
|
||||||
"checked": [
|
|
||||||
"input"
|
|
||||||
],
|
|
||||||
"controls": [
|
|
||||||
"media"
|
|
||||||
],
|
|
||||||
"default": [
|
|
||||||
"track"
|
|
||||||
],
|
|
||||||
"defaultchecked": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"defer": [
|
|
||||||
"script"
|
|
||||||
],
|
|
||||||
"disabled": [
|
|
||||||
"button",
|
|
||||||
"fieldset",
|
|
||||||
"input",
|
|
||||||
"keygen",
|
|
||||||
"optgroup",
|
|
||||||
"option",
|
|
||||||
"select",
|
|
||||||
"textarea"
|
|
||||||
],
|
|
||||||
"disablepictureinpicture": [
|
|
||||||
"video"
|
|
||||||
],
|
|
||||||
"formnovalidate": [
|
|
||||||
"button",
|
|
||||||
"input"
|
|
||||||
],
|
|
||||||
"hidden": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"itemscope": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"loop": [
|
|
||||||
"media"
|
|
||||||
],
|
|
||||||
"multiple": [
|
|
||||||
"input",
|
|
||||||
"select"
|
|
||||||
],
|
|
||||||
"muted": [
|
|
||||||
"media"
|
|
||||||
],
|
|
||||||
"nomodule": [
|
|
||||||
"script"
|
|
||||||
],
|
|
||||||
"novalidate": [
|
|
||||||
"form"
|
|
||||||
],
|
|
||||||
"open": [
|
|
||||||
"details",
|
|
||||||
"dialog"
|
|
||||||
],
|
|
||||||
"playsinline": [
|
|
||||||
"media",
|
|
||||||
"video"
|
|
||||||
],
|
|
||||||
"readonly": [
|
|
||||||
"input",
|
|
||||||
"textarea"
|
|
||||||
],
|
|
||||||
"required": [
|
|
||||||
"input",
|
|
||||||
"select",
|
|
||||||
"textarea"
|
|
||||||
],
|
|
||||||
"reversed": [
|
|
||||||
"ol"
|
|
||||||
],
|
|
||||||
"scoped": [
|
|
||||||
"style"
|
|
||||||
],
|
|
||||||
"seamless": [
|
|
||||||
"iframe"
|
|
||||||
],
|
|
||||||
"selected": [
|
|
||||||
"option"
|
|
||||||
],
|
|
||||||
"suppresscontenteditablewarning": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"suppresshydrationwarning": [
|
|
||||||
""
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -0,0 +1,205 @@
|
||||||
|
const request = require('request-promise-native');
|
||||||
|
const {promises: fs} = require('fs');
|
||||||
|
const ts = require('typescript');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const fromCamelCase = camelCase => camelCase.split(/(?=^|[A-Z])/).map(w => w.toLowerCase());
|
||||||
|
|
||||||
|
const ATTRS_PATH = path.join(__dirname, '..', 'attrs.json');
|
||||||
|
|
||||||
|
const REACT_TYPINGS_URL = 'https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/master/types/react/index.d.ts';
|
||||||
|
const REACT_TYPINGS_FILE = path.join(__dirname, 'react.d.ts');
|
||||||
|
const fetchReactTypingsSource = async () => {
|
||||||
|
try {
|
||||||
|
return await fs.readFile(REACT_TYPINGS_FILE, 'utf8');
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ENOENT') {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
const source = await request(REACT_TYPINGS_URL);
|
||||||
|
await fs.writeFile(REACT_TYPINGS_FILE, source);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tagNameNormalised = {
|
||||||
|
'anchor': 'a',
|
||||||
|
};
|
||||||
|
|
||||||
|
const attrNameNormalised = {
|
||||||
|
'classname': 'class',
|
||||||
|
};
|
||||||
|
|
||||||
|
const reactSpecificAttributes = [
|
||||||
|
'defaultChecked', 'defaultValue', 'suppressContentEditableWarning', 'suppressHydrationWarning',
|
||||||
|
];
|
||||||
|
|
||||||
|
// TODO Consider and check behaviour when value matches case insensitively, after trimming whitespace, numerically (for number values), etc.
|
||||||
|
// TODO This is currently manually sourced and written. Try to get machine-readable spec and automate.
|
||||||
|
const defaultAttributeValues = {
|
||||||
|
'align': [{
|
||||||
|
tags: ['img'],
|
||||||
|
defaultValue: 'bottom',
|
||||||
|
}],
|
||||||
|
'decoding': [{
|
||||||
|
tags: ['img'],
|
||||||
|
defaultValue: 'auto',
|
||||||
|
}],
|
||||||
|
'enctype': [{
|
||||||
|
tags: ['form'],
|
||||||
|
defaultValue: 'application/x-www-form-urlencoded',
|
||||||
|
}],
|
||||||
|
'frameborder': [{
|
||||||
|
tags: ['iframe'],
|
||||||
|
defaultValue: '1',
|
||||||
|
isPositiveInteger: true,
|
||||||
|
}],
|
||||||
|
'formenctype': [{
|
||||||
|
tags: ['button', 'input'],
|
||||||
|
defaultValue: 'application/x-www-form-urlencoded',
|
||||||
|
}],
|
||||||
|
'height': [{
|
||||||
|
tags: ['iframe'],
|
||||||
|
defaultValue: '150',
|
||||||
|
isPositiveInteger: true,
|
||||||
|
}],
|
||||||
|
'importance': [{
|
||||||
|
tags: ['iframe'],
|
||||||
|
defaultValue: 'auto',
|
||||||
|
}],
|
||||||
|
'loading': [{
|
||||||
|
tags: ['iframe', 'img'],
|
||||||
|
defaultValue: 'eager',
|
||||||
|
}],
|
||||||
|
'method': [{
|
||||||
|
tags: ['form'],
|
||||||
|
defaultValue: 'get',
|
||||||
|
}],
|
||||||
|
'referrerpolicy': [{
|
||||||
|
tags: ['iframe', 'img'],
|
||||||
|
defaultValue: 'no-referrer-when-downgrade',
|
||||||
|
}],
|
||||||
|
'rules': [{
|
||||||
|
tags: ['table'],
|
||||||
|
defaultValue: 'none',
|
||||||
|
}],
|
||||||
|
'span': [{
|
||||||
|
tags: ['col', 'colgroup'],
|
||||||
|
defaultValue: '1',
|
||||||
|
isPositiveInteger: true,
|
||||||
|
}],
|
||||||
|
'target': [{
|
||||||
|
tags: ['a', 'form'],
|
||||||
|
defaultValue: '_self',
|
||||||
|
}],
|
||||||
|
'type': [{
|
||||||
|
tags: ['button'],
|
||||||
|
defaultValue: 'submit',
|
||||||
|
}, {
|
||||||
|
tags: ['input'],
|
||||||
|
defaultValue: 'text',
|
||||||
|
}, {
|
||||||
|
tags: ['link'],
|
||||||
|
defaultValue: 'text/css',
|
||||||
|
}],
|
||||||
|
'width': [{
|
||||||
|
tags: ['iframe'],
|
||||||
|
defaultValue: '300',
|
||||||
|
isPositiveInteger: true,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
const collapsibleAndTrimmable = {
|
||||||
|
'class': [''],
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO Is escapedText the API for getting name?
|
||||||
|
const getNameOfNode = n => n.name.escapedText;
|
||||||
|
const normaliseName = (name, norms) => [name.toLowerCase()].map(n => norms[n] || n)[0];
|
||||||
|
|
||||||
|
const processReactTypeDeclarations = async (source) => {
|
||||||
|
const nodes = [source];
|
||||||
|
// Use index-based loop to keep iterating as nodes array grows.
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
// forEachChild doesn't work if return value is number (e.g. return value of Array.prototype.push).
|
||||||
|
nodes[i].forEachChild(c => void nodes.push(c));
|
||||||
|
}
|
||||||
|
const attributeNodes = nodes
|
||||||
|
.filter(n => n.kind === ts.SyntaxKind.InterfaceDeclaration)
|
||||||
|
.map(n => [/^([A-Za-z]*)HTMLAttributes/.exec(getNameOfNode(n)), n])
|
||||||
|
.filter(([matches]) => matches)
|
||||||
|
.map(([matches, node]) => [normaliseName(matches[1], tagNameNormalised), node])
|
||||||
|
.filter(([tagName]) => !['all', 'webview'].includes(tagName))
|
||||||
|
.sort((a, b) => a[0].localeCompare(b[0]));
|
||||||
|
|
||||||
|
// Process global attributes first as they also appear on some specific tags but we don't want to keep the specific ones if they're global.
|
||||||
|
if (attributeNodes[0][0] !== '') {
|
||||||
|
throw new Error(`Global attributes is not first to be processed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributes = new Map();
|
||||||
|
|
||||||
|
for (const [tagName, node] of attributeNodes) {
|
||||||
|
for (const n of node.members.filter(n => n.kind === ts.SyntaxKind.PropertySignature)) {
|
||||||
|
const attrName = normaliseName(getNameOfNode(n), attrNameNormalised);
|
||||||
|
if (reactSpecificAttributes.includes(attrName)) continue;
|
||||||
|
|
||||||
|
const types = n.type.kind === ts.SyntaxKind.UnionType
|
||||||
|
? n.type.types.map(t => t.kind)
|
||||||
|
: [n.type.kind];
|
||||||
|
|
||||||
|
const boolean = types.includes(ts.SyntaxKind.BooleanKeyword);
|
||||||
|
// If types includes boolean and string, make it a boolean attr to prevent it from being removed if empty value.
|
||||||
|
const redundantIfEmpty = !boolean &&
|
||||||
|
(types.includes(ts.SyntaxKind.StringKeyword) || types.includes(ts.SyntaxKind.NumberKeyword));
|
||||||
|
const defaultValue = (defaultAttributeValues[attrName] || [])
|
||||||
|
.filter(a => a.tags.includes(tagName))
|
||||||
|
.map(a => a.defaultValue);
|
||||||
|
const collapseAndTrim = (collapsibleAndTrimmable[attrName] || []).includes(tagName);
|
||||||
|
if (defaultValue.length > 1) {
|
||||||
|
throw new Error(`Tag-attribute combination has multiple default values: ${defaultValue}`);
|
||||||
|
}
|
||||||
|
const attr = {
|
||||||
|
boolean,
|
||||||
|
redundant_if_empty: redundantIfEmpty,
|
||||||
|
collapse_and_trim: collapseAndTrim,
|
||||||
|
default_value: defaultValue[0],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!attributes.has(attrName)) attributes.set(attrName, new Map());
|
||||||
|
const tagsForAttribute = attributes.get(attrName);
|
||||||
|
if (tagsForAttribute.has(tagName)) throw new Error(`Duplicate tag-attribute combination: <${tagName} ${attrName}>`);
|
||||||
|
|
||||||
|
const globalAttr = tagsForAttribute.get('');
|
||||||
|
if (globalAttr) {
|
||||||
|
if (globalAttr.boolean !== attr.boolean
|
||||||
|
|| globalAttr.redundant_if_empty !== attr.redundant_if_empty
|
||||||
|
|| globalAttr.collapse_and_trim !== attr.collapse_and_trim
|
||||||
|
|| globalAttr.default_value !== attr.default_value) {
|
||||||
|
throw new Error(`Global and tag-specific attributes conflict: ${JSON.stringify(globalAttr, null, 2)} ${JSON.stringify(attr, null, 2)}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tagsForAttribute.set(tagName, attr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort output JSON object by property so diffs are clearer.
|
||||||
|
await fs.writeFile(ATTRS_PATH, JSON.stringify(
|
||||||
|
Object.fromEntries(
|
||||||
|
[...attributes.entries()]
|
||||||
|
.map(([attrName, tagsMap]) => [attrName, Object.fromEntries(
|
||||||
|
[...tagsMap.entries()]
|
||||||
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
|
)])
|
||||||
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const source = ts.createSourceFile(`react.d.ts`, await fetchReactTypingsSource(), ts.ScriptTarget.ES2019);
|
||||||
|
await processReactTypeDeclarations(source);
|
||||||
|
})();
|
|
@ -1,93 +0,0 @@
|
||||||
const request = require('request-promise-native');
|
|
||||||
const {promises: fs} = require('fs');
|
|
||||||
const ts = require('typescript');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const fromCamelCase = camelCase => camelCase.split(/(?=^|[A-Z])/).map(w => w.toLowerCase());
|
|
||||||
|
|
||||||
const BOOLEAN_ATTRS_PATH = path.join(__dirname, '..', 'boolean_attrs.json');
|
|
||||||
const REDUNDANT_IF_EMPTY_ATTRS_PATH = path.join(__dirname, '..', 'redundant_if_empty_attrs.json');
|
|
||||||
|
|
||||||
const REACT_TYPINGS_URL = 'https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/master/types/react/index.d.ts';
|
|
||||||
const REACT_TYPINGS_FILE = path.join(__dirname, 'react.d.ts');
|
|
||||||
const fetchReactTypingsSource = async () => {
|
|
||||||
try {
|
|
||||||
return await fs.readFile(REACT_TYPINGS_FILE, 'utf8');
|
|
||||||
} catch (err) {
|
|
||||||
if (err.code !== 'ENOENT') {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
const source = await request(REACT_TYPINGS_URL);
|
|
||||||
await fs.writeFile(REACT_TYPINGS_FILE, source);
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const attrInterfaceToTagName = {
|
|
||||||
'anchor': 'a',
|
|
||||||
};
|
|
||||||
|
|
||||||
const attrNameNormalised = {
|
|
||||||
'classname': 'class',
|
|
||||||
};
|
|
||||||
|
|
||||||
const reactSpecificAttributes = [
|
|
||||||
'defaultChecked', 'defaultValue', 'suppressContentEditableWarning', 'suppressHydrationWarning',
|
|
||||||
];
|
|
||||||
|
|
||||||
const processReactTypeDeclarations = async (source) => {
|
|
||||||
const booleanAttributes = new Map();
|
|
||||||
const redundantIfEmptyAttributes = new Map();
|
|
||||||
|
|
||||||
const unvisited = [source];
|
|
||||||
while (unvisited.length) {
|
|
||||||
const node = unvisited.shift();
|
|
||||||
if (node.kind === ts.SyntaxKind.InterfaceDeclaration) {
|
|
||||||
const name = node.name.escapedText;
|
|
||||||
let matches;
|
|
||||||
if ((matches = /^([A-Za-z]*)HTMLAttributes/.exec(name))) {
|
|
||||||
const tagName = [matches[1].toLowerCase()].map(n => attrInterfaceToTagName[n] || n)[0];
|
|
||||||
if (!['all', 'webview'].includes(tagName)) {
|
|
||||||
for (const n of node.members.filter(n => n.kind === ts.SyntaxKind.PropertySignature)) {
|
|
||||||
// TODO Is escapedText the API for getting name?
|
|
||||||
const attr = [n.name.escapedText.toLowerCase()].map(n => attrNameNormalised[n] || n)[0];
|
|
||||||
const types = n.type.kind === ts.SyntaxKind.UnionType
|
|
||||||
? n.type.types.map(t => t.kind)
|
|
||||||
: [n.type.kind];
|
|
||||||
// If types includes boolean and string, make it a boolean attr to prevent it from being removed if empty value.
|
|
||||||
if (types.includes(ts.SyntaxKind.BooleanKeyword)) {
|
|
||||||
if (!booleanAttributes.has(attr)) {
|
|
||||||
booleanAttributes.set(attr, []);
|
|
||||||
}
|
|
||||||
booleanAttributes.get(attr).push(tagName);
|
|
||||||
} else if (types.includes(ts.SyntaxKind.StringKeyword) || types.includes(ts.SyntaxKind.NumberKeyword)) {
|
|
||||||
if (!redundantIfEmptyAttributes.has(attr)) {
|
|
||||||
redundantIfEmptyAttributes.set(attr, []);
|
|
||||||
}
|
|
||||||
redundantIfEmptyAttributes.get(attr).push(tagName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// forEachChild doesn't seem to work if return value is number (e.g. Array.prototype.push return value).
|
|
||||||
node.forEachChild(c => void unvisited.push(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort output JSON object by property so diffs are clearer.
|
|
||||||
await fs.writeFile(BOOLEAN_ATTRS_PATH, JSON.stringify(
|
|
||||||
Object.fromEntries([...booleanAttributes.entries()].sort((a, b) => a[0].localeCompare(b[0]))),
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
));
|
|
||||||
await fs.writeFile(REDUNDANT_IF_EMPTY_ATTRS_PATH, JSON.stringify(
|
|
||||||
Object.fromEntries([...redundantIfEmptyAttributes.entries()].sort((a, b) => a[0].localeCompare(b[0]))),
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const source = ts.createSourceFile(`react.d.ts`, await fetchReactTypingsSource(), ts.ScriptTarget.ES2019);
|
|
||||||
await processReactTypeDeclarations(source);
|
|
||||||
})();
|
|
|
@ -1,467 +0,0 @@
|
||||||
{
|
|
||||||
"abbr": [
|
|
||||||
"td",
|
|
||||||
"th"
|
|
||||||
],
|
|
||||||
"about": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"accept": [
|
|
||||||
"input"
|
|
||||||
],
|
|
||||||
"acceptcharset": [
|
|
||||||
"form"
|
|
||||||
],
|
|
||||||
"accesskey": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"action": [
|
|
||||||
"form"
|
|
||||||
],
|
|
||||||
"allow": [
|
|
||||||
"iframe"
|
|
||||||
],
|
|
||||||
"alt": [
|
|
||||||
"area",
|
|
||||||
"img",
|
|
||||||
"input"
|
|
||||||
],
|
|
||||||
"as": [
|
|
||||||
"link"
|
|
||||||
],
|
|
||||||
"autocapitalize": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"autocomplete": [
|
|
||||||
"form",
|
|
||||||
"input",
|
|
||||||
"select",
|
|
||||||
"textarea"
|
|
||||||
],
|
|
||||||
"autocorrect": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"autosave": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"cellpadding": [
|
|
||||||
"table"
|
|
||||||
],
|
|
||||||
"cellspacing": [
|
|
||||||
"table"
|
|
||||||
],
|
|
||||||
"challenge": [
|
|
||||||
"keygen"
|
|
||||||
],
|
|
||||||
"charset": [
|
|
||||||
"meta",
|
|
||||||
"script"
|
|
||||||
],
|
|
||||||
"cite": [
|
|
||||||
"blockquote",
|
|
||||||
"del",
|
|
||||||
"ins",
|
|
||||||
"quote"
|
|
||||||
],
|
|
||||||
"class": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"classid": [
|
|
||||||
"object"
|
|
||||||
],
|
|
||||||
"color": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"cols": [
|
|
||||||
"textarea"
|
|
||||||
],
|
|
||||||
"colspan": [
|
|
||||||
"td",
|
|
||||||
"th"
|
|
||||||
],
|
|
||||||
"content": [
|
|
||||||
"meta"
|
|
||||||
],
|
|
||||||
"contextmenu": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"controlslist": [
|
|
||||||
"media"
|
|
||||||
],
|
|
||||||
"coords": [
|
|
||||||
"area"
|
|
||||||
],
|
|
||||||
"crossorigin": [
|
|
||||||
"input",
|
|
||||||
"link",
|
|
||||||
"media",
|
|
||||||
"script"
|
|
||||||
],
|
|
||||||
"data": [
|
|
||||||
"object"
|
|
||||||
],
|
|
||||||
"datatype": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"datetime": [
|
|
||||||
"del",
|
|
||||||
"ins",
|
|
||||||
"time"
|
|
||||||
],
|
|
||||||
"defaultvalue": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"dir": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"dirname": [
|
|
||||||
"textarea"
|
|
||||||
],
|
|
||||||
"enctype": [
|
|
||||||
"form"
|
|
||||||
],
|
|
||||||
"form": [
|
|
||||||
"button",
|
|
||||||
"fieldset",
|
|
||||||
"input",
|
|
||||||
"keygen",
|
|
||||||
"label",
|
|
||||||
"meter",
|
|
||||||
"object",
|
|
||||||
"output",
|
|
||||||
"select",
|
|
||||||
"textarea"
|
|
||||||
],
|
|
||||||
"formaction": [
|
|
||||||
"button",
|
|
||||||
"input"
|
|
||||||
],
|
|
||||||
"formenctype": [
|
|
||||||
"button",
|
|
||||||
"input"
|
|
||||||
],
|
|
||||||
"formmethod": [
|
|
||||||
"button",
|
|
||||||
"input"
|
|
||||||
],
|
|
||||||
"formtarget": [
|
|
||||||
"button",
|
|
||||||
"input"
|
|
||||||
],
|
|
||||||
"frameborder": [
|
|
||||||
"iframe"
|
|
||||||
],
|
|
||||||
"headers": [
|
|
||||||
"td",
|
|
||||||
"th"
|
|
||||||
],
|
|
||||||
"height": [
|
|
||||||
"canvas",
|
|
||||||
"embed",
|
|
||||||
"iframe",
|
|
||||||
"img",
|
|
||||||
"input",
|
|
||||||
"object",
|
|
||||||
"video"
|
|
||||||
],
|
|
||||||
"high": [
|
|
||||||
"meter"
|
|
||||||
],
|
|
||||||
"href": [
|
|
||||||
"a",
|
|
||||||
"area",
|
|
||||||
"base",
|
|
||||||
"link"
|
|
||||||
],
|
|
||||||
"hreflang": [
|
|
||||||
"a",
|
|
||||||
"area",
|
|
||||||
"link"
|
|
||||||
],
|
|
||||||
"htmlfor": [
|
|
||||||
"label",
|
|
||||||
"output"
|
|
||||||
],
|
|
||||||
"httpequiv": [
|
|
||||||
"meta"
|
|
||||||
],
|
|
||||||
"id": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"integrity": [
|
|
||||||
"link",
|
|
||||||
"script"
|
|
||||||
],
|
|
||||||
"is": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"itemid": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"itemprop": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"itemref": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"itemtype": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"keyparams": [
|
|
||||||
"keygen"
|
|
||||||
],
|
|
||||||
"keytype": [
|
|
||||||
"keygen"
|
|
||||||
],
|
|
||||||
"kind": [
|
|
||||||
"track"
|
|
||||||
],
|
|
||||||
"label": [
|
|
||||||
"optgroup",
|
|
||||||
"option",
|
|
||||||
"track"
|
|
||||||
],
|
|
||||||
"lang": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"list": [
|
|
||||||
"input"
|
|
||||||
],
|
|
||||||
"low": [
|
|
||||||
"meter"
|
|
||||||
],
|
|
||||||
"manifest": [
|
|
||||||
"html"
|
|
||||||
],
|
|
||||||
"marginheight": [
|
|
||||||
"iframe"
|
|
||||||
],
|
|
||||||
"marginwidth": [
|
|
||||||
"iframe"
|
|
||||||
],
|
|
||||||
"max": [
|
|
||||||
"input",
|
|
||||||
"meter",
|
|
||||||
"progress"
|
|
||||||
],
|
|
||||||
"maxlength": [
|
|
||||||
"input",
|
|
||||||
"textarea"
|
|
||||||
],
|
|
||||||
"media": [
|
|
||||||
"a",
|
|
||||||
"area",
|
|
||||||
"link",
|
|
||||||
"source",
|
|
||||||
"style"
|
|
||||||
],
|
|
||||||
"mediagroup": [
|
|
||||||
"media"
|
|
||||||
],
|
|
||||||
"method": [
|
|
||||||
"form"
|
|
||||||
],
|
|
||||||
"min": [
|
|
||||||
"input",
|
|
||||||
"meter"
|
|
||||||
],
|
|
||||||
"minlength": [
|
|
||||||
"input",
|
|
||||||
"textarea"
|
|
||||||
],
|
|
||||||
"name": [
|
|
||||||
"button",
|
|
||||||
"fieldset",
|
|
||||||
"form",
|
|
||||||
"iframe",
|
|
||||||
"input",
|
|
||||||
"keygen",
|
|
||||||
"map",
|
|
||||||
"meta",
|
|
||||||
"object",
|
|
||||||
"output",
|
|
||||||
"param",
|
|
||||||
"select",
|
|
||||||
"textarea"
|
|
||||||
],
|
|
||||||
"nonce": [
|
|
||||||
"script",
|
|
||||||
"style"
|
|
||||||
],
|
|
||||||
"optimum": [
|
|
||||||
"meter"
|
|
||||||
],
|
|
||||||
"pattern": [
|
|
||||||
"input"
|
|
||||||
],
|
|
||||||
"ping": [
|
|
||||||
"a"
|
|
||||||
],
|
|
||||||
"placeholder": [
|
|
||||||
"",
|
|
||||||
"input",
|
|
||||||
"textarea"
|
|
||||||
],
|
|
||||||
"poster": [
|
|
||||||
"video"
|
|
||||||
],
|
|
||||||
"prefix": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"preload": [
|
|
||||||
"media"
|
|
||||||
],
|
|
||||||
"property": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"radiogroup": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"referrerpolicy": [
|
|
||||||
"a",
|
|
||||||
"iframe"
|
|
||||||
],
|
|
||||||
"rel": [
|
|
||||||
"a",
|
|
||||||
"area",
|
|
||||||
"link"
|
|
||||||
],
|
|
||||||
"resource": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"results": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"role": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"rows": [
|
|
||||||
"textarea"
|
|
||||||
],
|
|
||||||
"rowspan": [
|
|
||||||
"td",
|
|
||||||
"th"
|
|
||||||
],
|
|
||||||
"sandbox": [
|
|
||||||
"iframe"
|
|
||||||
],
|
|
||||||
"scope": [
|
|
||||||
"td",
|
|
||||||
"th"
|
|
||||||
],
|
|
||||||
"scrolling": [
|
|
||||||
"iframe"
|
|
||||||
],
|
|
||||||
"security": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"shape": [
|
|
||||||
"area"
|
|
||||||
],
|
|
||||||
"size": [
|
|
||||||
"input",
|
|
||||||
"select"
|
|
||||||
],
|
|
||||||
"sizes": [
|
|
||||||
"img",
|
|
||||||
"link",
|
|
||||||
"source"
|
|
||||||
],
|
|
||||||
"slot": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"span": [
|
|
||||||
"col",
|
|
||||||
"colgroup"
|
|
||||||
],
|
|
||||||
"src": [
|
|
||||||
"embed",
|
|
||||||
"iframe",
|
|
||||||
"img",
|
|
||||||
"input",
|
|
||||||
"media",
|
|
||||||
"script",
|
|
||||||
"source",
|
|
||||||
"track"
|
|
||||||
],
|
|
||||||
"srcdoc": [
|
|
||||||
"iframe"
|
|
||||||
],
|
|
||||||
"srclang": [
|
|
||||||
"track"
|
|
||||||
],
|
|
||||||
"srcset": [
|
|
||||||
"img",
|
|
||||||
"source"
|
|
||||||
],
|
|
||||||
"start": [
|
|
||||||
"ol"
|
|
||||||
],
|
|
||||||
"step": [
|
|
||||||
"input"
|
|
||||||
],
|
|
||||||
"summary": [
|
|
||||||
"table"
|
|
||||||
],
|
|
||||||
"tabindex": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"target": [
|
|
||||||
"a",
|
|
||||||
"area",
|
|
||||||
"base",
|
|
||||||
"form"
|
|
||||||
],
|
|
||||||
"title": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"type": [
|
|
||||||
"a",
|
|
||||||
"embed",
|
|
||||||
"input",
|
|
||||||
"link",
|
|
||||||
"menu",
|
|
||||||
"object",
|
|
||||||
"script",
|
|
||||||
"source",
|
|
||||||
"style"
|
|
||||||
],
|
|
||||||
"typeof": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"usemap": [
|
|
||||||
"img",
|
|
||||||
"object"
|
|
||||||
],
|
|
||||||
"value": [
|
|
||||||
"button",
|
|
||||||
"data",
|
|
||||||
"input",
|
|
||||||
"li",
|
|
||||||
"meter",
|
|
||||||
"option",
|
|
||||||
"param",
|
|
||||||
"progress",
|
|
||||||
"select",
|
|
||||||
"textarea"
|
|
||||||
],
|
|
||||||
"vocab": [
|
|
||||||
""
|
|
||||||
],
|
|
||||||
"width": [
|
|
||||||
"canvas",
|
|
||||||
"col",
|
|
||||||
"embed",
|
|
||||||
"iframe",
|
|
||||||
"img",
|
|
||||||
"input",
|
|
||||||
"object",
|
|
||||||
"video"
|
|
||||||
],
|
|
||||||
"wmode": [
|
|
||||||
"object"
|
|
||||||
],
|
|
||||||
"wrap": [
|
|
||||||
"textarea"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,5 +1,3 @@
|
||||||
use phf::{Map, Set};
|
|
||||||
|
|
||||||
pub struct SinglePattern {
|
pub struct SinglePattern {
|
||||||
pub seq: &'static [u8],
|
pub seq: &'static [u8],
|
||||||
pub table: &'static [usize],
|
pub table: &'static [usize],
|
||||||
|
@ -31,23 +29,3 @@ impl SinglePattern {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum AttrMapEntry {
|
|
||||||
AllHtmlElements,
|
|
||||||
SomeHtmlElements(&'static Set<&'static [u8]>),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AttrMap(Map<&'static [u8], AttrMapEntry>);
|
|
||||||
|
|
||||||
impl AttrMap {
|
|
||||||
pub const fn new(map: Map<&'static [u8], AttrMapEntry>) -> AttrMap {
|
|
||||||
AttrMap(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn contains(&self, tag: &[u8], attr: &[u8]) -> bool {
|
|
||||||
self.0.get(attr).filter(|elems| match elems {
|
|
||||||
AttrMapEntry::AllHtmlElements => true,
|
|
||||||
AttrMapEntry::SomeHtmlElements(set) => set.contains(tag),
|
|
||||||
}).is_some()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ impl ClosingTagOmissionRule {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn can_omit_as_prev(&self, after: &[u8]) -> bool {
|
pub fn can_omit_as_before(&self, after: &[u8]) -> bool {
|
||||||
self.followed_by.contains(after)
|
self.followed_by.contains(after)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use phf::{phf_set, Set};
|
use phf::Map;
|
||||||
|
|
||||||
use crate::err::ProcessingResult;
|
use crate::err::ProcessingResult;
|
||||||
use crate::proc::{Processor, ProcessorRange};
|
use crate::proc::{Processor, ProcessorRange};
|
||||||
|
@ -7,11 +7,34 @@ use crate::unit::attr::value::{DelimiterType, process_attr_value, ProcessedAttrV
|
||||||
|
|
||||||
mod value;
|
mod value;
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/gen_boolean_attrs.rs"));
|
pub struct AttributeMinification {
|
||||||
|
pub boolean: bool,
|
||||||
|
pub redundant_if_empty: bool,
|
||||||
|
pub collapse_and_trim: bool,
|
||||||
|
pub default_value: Option<&'static [u8]>,
|
||||||
|
}
|
||||||
|
|
||||||
static COLLAPSIBLE_AND_TRIMMABLE_ATTRS: Set<&'static [u8]> = phf_set! {
|
pub enum AttrMapEntry {
|
||||||
b"class",
|
AllHtmlElements(AttributeMinification),
|
||||||
};
|
DistinctHtmlElements(Map<&'static [u8], AttributeMinification>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AttrMap(Map<&'static [u8], &'static AttrMapEntry>);
|
||||||
|
|
||||||
|
impl AttrMap {
|
||||||
|
pub const fn new(map: Map<&'static [u8], &'static AttrMapEntry>) -> AttrMap {
|
||||||
|
AttrMap(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, tag: &[u8], attr: &[u8]) -> Option<&AttributeMinification> {
|
||||||
|
self.0.get(attr).and_then(|entry| match entry {
|
||||||
|
AttrMapEntry::AllHtmlElements(min) => Some(min),
|
||||||
|
AttrMapEntry::DistinctHtmlElements(map) => map.get(tag),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/gen_attrs.rs"));
|
||||||
|
|
||||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||||
pub enum AttrType {
|
pub enum AttrType {
|
||||||
|
@ -40,10 +63,11 @@ pub fn process_attr(proc: &mut Processor, element: ProcessorRange) -> Processing
|
||||||
// It's possible to expect attribute name but not be called at an attribute, e.g. due to whitespace between name and
|
// It's possible to expect attribute name but not be called at an attribute, e.g. due to whitespace between name and
|
||||||
// value, which causes name to be considered boolean attribute and `=` to be start of new (invalid) attribute name.
|
// value, which causes name to be considered boolean attribute and `=` to be start of new (invalid) attribute name.
|
||||||
let name = chain!(proc.match_while_pred(is_name_char).require_with_reason("attribute name")?.keep().out_range());
|
let name = chain!(proc.match_while_pred(is_name_char).require_with_reason("attribute name")?.keep().out_range());
|
||||||
let is_boolean = BOOLEAN_ATTRS.contains(&proc[element], &proc[name]);
|
let attr_cfg = ATTRS.get(&proc[element], &proc[name]);
|
||||||
|
let is_boolean = attr_cfg.filter(|attr| attr.boolean).is_some();
|
||||||
let after_name = proc.checkpoint();
|
let after_name = proc.checkpoint();
|
||||||
|
|
||||||
let should_collapse_and_trim_value_ws = COLLAPSIBLE_AND_TRIMMABLE_ATTRS.contains(&proc[name]);
|
let should_collapse_and_trim_value_ws = attr_cfg.filter(|attr| attr.collapse_and_trim).is_some();
|
||||||
chain!(proc.match_while_pred(is_whitespace).discard());
|
chain!(proc.match_while_pred(is_whitespace).discard());
|
||||||
let has_value = chain!(proc.match_char(b'=').keep().matched());
|
let has_value = chain!(proc.match_char(b'=').keep().matched());
|
||||||
|
|
||||||
|
@ -53,6 +77,9 @@ pub fn process_attr(proc: &mut Processor, element: ProcessorRange) -> Processing
|
||||||
chain!(proc.match_while_pred(is_whitespace).discard());
|
chain!(proc.match_while_pred(is_whitespace).discard());
|
||||||
if is_boolean {
|
if is_boolean {
|
||||||
skip_attr_value(proc)?;
|
skip_attr_value(proc)?;
|
||||||
|
// Discard `=`.
|
||||||
|
debug_assert_eq!(proc.written_count(after_name), 1);
|
||||||
|
proc.erase_written(after_name);
|
||||||
(AttrType::NoValue, None)
|
(AttrType::NoValue, None)
|
||||||
} else {
|
} else {
|
||||||
match process_attr_value(proc, should_collapse_and_trim_value_ws)? {
|
match process_attr_value(proc, should_collapse_and_trim_value_ws)? {
|
||||||
|
|
|
@ -195,10 +195,10 @@ pub fn process_content(proc: &mut Processor, parent: Option<ProcessorRange>) ->
|
||||||
// Whitespace is leading or trailing.
|
// Whitespace is leading or trailing.
|
||||||
// `trim` is on, so don't write it.
|
// `trim` is on, so don't write it.
|
||||||
} else if collapse {
|
} else if collapse {
|
||||||
|
// If writing space, then prev_sibling_closing_tag no longer represents immediate previous sibling node; space will be new previous sibling node (as a text node).
|
||||||
|
prev_sibling_closing_tag.take().map(|tag| tag.write_closing_tag(proc));
|
||||||
// Current contiguous whitespace needs to be reduced to a single space character.
|
// Current contiguous whitespace needs to be reduced to a single space character.
|
||||||
proc.write(b' ');
|
proc.write(b' ');
|
||||||
// If writing space, then prev_sibling_closing_tag no longer represents immediate previous sibling node.
|
|
||||||
prev_sibling_closing_tag.take().map(|tag| tag.write_closing_tag(proc));
|
|
||||||
} else {
|
} else {
|
||||||
unreachable!();
|
unreachable!();
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,13 +5,11 @@ use crate::proc::{Processor, ProcessorRange};
|
||||||
use crate::spec::codepoint::{is_alphanumeric, is_whitespace};
|
use crate::spec::codepoint::{is_alphanumeric, is_whitespace};
|
||||||
use crate::spec::tag::omission::CLOSING_TAG_OMISSION_RULES;
|
use crate::spec::tag::omission::CLOSING_TAG_OMISSION_RULES;
|
||||||
use crate::spec::tag::void::VOID_TAGS;
|
use crate::spec::tag::void::VOID_TAGS;
|
||||||
use crate::unit::attr::{AttrType, process_attr, ProcessedAttr};
|
use crate::unit::attr::{AttributeMinification, ATTRS, AttrType, process_attr, ProcessedAttr};
|
||||||
use crate::unit::content::process_content;
|
use crate::unit::content::process_content;
|
||||||
use crate::unit::script::process_script;
|
use crate::unit::script::process_script;
|
||||||
use crate::unit::style::process_style;
|
use crate::unit::style::process_style;
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/gen_redundant_if_empty_attrs.rs"));
|
|
||||||
|
|
||||||
pub static JAVASCRIPT_MIME_TYPES: Set<&'static [u8]> = phf_set! {
|
pub static JAVASCRIPT_MIME_TYPES: Set<&'static [u8]> = phf_set! {
|
||||||
b"application/ecmascript",
|
b"application/ecmascript",
|
||||||
b"application/javascript",
|
b"application/javascript",
|
||||||
|
@ -73,7 +71,7 @@ pub fn process_tag(proc: &mut Processor, prev_sibling_closing_tag: Option<Proces
|
||||||
let source_tag_name = chain!(proc.match_while_pred(is_valid_tag_name_char).require_with_reason("tag name")?.discard().range());
|
let source_tag_name = chain!(proc.match_while_pred(is_valid_tag_name_char).require_with_reason("tag name")?.discard().range());
|
||||||
if let Some(prev_tag) = prev_sibling_closing_tag {
|
if let Some(prev_tag) = prev_sibling_closing_tag {
|
||||||
let can_omit = match CLOSING_TAG_OMISSION_RULES.get(&proc[prev_tag.name]) {
|
let can_omit = match CLOSING_TAG_OMISSION_RULES.get(&proc[prev_tag.name]) {
|
||||||
Some(rule) => rule.can_omit_as_prev(&proc[source_tag_name]),
|
Some(rule) => rule.can_omit_as_before(&proc[source_tag_name]),
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
if !can_omit {
|
if !can_omit {
|
||||||
|
@ -142,7 +140,11 @@ pub fn process_tag(proc: &mut Processor, prev_sibling_closing_tag: Option<Proces
|
||||||
}
|
}
|
||||||
(_, name) => {
|
(_, name) => {
|
||||||
// TODO Check if HTML tag before checking if attribute removal applies to all elements.
|
// TODO Check if HTML tag before checking if attribute removal applies to all elements.
|
||||||
erase_attr = value.is_none() && REDUNDANT_IF_EMPTY_ATTRS.contains(&proc[tag_name], name);
|
erase_attr = match (value, ATTRS.get(&proc[tag_name], name)) {
|
||||||
|
(None, Some(AttributeMinification { redundant_if_empty: true, .. })) => true,
|
||||||
|
(Some(val), Some(AttributeMinification { default_value: Some(defval), .. })) => proc[val].eq(*defval),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if erase_attr {
|
if erase_attr {
|
||||||
|
@ -157,7 +159,11 @@ pub fn process_tag(proc: &mut Processor, prev_sibling_closing_tag: Option<Proces
|
||||||
if self_closing || is_void_tag {
|
if self_closing || is_void_tag {
|
||||||
if self_closing {
|
if self_closing {
|
||||||
// Write discarded tag closing characters.
|
// Write discarded tag closing characters.
|
||||||
if is_void_tag { proc.write_slice(b">"); } else { proc.write_slice(b"/>"); };
|
if is_void_tag {
|
||||||
|
proc.write_slice(b">");
|
||||||
|
} else {
|
||||||
|
proc.write_slice(b"/>");
|
||||||
|
};
|
||||||
};
|
};
|
||||||
return Ok(ProcessedTag { name: tag_name, has_closing_tag: false });
|
return Ok(ProcessedTag { name: tag_name, has_closing_tag: false });
|
||||||
};
|
};
|
||||||
|
@ -171,6 +177,7 @@ pub fn process_tag(proc: &mut Processor, prev_sibling_closing_tag: Option<Proces
|
||||||
// Require closing tag for non-void.
|
// Require closing tag for non-void.
|
||||||
chain!(proc.match_seq(b"</").require_with_reason("closing tag")?.discard());
|
chain!(proc.match_seq(b"</").require_with_reason("closing tag")?.discard());
|
||||||
let closing_tag = chain!(proc.match_while_pred(is_valid_tag_name_char).require_with_reason("closing tag name")?.discard().range());
|
let closing_tag = chain!(proc.match_while_pred(is_valid_tag_name_char).require_with_reason("closing tag name")?.discard().range());
|
||||||
|
// We need to check closing tag matches as otherwise when we later write closing tag, it might be longer than source closing tag and cause source to be overwritten.
|
||||||
if !proc[closing_tag].eq(&proc[tag_name]) {
|
if !proc[closing_tag].eq(&proc[tag_name]) {
|
||||||
return Err(ErrorType::ClosingTagMismatch);
|
return Err(ErrorType::ClosingTagMismatch);
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue