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"
|
||||
|
||||
rm -rf node_modules
|
||||
if [ -f "$nodejs_cargo_toml.orig" ]; then
|
||||
echo 'Not altering Node.js Cargo.toml file'
|
||||
else
|
||||
cp "$nodejs_cargo_toml" "$nodejs_cargo_toml.orig"
|
||||
fi
|
||||
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"
|
||||
pushd hyperbuild-bench
|
||||
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
|
||||
},
|
||||
"hyperbuild-nodejs": {
|
||||
"absolute": 353526,
|
||||
"relative": 0.9585430133182942
|
||||
"absolute": 353501,
|
||||
"relative": 0.9584752288403974
|
||||
},
|
||||
"html-minifier": {
|
||||
"absolute": 353730,
|
||||
"relative": 0.9590961346579324
|
||||
"absolute": 355185,
|
||||
"relative": 0.9630411912715283
|
||||
},
|
||||
"minimize": {
|
||||
"absolute": 359912,
|
||||
|
@ -23,12 +23,12 @@
|
|||
"relative": 1
|
||||
},
|
||||
"hyperbuild-nodejs": {
|
||||
"absolute": 231730,
|
||||
"relative": 0.941161658212065
|
||||
"absolute": 231706,
|
||||
"relative": 0.9410641832204925
|
||||
},
|
||||
"html-minifier": {
|
||||
"absolute": 234186,
|
||||
"relative": 0.9511365990163149
|
||||
"absolute": 234306,
|
||||
"relative": 0.9516239739741772
|
||||
},
|
||||
"minimize": {
|
||||
"absolute": 238547,
|
||||
|
@ -41,8 +41,8 @@
|
|||
"relative": 1
|
||||
},
|
||||
"hyperbuild-nodejs": {
|
||||
"absolute": 90485,
|
||||
"relative": 0.983222679807452
|
||||
"absolute": 90484,
|
||||
"relative": 0.9832118136674309
|
||||
},
|
||||
"html-minifier": {
|
||||
"absolute": 90599,
|
||||
|
@ -59,12 +59,12 @@
|
|||
"relative": 1
|
||||
},
|
||||
"hyperbuild-nodejs": {
|
||||
"absolute": 271220,
|
||||
"relative": 0.8756319775813419
|
||||
"absolute": 270833,
|
||||
"relative": 0.8743825506389188
|
||||
},
|
||||
"html-minifier": {
|
||||
"absolute": 270355,
|
||||
"relative": 0.8728393307978899
|
||||
"absolute": 277911,
|
||||
"relative": 0.8972338268623564
|
||||
},
|
||||
"minimize": {
|
||||
"absolute": 278990,
|
||||
|
@ -77,12 +77,12 @@
|
|||
"relative": 1
|
||||
},
|
||||
"hyperbuild-nodejs": {
|
||||
"absolute": 79834,
|
||||
"relative": 0.9433520820532212
|
||||
"absolute": 79806,
|
||||
"relative": 0.9430212222904949
|
||||
},
|
||||
"html-minifier": {
|
||||
"absolute": 79273,
|
||||
"relative": 0.9367230703785981
|
||||
"absolute": 81446,
|
||||
"relative": 0.9624001512501772
|
||||
},
|
||||
"minimize": {
|
||||
"absolute": 81844,
|
||||
|
@ -95,12 +95,12 @@
|
|||
"relative": 1
|
||||
},
|
||||
"hyperbuild-nodejs": {
|
||||
"absolute": 5744300,
|
||||
"relative": 0.9094278392290278
|
||||
"absolute": 5744290,
|
||||
"relative": 0.9094262560459782
|
||||
},
|
||||
"html-minifier": {
|
||||
"absolute": 5663106,
|
||||
"relative": 0.896573342775437
|
||||
"absolute": 5785725,
|
||||
"relative": 0.9159861750123369
|
||||
},
|
||||
"minimize": {
|
||||
"absolute": 5799350,
|
||||
|
@ -113,12 +113,12 @@
|
|||
"relative": 1
|
||||
},
|
||||
"hyperbuild-nodejs": {
|
||||
"absolute": 196589,
|
||||
"relative": 0.9964468548836738
|
||||
"absolute": 196577,
|
||||
"relative": 0.9963860307162046
|
||||
},
|
||||
"html-minifier": {
|
||||
"absolute": 196568,
|
||||
"relative": 0.9963404125906027
|
||||
"absolute": 196600,
|
||||
"relative": 0.9965026103705206
|
||||
},
|
||||
"minimize": {
|
||||
"absolute": 196776,
|
||||
|
@ -131,12 +131,12 @@
|
|||
"relative": 1
|
||||
},
|
||||
"hyperbuild-nodejs": {
|
||||
"absolute": 28189,
|
||||
"relative": 0.8285764674759707
|
||||
"absolute": 28154,
|
||||
"relative": 0.8275476911319479
|
||||
},
|
||||
"html-minifier": {
|
||||
"absolute": 28109,
|
||||
"relative": 0.8262249786896328
|
||||
"absolute": 29086,
|
||||
"relative": 0.8549425354927839
|
||||
},
|
||||
"minimize": {
|
||||
"absolute": 30318,
|
||||
|
@ -149,12 +149,12 @@
|
|||
"relative": 1
|
||||
},
|
||||
"hyperbuild-nodejs": {
|
||||
"absolute": 1263698,
|
||||
"relative": 0.9967377432692293
|
||||
"absolute": 1263682,
|
||||
"relative": 0.9967251233205608
|
||||
},
|
||||
"html-minifier": {
|
||||
"absolute": 1263082,
|
||||
"relative": 0.9962518752454974
|
||||
"absolute": 1263150,
|
||||
"relative": 0.9963055100273379
|
||||
},
|
||||
"minimize": {
|
||||
"absolute": 1264354,
|
||||
|
@ -167,12 +167,12 @@
|
|||
"relative": 1
|
||||
},
|
||||
"hyperbuild-nodejs": {
|
||||
"absolute": 648036,
|
||||
"relative": 0.9945654926432332
|
||||
"absolute": 648004,
|
||||
"relative": 0.9945163810263407
|
||||
},
|
||||
"html-minifier": {
|
||||
"absolute": 647776,
|
||||
"relative": 0.9941664607559813
|
||||
"absolute": 647822,
|
||||
"relative": 0.9942370587052644
|
||||
},
|
||||
"minimize": {
|
||||
"absolute": 648422,
|
||||
|
@ -185,12 +185,12 @@
|
|||
"relative": 1
|
||||
},
|
||||
"hyperbuild-nodejs": {
|
||||
"absolute": 86770,
|
||||
"relative": 0.7731513245239644
|
||||
"absolute": 86693,
|
||||
"relative": 0.7724652273476552
|
||||
},
|
||||
"html-minifier": {
|
||||
"absolute": 86383,
|
||||
"relative": 0.7697030179365405
|
||||
"absolute": 88366,
|
||||
"relative": 0.7873722478147359
|
||||
},
|
||||
"minimize": {
|
||||
"absolute": 89682,
|
||||
|
@ -203,12 +203,12 @@
|
|||
"relative": 1
|
||||
},
|
||||
"hyperbuild-nodejs": {
|
||||
"absolute": 272544,
|
||||
"relative": 0.8647962583371939
|
||||
"absolute": 272480,
|
||||
"relative": 0.8645931830152878
|
||||
},
|
||||
"html-minifier": {
|
||||
"absolute": 265242,
|
||||
"relative": 0.841626633328468
|
||||
"absolute": 266639,
|
||||
"relative": 0.8460593868394499
|
||||
},
|
||||
"minimize": {
|
||||
"absolute": 307860,
|
||||
|
@ -221,12 +221,12 @@
|
|||
"relative": 1
|
||||
},
|
||||
"hyperbuild-nodejs": {
|
||||
"absolute": 1319433,
|
||||
"relative": 0.9350021790660842
|
||||
"absolute": 1319432,
|
||||
"relative": 0.9350014704267072
|
||||
},
|
||||
"html-minifier": {
|
||||
"absolute": 1308864,
|
||||
"relative": 0.927512569490949
|
||||
"absolute": 1327244,
|
||||
"relative": 0.940537361239552
|
||||
},
|
||||
"minimize": {
|
||||
"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,
|
||||
collapseInlineTagWhitespace: 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: [],
|
||||
decodeEntities: true,
|
||||
ignoreCustomComments: [],
|
||||
|
|
|
@ -7,7 +7,7 @@ const tests = require('./tests');
|
|||
for (const t of tests) {
|
||||
for (const p of Object.keys(programs)) {
|
||||
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));
|
||||
fs.writeFileSync(minPath, programs[p](t.contentAsString, t.contentAsBuffer));
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,67 +1,67 @@
|
|||
{
|
||||
"Amazon": {
|
||||
"hyperbuild-nodejs": 508.6052506017873,
|
||||
"html-minifier": 42.25586310792637,
|
||||
"minimize": 112.84668317196737
|
||||
"hyperbuild-nodejs": 499.9428905642954,
|
||||
"html-minifier": 36.3036627447208,
|
||||
"minimize": 113.33527430279831
|
||||
},
|
||||
"BBC": {
|
||||
"hyperbuild-nodejs": 532.8102905973569,
|
||||
"html-minifier": 54.543166381705994,
|
||||
"minimize": 158.04711027873367
|
||||
"hyperbuild-nodejs": 534.5358584753244,
|
||||
"html-minifier": 49.23362197831472,
|
||||
"minimize": 161.96803168572117
|
||||
},
|
||||
"Bing": {
|
||||
"hyperbuild-nodejs": 2163.2302078550147,
|
||||
"html-minifier": 226.06394079630297,
|
||||
"minimize": 545.0653574947338
|
||||
"hyperbuild-nodejs": 2137.27013270331,
|
||||
"html-minifier": 223.08512226985184,
|
||||
"minimize": 550.9181761277221
|
||||
},
|
||||
"Bootstrap": {
|
||||
"hyperbuild-nodejs": 276.9069916779089,
|
||||
"html-minifier": 9.088104379498866,
|
||||
"minimize": 23.551904759608103
|
||||
"hyperbuild-nodejs": 277.1391080206004,
|
||||
"html-minifier": 8.043255283692064,
|
||||
"minimize": 22.245439492019898
|
||||
},
|
||||
"Coding Horror": {
|
||||
"hyperbuild-nodejs": 1130.8127333329164,
|
||||
"html-minifier": 57.409616058969526,
|
||||
"minimize": 187.28893041569714
|
||||
"hyperbuild-nodejs": 1096.4910673601032,
|
||||
"html-minifier": 49.83595257976626,
|
||||
"minimize": 188.32749988717788
|
||||
},
|
||||
"ECMA-262": {
|
||||
"hyperbuild-nodejs": 16.78425400473028,
|
||||
"html-minifier": 0.5081461293026476,
|
||||
"minimize": 1.3377251957362182
|
||||
"hyperbuild-nodejs": 16.200240897950334,
|
||||
"html-minifier": 0.45522858062374655,
|
||||
"minimize": 1.3356053866389666
|
||||
},
|
||||
"Google": {
|
||||
"hyperbuild-nodejs": 1856.1401576820815,
|
||||
"html-minifier": 326.33685434942925,
|
||||
"minimize": 555.6574376056606
|
||||
"hyperbuild-nodejs": 1832.4626475818236,
|
||||
"html-minifier": 242.1462398878334,
|
||||
"minimize": 564.1884364813526
|
||||
},
|
||||
"Hacker News": {
|
||||
"hyperbuild-nodejs": 2243.718482073889,
|
||||
"html-minifier": 86.20536879655822,
|
||||
"minimize": 272.83901988612814
|
||||
"hyperbuild-nodejs": 2127.8084431041266,
|
||||
"html-minifier": 74.78979361035866,
|
||||
"minimize": 272.3995630011103
|
||||
},
|
||||
"NY Times": {
|
||||
"hyperbuild-nodejs": 281.05098122387943,
|
||||
"html-minifier": 35.57258739419913,
|
||||
"minimize": 85.73538788107287
|
||||
"hyperbuild-nodejs": 265.55362689112695,
|
||||
"html-minifier": 37.146711151201565,
|
||||
"minimize": 87.7133467873164
|
||||
},
|
||||
"Reddit": {
|
||||
"hyperbuild-nodejs": 410.12831233429847,
|
||||
"html-minifier": 45.16614107393938,
|
||||
"minimize": 122.77229783626386
|
||||
"hyperbuild-nodejs": 391.75000439723124,
|
||||
"html-minifier": 45.067854272152125,
|
||||
"minimize": 125.87983932864549
|
||||
},
|
||||
"Stack Overflow": {
|
||||
"hyperbuild-nodejs": 844.9492250936706,
|
||||
"html-minifier": 47.752958982642866,
|
||||
"minimize": 157.06321076776504
|
||||
"hyperbuild-nodejs": 818.3755008258345,
|
||||
"html-minifier": 41.43093414076361,
|
||||
"minimize": 159.71387801780298
|
||||
},
|
||||
"Twitter": {
|
||||
"hyperbuild-nodejs": 279.4114354197891,
|
||||
"html-minifier": 41.71822833204345,
|
||||
"minimize": 164.8527211050192
|
||||
"hyperbuild-nodejs": 274.57816497268476,
|
||||
"html-minifier": 36.94949014023178,
|
||||
"minimize": 168.81796573617953
|
||||
},
|
||||
"Wikipedia": {
|
||||
"hyperbuild-nodejs": 57.721598283350104,
|
||||
"html-minifier": 3.1325913382989596,
|
||||
"minimize": 8.741663457588
|
||||
"hyperbuild-nodejs": 54.852210553433345,
|
||||
"html-minifier": 2.821530343574604,
|
||||
"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) {
|
||||
let name_words = name_words(name);
|
||||
let snake_case = snake_case(&name_words);
|
||||
let file_name = name_words.join("_");
|
||||
let attrs: HashMap<String, Vec<String>> = read_json(file_name.as_str());
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct TagAttr {
|
||||
boolean: bool,
|
||||
redundant_if_empty: bool,
|
||||
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();
|
||||
for (name, elems) in attrs.iter() {
|
||||
if !elems.contains(&"".to_string()) {
|
||||
for (attr_name, tags_map) in attrs.iter() {
|
||||
if let Some(global_attr) = tags_map.get("") {
|
||||
code.push_str(format!(
|
||||
"static {}_{}_ATTR: &phf::Set<&'static [u8]> = &phf::phf_set!({});\n\n",
|
||||
name.to_uppercase(),
|
||||
snake_case,
|
||||
elems.iter().map(|e| format!("b\"{}\"", e)).collect::<Vec<String>>().join(", "),
|
||||
"static {}_ATTR: &AttrMapEntry = &AttrMapEntry::AllHtmlElements({});\n\n",
|
||||
attr_name.to_uppercase(),
|
||||
global_attr.code(),
|
||||
).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());
|
||||
};
|
||||
};
|
||||
code.push_str(format!("pub static {}: crate::pattern::AttrMap = crate::pattern::AttrMap::new(phf::phf_map!{{\n", snake_case).as_str());
|
||||
for (name, elems) in attrs.iter() {
|
||||
if elems.contains(&"".to_string()) {
|
||||
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("pub static ATTRS: AttrMap = AttrMap::new(phf::phf_map! {\n");
|
||||
for attr_name in attrs.keys() {
|
||||
code.push_str(format!("\tb\"{}\" => {}_ATTR,\n", attr_name, attr_name.to_uppercase()).as_str());
|
||||
};
|
||||
code.push_str("});\n\n");
|
||||
write_rs(file_name.as_str(), code);
|
||||
write_rs("attrs", code);
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
@ -182,8 +217,7 @@ fn generate_tries() {
|
|||
}
|
||||
|
||||
fn main() {
|
||||
generate_attr_map("boolean attrs");
|
||||
generate_attr_map("redundant if empty attrs");
|
||||
generate_attr_map();
|
||||
generate_entities();
|
||||
generate_patterns();
|
||||
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 seq: &'static [u8],
|
||||
pub table: &'static [usize],
|
||||
|
@ -31,23 +29,3 @@ impl SinglePattern {
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use phf::{phf_set, Set};
|
||||
use phf::Map;
|
||||
|
||||
use crate::err::ProcessingResult;
|
||||
use crate::proc::{Processor, ProcessorRange};
|
||||
|
@ -7,11 +7,34 @@ use crate::unit::attr::value::{DelimiterType, process_attr_value, ProcessedAttrV
|
|||
|
||||
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! {
|
||||
b"class",
|
||||
};
|
||||
pub enum AttrMapEntry {
|
||||
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)]
|
||||
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
|
||||
// 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 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 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());
|
||||
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());
|
||||
if is_boolean {
|
||||
skip_attr_value(proc)?;
|
||||
// Discard `=`.
|
||||
debug_assert_eq!(proc.written_count(after_name), 1);
|
||||
proc.erase_written(after_name);
|
||||
(AttrType::NoValue, None)
|
||||
} else {
|
||||
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.
|
||||
// `trim` is on, so don't write it.
|
||||
} 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.
|
||||
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 {
|
||||
unreachable!();
|
||||
};
|
||||
|
|
|
@ -5,13 +5,11 @@ use crate::proc::{Processor, ProcessorRange};
|
|||
use crate::spec::codepoint::{is_alphanumeric, is_whitespace};
|
||||
use crate::spec::tag::omission::CLOSING_TAG_OMISSION_RULES;
|
||||
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::script::process_script;
|
||||
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! {
|
||||
b"application/ecmascript",
|
||||
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());
|
||||
if let Some(prev_tag) = prev_sibling_closing_tag {
|
||||
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,
|
||||
};
|
||||
if !can_omit {
|
||||
|
@ -142,7 +140,11 @@ pub fn process_tag(proc: &mut Processor, prev_sibling_closing_tag: Option<Proces
|
|||
}
|
||||
(_, name) => {
|
||||
// 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 {
|
||||
|
@ -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 {
|
||||
// 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 });
|
||||
};
|
||||
|
@ -171,6 +177,7 @@ pub fn process_tag(proc: &mut Processor, prev_sibling_closing_tag: Option<Proces
|
|||
// Require closing tag for non-void.
|
||||
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());
|
||||
// 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]) {
|
||||
return Err(ErrorType::ClosingTagMismatch);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue