html-validate
Version:
Offline HTML5 validator and linter
2,403 lines (2,402 loc) • 89.1 kB
JavaScript
import { d as defineMetadata, m as metadataHelper } from './meta-helper.js';
const {
allowedIfAttributeIsPresent,
allowedIfAttributeIsAbsent,
allowedIfAttributeHasValue,
allowedIfParentIsPresent
} = metadataHelper;
const validId = "/\\S+/";
const ReferrerPolicy = [
"",
"no-referrer",
"no-referrer-when-downgrade",
"same-origin",
"origin",
"strict-origin",
"origin-when-cross-origin",
"strict-origin-when-cross-origin",
"unsafe-url"
];
function isInsideLandmark(node) {
const selectors = [
"article",
"aside",
"main",
"nav",
"section",
'[role="article"]',
'[role="complementary"]',
'[role="main"]',
'[role="navigation"]',
'[role="region"]'
];
return Boolean(node.closest(selectors.join(",")));
}
function linkBodyOk(node) {
if (node.hasAttribute("itemprop")) {
return true;
}
const rel = node.getAttribute("rel");
if (!rel) {
return false;
}
if (typeof rel !== "string") {
return false;
}
const bodyOk = [
"dns-prefetch",
"modulepreload",
"pingback",
"preconnect",
"prefetch",
"preload",
"stylesheet"
];
const tokens = rel.toLowerCase().split(/\s+/);
return tokens.some((keyword) => bodyOk.includes(keyword));
}
var html5 = defineMetadata({
"*": {
attributes: {
contenteditable: {
omit: true,
enum: ["true", "false"]
},
contextmenu: {
deprecated: true
},
dir: {
enum: ["ltr", "rtl", "auto"]
},
draggable: {
enum: ["true", "false"]
},
hidden: {
boolean: true
},
id: {
enum: [validId]
},
inert: {
boolean: true
},
spellcheck: {
omit: true,
enum: ["true", "false"]
},
tabindex: {
enum: ["/-?\\d+/"]
}
}
},
a: {
flow: true,
focusable(node) {
return node.hasAttribute("href");
},
phrasing: true,
interactive: true,
transparent: true,
attributes: {
charset: {
deprecated: true
},
coords: {
deprecated: true
},
datafld: {
deprecated: true
},
datasrc: {
deprecated: true
},
download: {
allowed: allowedIfAttributeIsPresent("href"),
omit: true,
enum: ["/.+/"]
},
href: {
enum: ["/.*/"]
},
hreflang: {
allowed: allowedIfAttributeIsPresent("href")
},
itemprop: {
allowed: allowedIfAttributeIsPresent("href")
},
methods: {
deprecated: true
},
name: {
deprecated: true
},
ping: {
allowed: allowedIfAttributeIsPresent("href")
},
referrerpolicy: {
allowed: allowedIfAttributeIsPresent("href"),
enum: ReferrerPolicy
},
rel: {
allowed(node, attr) {
if (!node.hasAttribute("href")) {
return `requires "href" attribute to be present`;
}
if (!attr || attr === "" || typeof attr !== "string") {
return null;
}
const disallowed = [
/* whatwg */
"canonical",
"dns-prefetch",
"expect",
"icon",
"manifest",
"modulepreload",
"pingback",
"preconnect",
"prefetch",
"preload",
"stylesheet",
/* microformats.org */
"apple-touch-icon",
"apple-touch-icon-precomposed",
"apple-touch-startup-image",
"authorization_endpoint",
"component",
"chrome-webstore-item",
"dns-prefetch",
"edit",
"gbfs",
"gtfs-static",
"gtfs-realtime",
"import",
"mask-icon",
"meta",
"micropub",
"openid.delegate",
"openid.server",
"openid2.local_id",
"openid2.provider",
"p3pv1",
"pgpkey",
"schema.dcterms",
"service",
"shortlink",
"sitemap",
"subresource",
"sword",
"timesheet",
"token_endpoint",
"wlwmanifest",
"stylesheet/less",
"token_endpoint",
"yandex-tableau-widget"
];
const tokens = attr.toLowerCase().split(/\s+/);
for (const keyword of tokens) {
if (disallowed.includes(keyword)) {
return `<a> does not allow rel="${keyword}"`;
}
if (keyword.startsWith("dcterms.")) {
return `<a> does not allow rel="${keyword}"`;
}
}
return null;
},
list: true,
enum: ["/.+/"]
},
shape: {
deprecated: true
},
target: {
allowed: allowedIfAttributeIsPresent("href"),
enum: ["/[^_].*/", "_blank", "_self", "_parent", "_top"]
},
type: {
allowed: allowedIfAttributeIsPresent("href")
},
urn: {
deprecated: true
}
},
permittedDescendants: [{ exclude: "@interactive" }],
aria: {
implicitRole(node) {
return node.hasAttribute("href") ? "link" : "generic";
},
naming(node) {
return node.hasAttribute("href") ? "allowed" : "prohibited";
}
}
},
abbr: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing"],
aria: {
naming: "prohibited"
}
},
acronym: {
deprecated: {
message: "use <abbr> instead",
documentation: "`<abbr>` can be used as a replacement.",
source: "html5"
}
},
address: {
flow: true,
aria: {
implicitRole: "group"
},
permittedContent: ["@flow"],
permittedDescendants: [{ exclude: ["address", "header", "footer", "@heading", "@sectioning"] }]
},
applet: {
deprecated: {
source: "html5"
},
attributes: {
datafld: {
deprecated: true
},
datasrc: {
deprecated: true
}
}
},
area: {
flow(node) {
return Boolean(node.closest("map"));
},
focusable(node) {
return node.hasAttribute("href");
},
phrasing(node) {
return Boolean(node.closest("map"));
},
void: true,
attributes: {
alt: {},
coords: {
allowed(node) {
const attr = node.getAttribute("shape");
if (attr === "default") {
return `cannot be used when "shape" attribute is "default"`;
} else {
return null;
}
}
},
download: {
allowed: allowedIfAttributeIsPresent("href")
},
nohref: {
deprecated: true
},
itemprop: {
allowed: allowedIfAttributeIsPresent("href")
},
ping: {
allowed: allowedIfAttributeIsPresent("href")
},
referrerpolicy: {
allowed: allowedIfAttributeIsPresent("href"),
enum: ReferrerPolicy
},
rel: {
allowed(node, attr) {
if (!node.hasAttribute("href")) {
return `requires "href" attribute to be present`;
}
if (!attr || attr === "" || typeof attr !== "string") {
return null;
}
const disallowed = [
/* whatwg */
"canonical",
"dns-prefetch",
"expect",
"icon",
"manifest",
"modulepreload",
"pingback",
"preconnect",
"prefetch",
"preload",
"stylesheet",
/* microformats.org */
"apple-touch-icon",
"apple-touch-icon-precomposed",
"apple-touch-startup-image",
"authorization_endpoint",
"component",
"chrome-webstore-item",
"dns-prefetch",
"edit",
"gbfs",
"gtfs-static",
"gtfs-realtime",
"import",
"mask-icon",
"meta",
"micropub",
"openid.delegate",
"openid.server",
"openid2.local_id",
"openid2.provider",
"p3pv1",
"pgpkey",
"schema.dcterms",
"service",
"shortlink",
"sitemap",
"subresource",
"sword",
"timesheet",
"token_endpoint",
"wlwmanifest",
"stylesheet/less",
"token_endpoint",
"yandex-tableau-widget"
];
const tokens = attr.toLowerCase().split(/\s+/);
for (const keyword of tokens) {
if (disallowed.includes(keyword)) {
return `<area> does not allow rel="${keyword}"`;
}
if (keyword.startsWith("dcterms.")) {
return `<area> does not allow rel="${keyword}"`;
}
}
return null;
}
},
shape: {
allowed(node, attr) {
const shape = attr ?? "rect";
switch (shape) {
case "circ":
case "circle":
case "poly":
case "polygon":
case "rect":
case "rectangle":
return allowedIfAttributeIsPresent("coords")(node, attr);
default:
return null;
}
},
enum: ["rect", "circle", "poly", "default"]
},
target: {
allowed: allowedIfAttributeIsPresent("href"),
enum: ["/[^_].*/", "_blank", "_self", "_parent", "_top"]
}
},
aria: {
implicitRole(node) {
return node.hasAttribute("href") ? "link" : "generic";
},
naming(node) {
return node.hasAttribute("href") ? "allowed" : "prohibited";
}
},
requiredAncestors: ["map", "template"]
},
article: {
flow: true,
sectioning: true,
permittedContent: ["@flow"],
permittedDescendants: [{ exclude: ["main"] }],
aria: {
implicitRole: "article"
}
},
aside: {
flow: true,
sectioning: true,
permittedContent: ["@flow"],
permittedDescendants: [{ exclude: ["main"] }],
aria: {
implicitRole: "complementary"
}
},
audio: {
flow: true,
focusable(node) {
return node.hasAttribute("controls");
},
phrasing: true,
embedded: true,
interactive(node) {
return node.hasAttribute("controls");
},
transparent: ["@flow"],
attributes: {
crossorigin: {
omit: true,
enum: ["anonymous", "use-credentials"]
},
itemprop: {
allowed: allowedIfAttributeIsPresent("src")
},
preload: {
omit: true,
enum: ["none", "metadata", "auto"]
}
},
permittedContent: ["@flow", "track", "source"],
permittedDescendants: [{ exclude: ["audio", "video"] }],
permittedOrder: ["source", "track", "@flow"]
},
b: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing"],
aria: {
implicitRole: "generic",
naming: "prohibited"
}
},
base: {
metadata: true,
void: true,
permittedParent: ["head"],
aria: {
naming: "prohibited"
}
},
basefont: {
deprecated: {
message: "use CSS instead",
documentation: "Use CSS `font-size` property instead.",
source: "html4"
}
},
bdi: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing"],
aria: {
implicitRole: "generic",
naming: "prohibited"
}
},
bdo: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing"],
aria: {
implicitRole: "generic",
naming: "prohibited"
}
},
bgsound: {
deprecated: {
message: "use <audio> instead",
documentation: "Use the `<audio>` element instead but consider accessibility concerns with autoplaying sounds.",
source: "non-standard"
}
},
big: {
deprecated: {
message: "use CSS instead",
documentation: "Use CSS `font-size` property instead.",
source: "html5"
}
},
blink: {
deprecated: {
documentation: "`<blink>` has no direct replacement and blinking text is frowned upon by accessibility standards.",
source: "non-standard"
}
},
blockquote: {
flow: true,
sectioning: true,
aria: {
implicitRole: "blockquote"
},
permittedContent: ["@flow"]
},
body: {
permittedContent: ["@flow"],
permittedParent: ["html"],
attributes: {
alink: {
deprecated: true
},
background: {
deprecated: true
},
bgcolor: {
deprecated: true
},
link: {
deprecated: true
},
marginbottom: {
deprecated: true
},
marginheight: {
deprecated: true
},
marginleft: {
deprecated: true
},
marginright: {
deprecated: true
},
margintop: {
deprecated: true
},
marginwidth: {
deprecated: true
},
text: {
deprecated: true
},
vlink: {
deprecated: true
}
},
aria: {
implicitRole: "generic",
naming: "prohibited"
}
},
br: {
flow: true,
phrasing: true,
void: true,
attributes: {
clear: {
deprecated: true
}
},
aria: {
naming: "prohibited"
}
},
button: {
flow: true,
focusable: true,
phrasing: true,
interactive: true,
formAssociated: {
disablable: true,
listed: true
},
labelable: true,
attributes: {
autofocus: {
boolean: true
},
datafld: {
deprecated: true
},
dataformatas: {
deprecated: true
},
datasrc: {
deprecated: true
},
disabled: {
boolean: true
},
formaction: {
allowed: allowedIfAttributeHasValue("type", ["submit"], { defaultValue: "submit" })
},
formenctype: {
allowed: allowedIfAttributeHasValue("type", ["submit"], { defaultValue: "submit" })
},
formmethod: {
allowed: allowedIfAttributeHasValue("type", ["submit"], { defaultValue: "submit" }),
enum: ["get", "post", "dialog"]
},
formnovalidate: {
allowed: allowedIfAttributeHasValue("type", ["submit"], { defaultValue: "submit" }),
boolean: true
},
formtarget: {
allowed: allowedIfAttributeHasValue("type", ["submit"], { defaultValue: "submit" }),
enum: ["/[^_].*/", "_blank", "_self", "_parent", "_top"]
},
type: {
enum: ["submit", "reset", "button"]
}
},
aria: {
implicitRole: "button"
},
permittedContent: ["@phrasing"],
permittedDescendants: [{ exclude: ["@interactive"] }],
textContent: "accessible"
},
canvas: {
flow: true,
phrasing: true,
embedded: true,
transparent: true
},
caption: {
permittedContent: ["@flow"],
permittedDescendants: [{ exclude: ["table"] }],
attributes: {
align: {
deprecated: true
}
},
aria: {
implicitRole: "caption",
naming: "prohibited"
}
},
center: {
deprecated: {
message: "use CSS instead",
documentation: "Use the CSS `text-align` or `margin: auto` properties instead.",
source: "html4"
}
},
cite: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing"],
aria: {
naming: "prohibited"
}
},
code: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing"],
aria: {
implicitRole: "code",
naming: "prohibited"
}
},
col: {
attributes: {
align: {
deprecated: true
},
char: {
deprecated: true
},
charoff: {
deprecated: true
},
span: {
enum: ["/\\d+/"]
},
valign: {
deprecated: true
},
width: {
deprecated: true
}
},
void: true,
aria: {
naming: "prohibited"
}
},
colgroup: {
implicitClosed: ["colgroup"],
attributes: {
span: {
enum: ["/\\d+/"]
}
},
permittedContent: ["col", "template"],
aria: {
naming: "prohibited"
}
},
data: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing"],
aria: {
implicitRole: "generic",
naming: "prohibited"
}
},
datalist: {
flow: true,
phrasing: true,
aria: {
implicitRole: "listbox",
naming: "prohibited"
},
permittedContent: ["@phrasing", "option"]
},
dd: {
implicitClosed: ["dd", "dt"],
permittedContent: ["@flow"],
requiredAncestors: ["dl > dd", "dl > div > dd", "template > dd", "template > div > dd"]
},
del: {
flow: true,
phrasing: true,
transparent: true,
aria: {
implicitRole: "deletion",
naming: "prohibited"
}
},
details: {
flow: true,
sectioning: true,
interactive: true,
attributes: {
open: {
boolean: true
}
},
aria: {
implicitRole: "group"
},
permittedContent: ["summary", "@flow"],
permittedOrder: ["summary", "@flow"],
requiredContent: ["summary"]
},
dfn: {
flow: true,
phrasing: true,
aria: {
implicitRole: "term"
},
permittedContent: ["@phrasing"],
permittedDescendants: [{ exclude: ["dfn"] }]
},
dialog: {
flow: true,
permittedContent: ["@flow"],
attributes: {
open: {
boolean: true
}
},
aria: {
implicitRole: "dialog"
}
},
dir: {
deprecated: {
documentation: "The non-standard `<dir>` element has no direct replacement but MDN recommends replacing with `<ul>` and CSS.",
source: "html4"
}
},
div: {
flow: true,
permittedContent: ["@flow", "dt", "dd"],
attributes: {
align: {
deprecated: true
},
datafld: {
deprecated: true
},
dataformatas: {
deprecated: true
},
datasrc: {
deprecated: true
}
},
aria: {
implicitRole: "generic",
naming: "prohibited"
}
},
dl: {
flow: true,
permittedContent: ["@script", "dt", "dd", "div"],
attributes: {
compact: {
deprecated: true
}
}
},
dt: {
implicitClosed: ["dd", "dt"],
permittedContent: ["@flow"],
permittedDescendants: [{ exclude: ["header", "footer", "@sectioning", "@heading"] }],
requiredAncestors: ["dl > dt", "dl > div > dt", "template > dt", "template > div > dt"]
},
em: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing"],
aria: {
implicitRole: "emphasis",
naming: "prohibited"
}
},
embed: {
flow: true,
phrasing: true,
embedded: true,
interactive: true,
void: true,
attributes: {
height: {
enum: ["/\\d+/"]
},
src: {
required: true,
enum: ["/.+/"]
},
title: {
required: true
},
width: {
enum: ["/\\d+/"]
}
}
},
fieldset: {
flow: true,
formAssociated: {
disablable: true,
listed: true
},
attributes: {
datafld: {
deprecated: true
},
disabled: {
boolean: true
}
},
aria: {
implicitRole: "group"
},
permittedContent: ["@flow", "legend?"],
permittedOrder: ["legend", "@flow"]
},
figcaption: {
permittedContent: ["@flow"],
aria: {
naming: "prohibited"
}
},
figure: {
flow: true,
aria: {
implicitRole: "figure"
},
permittedContent: ["@flow", "figcaption?"],
permittedOrder: ["figcaption", "@flow", "figcaption"]
},
font: {
deprecated: {
message: "use CSS instead",
documentation: "Use CSS font properties instead.",
source: "html4"
}
},
footer: {
flow: true,
aria: {
implicitRole(node) {
if (isInsideLandmark(node)) {
return "generic";
} else {
return "contentinfo";
}
},
naming(node) {
if (isInsideLandmark(node)) {
return "prohibited";
} else {
return "allowed";
}
}
},
permittedContent: ["@flow"],
permittedDescendants: [{ exclude: ["header", "footer", "main"] }]
},
form: {
flow: true,
form: true,
attributes: {
action: {
enum: [/^\s*\S+\s*$/]
},
accept: {
deprecated: true
},
autocomplete: {
enum: ["on", "off"]
},
method: {
enum: ["get", "post", "dialog"]
},
novalidate: {
boolean: true
},
rel: {
allowed(_, attr) {
if (!attr || attr === "" || typeof attr !== "string") {
return null;
}
const disallowed = [
/* whatwg */
"alternate",
"canonical",
"author",
"bookmark",
"dns-prefetch",
"expect",
"icon",
"manifest",
"modulepreload",
"pingback",
"preconnect",
"prefetch",
"preload",
"privacy-policy",
"stylesheet",
"tag",
"terms-of-service"
];
const tokens = attr.toLowerCase().split(/\s+/);
for (const keyword of tokens) {
if (disallowed.includes(keyword)) {
return `<form> does not allow rel="${keyword}"`;
}
}
return null;
},
list: true,
enum: ["/.+/"]
},
target: {
enum: ["/[^_].*/", "_blank", "_self", "_parent", "_top"]
}
},
aria: {
implicitRole: "form"
},
permittedContent: ["@flow"],
permittedDescendants: [{ exclude: ["@form"] }]
},
frame: {
deprecated: {
documentation: "The `<frame>` element can be replaced with the `<iframe>` element but a better solution is to remove usage of frames entirely.",
source: "html5"
},
attributes: {
datafld: {
deprecated: true
},
datasrc: {
deprecated: true
},
title: {
required: true
}
}
},
frameset: {
deprecated: {
documentation: "The `<frameset>` element can be replaced with the `<iframe>` element but a better solution is to remove usage of frames entirely.",
source: "html5"
}
},
h1: {
flow: true,
heading: true,
permittedContent: ["@phrasing"],
attributes: {
align: {
deprecated: true
}
},
aria: {
implicitRole: "heading"
}
},
h2: {
flow: true,
heading: true,
permittedContent: ["@phrasing"],
attributes: {
align: {
deprecated: true
}
},
aria: {
implicitRole: "heading"
}
},
h3: {
flow: true,
heading: true,
permittedContent: ["@phrasing"],
attributes: {
align: {
deprecated: true
}
},
aria: {
implicitRole: "heading"
}
},
h4: {
flow: true,
heading: true,
permittedContent: ["@phrasing"],
attributes: {
align: {
deprecated: true
}
},
aria: {
implicitRole: "heading"
}
},
h5: {
flow: true,
heading: true,
permittedContent: ["@phrasing"],
attributes: {
align: {
deprecated: true
}
},
aria: {
implicitRole: "heading"
}
},
h6: {
flow: true,
heading: true,
permittedContent: ["@phrasing"],
attributes: {
align: {
deprecated: true
}
},
aria: {
implicitRole: "heading"
}
},
head: {
permittedContent: ["base?", "title?", "@meta"],
permittedParent: ["html"],
requiredContent: ["title"],
attributes: {
profile: {
deprecated: true
}
},
aria: {
naming: "prohibited"
}
},
header: {
flow: true,
aria: {
implicitRole(node) {
if (isInsideLandmark(node)) {
return "generic";
} else {
return "banner";
}
},
naming(node) {
if (isInsideLandmark(node)) {
return "prohibited";
} else {
return "allowed";
}
}
},
permittedContent: ["@flow"],
permittedDescendants: [{ exclude: ["header", "footer", "main"] }]
},
hgroup: {
flow: true,
heading: true,
permittedContent: ["p", "@heading?"],
permittedDescendants: [{ exclude: ["hgroup"] }],
requiredContent: ["@heading"],
aria: {
implicitRole: "group"
}
},
hr: {
flow: true,
void: true,
attributes: {
align: {
deprecated: true
},
color: {
deprecated: true
},
noshade: {
deprecated: true
},
size: {
deprecated: true
},
width: {
deprecated: true
}
},
aria: {
implicitRole: "separator"
}
},
html: {
permittedContent: ["head?", "body?"],
permittedOrder: ["head", "body"],
requiredContent: ["head", "body"],
attributes: {
lang: {
required: true
},
version: {
deprecated: true
}
},
aria: {
implicitRole: "document",
naming: "prohibited"
}
},
i: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing"],
aria: {
implicitRole: "generic",
naming: "prohibited"
}
},
iframe: {
flow: true,
phrasing: true,
embedded: true,
interactive: true,
attributes: {
align: {
deprecated: true
},
allowtransparency: {
deprecated: true
},
datafld: {
deprecated: true
},
datasrc: {
deprecated: true
},
frameborder: {
deprecated: true
},
height: {
enum: ["/\\d+/"]
},
hspace: {
deprecated: true
},
marginheight: {
deprecated: true
},
marginwidth: {
deprecated: true
},
referrerpolicy: {
enum: ReferrerPolicy
},
scrolling: {
deprecated: true
},
src: {
enum: ["/.+/"]
},
title: {
required: true
},
vspace: {
deprecated: true
},
width: {
enum: ["/\\d+/"]
}
},
permittedContent: []
},
img: {
flow: true,
phrasing: true,
embedded: true,
interactive(node) {
return node.hasAttribute("usemap");
},
void: true,
attributes: {
align: {
deprecated: true
},
border: {
deprecated: true
},
crossorigin: {
omit: true,
enum: ["anonymous", "use-credentials"]
},
datafld: {
deprecated: true
},
datasrc: {
deprecated: true
},
decoding: {
enum: ["sync", "async", "auto"]
},
height: {
enum: ["/\\d+/"]
},
hspace: {
deprecated: true
},
ismap: {
boolean: true
},
lowsrc: {
deprecated: true
},
name: {
deprecated: true
},
referrerpolicy: {
enum: ReferrerPolicy
},
src: {
required: true,
enum: ["/.+/"]
},
srcset: {
enum: ["/[^]+/"]
},
vspace: {
deprecated: true
},
width: {
enum: ["/\\d+/"]
}
},
aria: {
implicitRole(node) {
const alt = node.getAttribute("alt");
const ariaLabel = node.getAttribute("aria-label");
const ariaLabelledBy = node.getAttribute("aria-labelledby");
const title = node.getAttribute("title");
if (alt === "" && !ariaLabel && !ariaLabelledBy && !title) {
return "none";
} else {
return "img";
}
},
naming(node) {
const alt = node.getAttribute("alt");
const ariaLabel = node.getAttribute("aria-label");
const ariaLabelledBy = node.getAttribute("aria-labelledby");
const title = node.getAttribute("title");
if (!alt && !ariaLabel && !ariaLabelledBy && !title) {
return "prohibited";
} else {
return "allowed";
}
}
}
},
input: {
flow: true,
focusable(node) {
return node.getAttribute("type") !== "hidden";
},
phrasing: true,
interactive(node) {
return node.getAttribute("type") !== "hidden";
},
void: true,
formAssociated: {
disablable: true,
listed: true
},
labelable(node) {
return node.getAttribute("type") !== "hidden";
},
attributes: {
align: {
deprecated: true
},
autofocus: {
boolean: true
},
capture: {
omit: true,
enum: ["environment", "user"]
},
checked: {
boolean: true
},
datafld: {
deprecated: true
},
dataformatas: {
deprecated: true
},
datasrc: {
deprecated: true
},
disabled: {
boolean: true
},
formaction: {
allowed: allowedIfAttributeHasValue("type", ["submit", "image"], {
defaultValue: "submit"
})
},
formenctype: {
allowed: allowedIfAttributeHasValue("type", ["submit", "image"], {
defaultValue: "submit"
})
},
formmethod: {
allowed: allowedIfAttributeHasValue("type", ["submit", "image"], {
defaultValue: "submit"
}),
enum: ["get", "post", "dialog"]
},
formnovalidate: {
allowed: allowedIfAttributeHasValue("type", ["submit", "image"], {
defaultValue: "submit"
}),
boolean: true
},
formtarget: {
allowed: allowedIfAttributeHasValue("type", ["submit", "image"], {
defaultValue: "submit"
}),
enum: ["/[^_].*/", "_blank", "_self", "_parent", "_top"]
},
hspace: {
deprecated: true
},
inputmode: {
enum: ["none", "text", "decimal", "numeric", "tel", "search", "email", "url"]
},
ismap: {
deprecated: true
},
multiple: {
boolean: true
},
readonly: {
boolean: true
},
required: {
boolean: true
},
type: {
enum: [
"button",
"checkbox",
"color",
"date",
"datetime-local",
"email",
"file",
"hidden",
"image",
"month",
"number",
"password",
"radio",
"range",
"reset",
"search",
"submit",
"tel",
"text",
"time",
"url",
"week"
]
},
usemap: {
deprecated: true
},
vspace: {
deprecated: true
}
},
aria: {
/* eslint-disable-next-line complexity -- the standard is complicated */
implicitRole(node) {
const list = node.hasAttribute("list");
if (list) {
return "combobox";
}
const type = node.getAttribute("type");
switch (type) {
case "button":
return "button";
case "checkbox":
return "checkbox";
case "color":
return null;
case "date":
return null;
case "datetime-local":
return null;
case "email":
return "textbox";
case "file":
return null;
case "hidden":
return null;
case "image":
return "button";
case "month":
return null;
case "number":
return "spinbutton";
case "password":
return null;
case "radio":
return "radio";
case "range":
return "slider";
case "reset":
return "button";
case "search":
return "searchbox";
case "submit":
return "button";
case "tel":
return "textbox";
case "text":
return "textbox";
case "time":
return null;
case "url":
return "textbox";
case "week":
return null;
default:
return "textbox";
}
},
naming(node) {
return node.getAttribute("type") !== "hidden" ? "allowed" : "prohibited";
}
}
},
ins: {
flow: true,
phrasing: true,
transparent: true,
aria: {
implicitRole: "insertion",
naming: "prohibited"
}
},
isindex: {
deprecated: {
source: "html4"
}
},
kbd: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing"],
aria: {
naming: "prohibited"
}
},
keygen: {
flow: true,
phrasing: true,
interactive: true,
void: true,
labelable: true,
deprecated: true
},
label: {
flow: true,
phrasing: true,
interactive: true,
permittedContent: ["@phrasing"],
permittedDescendants: [{ exclude: ["label"] }],
attributes: {
datafld: {
deprecated: true
},
dataformatas: {
deprecated: true
},
datasrc: {
deprecated: true
},
for: {
enum: [validId]
}
},
aria: {
naming: "prohibited"
}
},
legend: {
permittedContent: ["@phrasing", "@heading"],
attributes: {
align: {
deprecated: true
},
datafld: {
deprecated: true
},
dataformatas: {
deprecated: true
},
datasrc: {
deprecated: true
}
},
aria: {
naming: "prohibited"
}
},
li: {
implicitClosed: ["li"],
permittedContent: ["@flow"],
permittedParent: ["ul", "ol", "menu", "template"],
attributes: {
type: {
deprecated: true
}
},
aria: {
implicitRole(node) {
return node.closest("ul, ol, menu") ? "listitem" : "generic";
}
}
},
link: {
metadata: true,
flow(node) {
return linkBodyOk(node);
},
phrasing(node) {
return linkBodyOk(node);
},
void: true,
attributes: {
as: {
allowed: allowedIfAttributeHasValue("rel", ["prefetch", "preload", "modulepreload"]),
enum: [
"audio",
"audioworklet",
"document",
"embed",
"fetch",
"font",
"frame",
"iframe",
"image",
"manifest",
"object",
"paintworklet",
"report",
"script",
"serviceworker",
"sharedworker",
"style",
"track",
"video",
"webidentity",
"worker",
"xslt"
]
},
blocking: {
allowed: allowedIfAttributeHasValue("rel", ["stylesheet", "preload", "modulepreload"]),
list: true,
enum: ["render"]
},
charset: {
deprecated: true
},
crossorigin: {
omit: true,
enum: ["anonymous", "use-credentials"]
},
disabled: {
allowed: allowedIfAttributeHasValue("rel", ["stylesheet"]),
boolean: true
},
href: {
required: true,
enum: ["/.+/"]
},
integrity: {
allowed: allowedIfAttributeHasValue("rel", ["stylesheet", "preload", "modulepreload"]),
enum: ["/.+/"]
},
methods: {
deprecated: true
},
referrerpolicy: {
enum: ReferrerPolicy
},
rel: {
allowed(_, attr) {
if (!attr || attr === "" || typeof attr !== "string") {
return null;
}
const disallowed = [
/* whatwg */
"bookmark",
"external",
"nofollow",
"noopener",
"noreferrer",
"opener",
"tag",
/* microformats.org */
"disclosure",
"entry-content",
"lightbox",
"lightvideo"
];
const tokens = attr.toLowerCase().split(/\s+/);
for (const keyword of tokens) {
if (disallowed.includes(keyword)) {
return `<link> does not allow rel="${keyword}"`;
}
}
return null;
},
list: true,
enum: ["/.+/"]
},
target: {
deprecated: true
},
urn: {
deprecated: true
}
},
aria: {
naming: "prohibited"
}
},
listing: {
deprecated: {
source: "html32"
}
},
main: {
flow: true,
aria: {
implicitRole: "main"
}
},
map: {
flow: true,
phrasing: true,
transparent: true,
attributes: {
name: {
required: true,
enum: ["/\\S+/"]
}
},
aria: {
naming: "prohibited"
}
},
mark: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing"],
aria: {
naming: "prohibited"
}
},
marquee: {
deprecated: {
documentation: "Marked as obsolete by both W3C and WHATWG standards but still implemented in most browsers. Animated text should be avoided for accessibility reasons as well.",
source: "html5"
},
attributes: {
datafld: {
deprecated: true
},
dataformatas: {
deprecated: true
},
datasrc: {
deprecated: true
}
}
},
math: {
flow: true,
foreign: true,
phrasing: true,
embedded: true,
attributes: {
align: {
deprecated: true
},
dir: {
enum: ["ltr", "rtl"]
},
display: {
enum: ["block", "inline"]
},
hspace: {
deprecated: true
},
name: {
deprecated: true
},
overflow: {
enum: ["linebreak", "scroll", "elide", "truncate", "scale"]
},
vspace: {
deprecated: true
}
},
aria: {
implicitRole: "math"
}
},
menu: {
flow: true,
aria: {
implicitRole: "list"
},
permittedContent: ["@script", "li"]
},
meta: {
flow(node) {
return node.hasAttribute("itemprop");
},
phrasing(node) {
return node.hasAttribute("itemprop");
},
metadata: true,
void: true,
attributes: {
charset: {
enum: ["utf-8"]
},
content: {
allowed: allowedIfAttributeIsPresent("name", "http-equiv", "itemprop", "property")
},
itemprop: {
allowed: allowedIfAttributeIsAbsent("http-equiv", "name")
},
name: {
allowed: allowedIfAttributeIsAbsent("http-equiv", "itemprop")
},
"http-equiv": {
allowed: allowedIfAttributeIsAbsent("name", "itemprop")
},
scheme: {
deprecated: true
}
},
aria: {
naming: "prohibited"
}
},
meter: {
flow: true,
phrasing: true,
labelable: true,
aria: {
implicitRole: "meter"
},
permittedContent: ["@phrasing"],
permittedDescendants: [{ exclude: "meter" }]
},
multicol: {
deprecated: {
message: "use CSS instead",
documentation: "Use CSS columns instead.",
source: "html5"
}
},
nav: {
flow: true,
sectioning: true,
aria: {
implicitRole: "navigation"
},
permittedContent: ["@flow"],
permittedDescendants: [{ exclude: "main" }]
},
nextid: {
deprecated: {
source: "html32"
}
},
nobr: {
deprecated: {
message: "use CSS instead",
documentation: "Use CSS `white-space` property instead.",
source: "non-standard"
}
},
noembed: {
deprecated: {
source: "non-standard"
}
},
noframes: {
deprecated: {
source: "html5"
}
},
noscript: {
metadata: true,
flow: true,
phrasing: true,
transparent: true,
permittedDescendants: [{ exclude: "noscript" }],
aria: {
naming: "prohibited"
}
},
object: {
flow: true,
phrasing: true,
embedded: true,
interactive(node) {
return node.hasAttribute("usemap");
},
transparent: true,
formAssociated: {
disablable: false,
listed: true
},
attributes: {
align: {
deprecated: true
},
archive: {
deprecated: true
},
blocking: {
list: true,
enum: ["render"]
},
border: {
deprecated: true
},
classid: {
deprecated: true
},
code: {
deprecated: true
},
codebase: {
deprecated: true
},
codetype: {
deprecated: true
},
data: {
enum: ["/.+/"],
required: true
},
datafld: {
deprecated: true
},
dataformatas: {
deprecated: true
},
datasrc: {
deprecated: true
},
declare: {
deprecated: true
},
height: {
enum: ["/\\d+/"]
},
hspace: {
deprecated: true
},
name: {
enum: ["/[^_].*/"]
},
standby: {
deprecated: true
},
vspace: {
deprecated: true
},
width: {
enum: ["/\\d+/"]
}
},
permittedContent: ["param", "@flow"],
permittedOrder: ["param", "@flow"]
},
ol: {
flow: true,
attributes: {
compact: {
deprecated: true
},
reversed: {
boolean: true
},
type: {
enum: ["a", "A", "i", "I", "1"]
}
},
aria: {
implicitRole: "list"
},
permittedContent: ["@script", "li"]
},
optgroup: {
implicitClosed: ["optgroup"],
attributes: {
disabled: {
boolean: true
}
},
aria: {
implicitRole: "group"
},
permittedContent: ["@script", "option"]
},
option: {
implicitClosed: ["option"],
attributes: {
dataformatas: {
deprecated: true
},
datasrc: {
deprecated: true
},
disabled: {
boolean: true
},
name: {
deprecated: true
},
selected: {
boolean: true
}
},
aria: {
implicitRole: "option"
},
permittedContent: []
},
output: {
flow: true,
phrasing: true,
formAssociated: {
disablable: false,
listed: true
},
labelable: true,
aria: {
implicitRole: "status"
},
permittedContent: ["@phrasing"]
},
p: {
flow: true,
implicitClosed: [
"address",
"article",
"aside",
"blockquote",
"div",
"dl",
"fieldset",
"footer",
"form",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"header",
"hgroup",
"hr",
"main",
"nav",
"ol",
"p",
"pre",
"section",
"table",
"ul"
],
permittedContent: ["@phrasing"],
attributes: {
align: {
deprecated: true
}
},
aria: {
implicitRole: "paragraph",
naming: "prohibited"
}
},
param: {
void: true,
attributes: {
datafld: {
deprecated: true
},
type: {
deprecated: true
},
valuetype: {
deprecated: true
}
},
aria: {
naming: "prohibited"
}
},
picture: {
flow: true,
phrasing: true,
embedded: true,
permittedContent: ["@script", "source", "img"],
permittedOrder: ["source", "img"],
aria: {
naming: "prohibited"
}
},
plaintext: {
deprecated: {
message: "use <pre> or CSS instead",
documentation: "Use the `<pre>` element or use CSS to set a monospace font.",
source: "html2"
}
},
pre: {
flow: true,
permittedContent: ["@phrasing"],
attributes: {
width: {
deprecated: true
}
},
aria: {
implicitRole: "generic",
naming: "prohibited"
}
},
progress: {
flow: true,
phrasing: true,
labelable: true,
aria: {
implicitRole: "progressbar"
},
permittedContent: ["@phrasing"],
permittedDescendants: [{ exclude: "progress" }]
},
q: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing"],
aria: {
implicitRole: "generic",
naming: "prohibited"
}
},
rb: {
implicitClosed: ["rb", "rt", "rtc", "rp"],
permittedContent: ["@phrasing"]
},
rp: {
implicitClosed: ["rb", "rt", "rtc", "rp"],
permittedContent: ["@phrasing"],
aria: {
naming: "prohibited"
}
},
rt: {
implicitClosed: ["rb", "rt", "rtc", "rp"],
permittedContent: ["@phrasing"],
aria: {
naming: "prohibited"
}
},
rtc: {
implicitClosed: ["rb", "rtc", "rp"],
permittedContent: ["@phrasing", "rt"]
},
ruby: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing", "rb", "rp", "rt", "rtc"]
},
s: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing"],
aria: {
implicitRole: "deletion",
naming: "prohibited"
}
},
samp: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing"],
aria: {
implicitRole: "generic",
naming: "prohibited"
}
},
script: {
metadata: true,
flow: true,
phrasing: true,
scriptSupporting: true,
attributes: {
async: {
boolean: true
},
crossorigin: {
omit: true,
enum: ["anonymous", "use-credentials"]
},
defer: {
boolean: true
},
event: {
deprecated: true
},
for: {
deprecated: true
},
integrity: {
allowed: allowedIfAttributeIsPresent("src"),
enum: ["/.+/"]
},
language: {
deprecated: true
},
nomodule: {
boolean: true
},
referrerpolicy: {
enum: ReferrerPolicy
},
src: {
enum: ["/.+/"]
}
},
aria: {
naming: "prohibited"
}
},
search: {
flow: true,
aria: {
implicitRole: "search"
}
},
section: {
flow: true,
sectioning: true,
aria: {
implicitRole(node) {
const name = node.hasAttribute("aria-label") || node.hasAttribute("aria-labelledby");
return name ? "region" : "generic";
}
},
permittedContent: ["@flow"]
},
select: {
flow: true,
focusable: true,
phrasing: true,
interactive: true,
formAssociated: {
disablable: true,
listed: true
},
labelable: true,
attributes: {
autofocus: {
boolean: true
},
disabled: {
boolean: true
},
multiple: {
boolean: true
},
required: {
boolean: true
},
size: {
enum: ["/\\d+/"]
}
},
aria: {
implicitRole(node) {
const multiple = node.hasAttribute("multiple");
if (multiple) {
return "listbox";
}
const size = node.getAttribute("size");
if (typeof size === "string") {
const parsed = parseInt(size, 10);
if (parsed > 1) {
return "listbox";
}
}
return "combobox";
}
},
permittedContent: ["@script", "datasrc", "datafld", "dataformatas", "option", "optgroup"]
},
slot: {
flow: true,
phrasing: true,
transparent: true,
aria: {
naming: "prohibited"
}
},
small: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing"],
aria: {
implicitRole: "generic",
naming: "prohibited"
}
},
source: {
void: true,
attributes: {
type: {},
media: {},
src: {
allowed: allowedIfParentIsPresent("audio", "video")
},
srcset: {
allowed: allowedIfParentIsPresent("picture")
},
sizes: {
allowed: allowedIfParentIsPresent("picture")
},
width: {
allowed: allowedIfParentIsPresent("picture"),
enum: ["/\\d+/"]
},
height: {
allowed: allowedIfParentIsPresent("picture"),
enum: ["/\\d+/"]
}
},
aria: {
naming: "prohibited"
}
},
spacer: {
deprecated: {
message: "use CSS instead",
documentation: "Use CSS margin or padding instead.",
source: "non-standard"
}
},
span: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing"],
attributes: {
datafld: {
deprecated: true
},
dataformatas: {
deprecated: true
},
datasrc: {
deprecated: true
}
},
aria: {
implicitRole: "generic",
naming: "prohibited"
}
},
strike: {
deprecated: {
message: "use <del> or <s> instead",
documentation: "Use the `<del>` or `<s>` element instead.",
source: "html5"
}
},
strong: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing"],
aria: {
implicitRole: "strong",
naming: "prohibited"
}
},
style: {
metadata: true,
aria: {
naming: "prohibited"
}
},
sub: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing"],
aria: {
implicitRole: "subscript",
naming: "prohibited"
}
},
summary: {
permittedContent: ["@phrasing", "@heading"],
focusable(node) {
return Boolean(node.closest("details"));
},
aria: {
implicitRole: "button"
}
},
sup: {
flow: true,
phrasing: true,
permittedContent: ["@phrasing"],
aria: {
imp