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
19 changed files with 2238 additions and 824 deletions
1830
gen/attrs.json
Normal file
1830
gen/attrs.json
Normal file
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": [
|
||||
""
|
||||
]
|
||||
}
|
||||
205
gen/build/attrs.js
Normal file
205
gen/build/attrs.js
Normal 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);
|
||||
})();
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
Reference in a new issue