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:
Wilson Lin 2020-01-18 11:42:01 +11:00
commit 27af2368ff
19 changed files with 2238 additions and 824 deletions

1830
gen/attrs.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -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": [
""
]
}

205
gen/build/attrs.js Normal file
View file

@ -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);
})();

View file

@ -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);
})();

View file

@ -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"
]
}