html-validate 8.8.0 → 8.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/cli.js +36 -21
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/core.js +345 -71
- package/dist/cjs/core.js.map +1 -1
- package/dist/cjs/elements.js +18 -0
- package/dist/cjs/elements.js.map +1 -1
- package/dist/cjs/tsdoc-metadata.json +1 -1
- package/dist/es/cli.js +37 -22
- package/dist/es/cli.js.map +1 -1
- package/dist/es/core.js +345 -71
- package/dist/es/core.js.map +1 -1
- package/dist/es/elements.js +18 -0
- package/dist/es/elements.js.map +1 -1
- package/dist/schema/elements.json +6 -0
- package/dist/tsdoc-metadata.json +1 -1
- package/dist/types/browser.d.ts +24 -2
- package/dist/types/index.d.ts +24 -2
- package/package.json +12 -12
package/dist/es/core.js
CHANGED
|
@@ -591,6 +591,18 @@ const patternProperties = {
|
|
|
591
591
|
description: "Script-supporting elements are elements which can be inserted where othersise not permitted to assist in templating",
|
|
592
592
|
type: "boolean"
|
|
593
593
|
},
|
|
594
|
+
focusable: {
|
|
595
|
+
title: "Mark this element as focusable",
|
|
596
|
+
description: "This element may contain an associated label element.",
|
|
597
|
+
anyOf: [
|
|
598
|
+
{
|
|
599
|
+
type: "boolean"
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
"function": true
|
|
603
|
+
}
|
|
604
|
+
]
|
|
605
|
+
},
|
|
594
606
|
form: {
|
|
595
607
|
title: "Mark element as a submittable form element",
|
|
596
608
|
type: "boolean"
|
|
@@ -965,6 +977,7 @@ const MetaCopyableProperty = [
|
|
|
965
977
|
"embedded",
|
|
966
978
|
"interactive",
|
|
967
979
|
"transparent",
|
|
980
|
+
"focusable",
|
|
968
981
|
"form",
|
|
969
982
|
"formAssociated",
|
|
970
983
|
"labelable",
|
|
@@ -1037,6 +1050,7 @@ function migrateElement(src) {
|
|
|
1037
1050
|
},
|
|
1038
1051
|
attributes: migrateAttributes(src),
|
|
1039
1052
|
textContent: src.textContent,
|
|
1053
|
+
focusable: src.focusable ?? false,
|
|
1040
1054
|
implicitRole: src.implicitRole ?? (() => null)
|
|
1041
1055
|
};
|
|
1042
1056
|
delete result.deprecatedAttributes;
|
|
@@ -1292,6 +1306,9 @@ function expandProperties(node, entry) {
|
|
|
1292
1306
|
setMetaProperty(entry, key, evaluateProperty(node, property));
|
|
1293
1307
|
}
|
|
1294
1308
|
}
|
|
1309
|
+
if (typeof entry.focusable === "function") {
|
|
1310
|
+
setMetaProperty(entry, "focusable", entry.focusable(node._adapter));
|
|
1311
|
+
}
|
|
1295
1312
|
}
|
|
1296
1313
|
function expandRegexValue(value) {
|
|
1297
1314
|
if (value instanceof RegExp) {
|
|
@@ -1506,10 +1523,10 @@ class Context {
|
|
|
1506
1523
|
constructor(source) {
|
|
1507
1524
|
this.state = State.INITIAL;
|
|
1508
1525
|
this.string = source.data;
|
|
1509
|
-
this.filename = source.filename
|
|
1510
|
-
this.offset = source.offset
|
|
1511
|
-
this.line = source.line
|
|
1512
|
-
this.column = source.column
|
|
1526
|
+
this.filename = source.filename;
|
|
1527
|
+
this.offset = source.offset;
|
|
1528
|
+
this.line = source.line;
|
|
1529
|
+
this.column = source.column;
|
|
1513
1530
|
this.contentModel = 1 /* TEXT */;
|
|
1514
1531
|
}
|
|
1515
1532
|
getTruncatedLine(n = 13) {
|
|
@@ -1542,6 +1559,16 @@ class Context {
|
|
|
1542
1559
|
}
|
|
1543
1560
|
}
|
|
1544
1561
|
|
|
1562
|
+
function normalizeSource(source) {
|
|
1563
|
+
return {
|
|
1564
|
+
filename: "",
|
|
1565
|
+
offset: 0,
|
|
1566
|
+
line: 1,
|
|
1567
|
+
column: 1,
|
|
1568
|
+
...source
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1545
1572
|
var NodeType = /* @__PURE__ */ ((NodeType2) => {
|
|
1546
1573
|
NodeType2[NodeType2["ELEMENT_NODE"] = 1] = "ELEMENT_NODE";
|
|
1547
1574
|
NodeType2[NodeType2["TEXT_NODE"] = 3] = "TEXT_NODE";
|
|
@@ -2122,6 +2149,7 @@ class TextNode extends DOMNode {
|
|
|
2122
2149
|
}
|
|
2123
2150
|
}
|
|
2124
2151
|
|
|
2152
|
+
const ROLE = Symbol("role");
|
|
2125
2153
|
var NodeClosed = /* @__PURE__ */ ((NodeClosed2) => {
|
|
2126
2154
|
NodeClosed2[NodeClosed2["Open"] = 0] = "Open";
|
|
2127
2155
|
NodeClosed2[NodeClosed2["EndTag"] = 1] = "EndTag";
|
|
@@ -2357,6 +2385,27 @@ class HtmlElement extends DOMNode {
|
|
|
2357
2385
|
get meta() {
|
|
2358
2386
|
return this.metaElement;
|
|
2359
2387
|
}
|
|
2388
|
+
/**
|
|
2389
|
+
* Get current role for this element (explicit with `role` attribute or mapped
|
|
2390
|
+
* with implicit role).
|
|
2391
|
+
*
|
|
2392
|
+
* @since 8.9.1
|
|
2393
|
+
*/
|
|
2394
|
+
get role() {
|
|
2395
|
+
const cached = this.cacheGet(ROLE);
|
|
2396
|
+
if (cached !== void 0) {
|
|
2397
|
+
return cached;
|
|
2398
|
+
}
|
|
2399
|
+
const role = this.getAttribute("role");
|
|
2400
|
+
if (role) {
|
|
2401
|
+
return this.cacheSet(ROLE, role.value);
|
|
2402
|
+
}
|
|
2403
|
+
if (this.metaElement) {
|
|
2404
|
+
const implicitRole = this.metaElement.implicitRole(this._adapter);
|
|
2405
|
+
return this.cacheSet(ROLE, implicitRole);
|
|
2406
|
+
}
|
|
2407
|
+
return this.cacheSet(ROLE, null);
|
|
2408
|
+
}
|
|
2360
2409
|
/**
|
|
2361
2410
|
* Set annotation for this element.
|
|
2362
2411
|
*/
|
|
@@ -3135,8 +3184,21 @@ function isKeywordIgnored(options, keyword, matcher = (list, it) => list.include
|
|
|
3135
3184
|
const ARIA_HIDDEN_CACHE = Symbol(isAriaHidden.name);
|
|
3136
3185
|
const HTML_HIDDEN_CACHE = Symbol(isHTMLHidden.name);
|
|
3137
3186
|
const ROLE_PRESENTATION_CACHE = Symbol(isPresentation.name);
|
|
3187
|
+
const STYLE_HIDDEN_CACHE = Symbol(isStyleHidden.name);
|
|
3138
3188
|
function inAccessibilityTree(node) {
|
|
3139
|
-
|
|
3189
|
+
if (isAriaHidden(node)) {
|
|
3190
|
+
return false;
|
|
3191
|
+
}
|
|
3192
|
+
if (isPresentation(node)) {
|
|
3193
|
+
return false;
|
|
3194
|
+
}
|
|
3195
|
+
if (isHTMLHidden(node)) {
|
|
3196
|
+
return false;
|
|
3197
|
+
}
|
|
3198
|
+
if (isStyleHidden(node)) {
|
|
3199
|
+
return false;
|
|
3200
|
+
}
|
|
3201
|
+
return true;
|
|
3140
3202
|
}
|
|
3141
3203
|
function isAriaHiddenImpl(node) {
|
|
3142
3204
|
const isHidden = (node2) => {
|
|
@@ -3174,22 +3236,41 @@ function isHTMLHidden(node, details) {
|
|
|
3174
3236
|
const result = node.cacheSet(HTML_HIDDEN_CACHE, isHTMLHiddenImpl(node));
|
|
3175
3237
|
return details ? result : result.byParent || result.bySelf;
|
|
3176
3238
|
}
|
|
3239
|
+
function isStyleHiddenImpl(node) {
|
|
3240
|
+
const isHidden = (node2) => {
|
|
3241
|
+
const style = node2.getAttribute("style");
|
|
3242
|
+
const { display, visibility } = parseCssDeclaration(style == null ? void 0 : style.value);
|
|
3243
|
+
return display === "none" || visibility === "hidden";
|
|
3244
|
+
};
|
|
3245
|
+
const byParent = node.parent ? isStyleHidden(node.parent) : false;
|
|
3246
|
+
const bySelf = isHidden(node);
|
|
3247
|
+
return byParent || bySelf;
|
|
3248
|
+
}
|
|
3249
|
+
function isStyleHidden(node) {
|
|
3250
|
+
const cached = node.cacheGet(STYLE_HIDDEN_CACHE);
|
|
3251
|
+
if (cached) {
|
|
3252
|
+
return cached;
|
|
3253
|
+
}
|
|
3254
|
+
return node.cacheSet(STYLE_HIDDEN_CACHE, isStyleHiddenImpl(node));
|
|
3255
|
+
}
|
|
3177
3256
|
function isPresentation(node) {
|
|
3178
3257
|
if (node.cacheExists(ROLE_PRESENTATION_CACHE)) {
|
|
3179
3258
|
return Boolean(node.cacheGet(ROLE_PRESENTATION_CACHE));
|
|
3180
3259
|
}
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3260
|
+
const meta = node.meta;
|
|
3261
|
+
if (meta && meta.interactive) {
|
|
3262
|
+
return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
|
|
3263
|
+
}
|
|
3264
|
+
const tabindex = node.getAttribute("tabindex");
|
|
3265
|
+
if (tabindex) {
|
|
3266
|
+
return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
|
|
3267
|
+
}
|
|
3268
|
+
const role = node.getAttribute("role");
|
|
3269
|
+
if (role && (role.value === "presentation" || role.value === "none")) {
|
|
3270
|
+
return node.cacheSet(ROLE_PRESENTATION_CACHE, true);
|
|
3271
|
+
} else {
|
|
3272
|
+
return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
|
|
3273
|
+
}
|
|
3193
3274
|
}
|
|
3194
3275
|
|
|
3195
3276
|
const cachePrefix = classifyNodeText.name;
|
|
@@ -6177,6 +6258,58 @@ class HeadingLevel extends Rule {
|
|
|
6177
6258
|
}
|
|
6178
6259
|
}
|
|
6179
6260
|
|
|
6261
|
+
class HiddenFocusable extends Rule {
|
|
6262
|
+
documentation(context) {
|
|
6263
|
+
const byParent = context === "parent" ? " In this case it is being hidden by an ancestor with `aria-hidden.`" : "";
|
|
6264
|
+
return {
|
|
6265
|
+
description: [
|
|
6266
|
+
`\`aria-hidden\` cannot be used on focusable elements.${byParent}`,
|
|
6267
|
+
"",
|
|
6268
|
+
"When focusable elements are hidden with `aria-hidden` they are still reachable using conventional means such as a mouse or keyboard but won't be exposed to assistive technology (AT).",
|
|
6269
|
+
"This is often confusing for users of AT such as screenreaders.",
|
|
6270
|
+
"",
|
|
6271
|
+
"To fix this either:",
|
|
6272
|
+
" - Remove `aria-hidden`.",
|
|
6273
|
+
" - Remove the element from the DOM instead.",
|
|
6274
|
+
" - Use the `hidden` attribute or similar means to hide the element."
|
|
6275
|
+
].join("\n"),
|
|
6276
|
+
url: "https://html-validate.org/rules/hidden-focusable.html"
|
|
6277
|
+
};
|
|
6278
|
+
}
|
|
6279
|
+
setup() {
|
|
6280
|
+
const focusable = this.getTagsWithProperty("focusable");
|
|
6281
|
+
const selector = ["[tabindex]", ...focusable].join(",");
|
|
6282
|
+
this.on("dom:ready", (event) => {
|
|
6283
|
+
const { document } = event;
|
|
6284
|
+
for (const element of document.querySelectorAll(selector)) {
|
|
6285
|
+
if (isHTMLHidden(element) || isStyleHidden(element)) {
|
|
6286
|
+
continue;
|
|
6287
|
+
}
|
|
6288
|
+
if (isAriaHidden(element)) {
|
|
6289
|
+
this.validateElement(element);
|
|
6290
|
+
}
|
|
6291
|
+
}
|
|
6292
|
+
});
|
|
6293
|
+
}
|
|
6294
|
+
validateElement(element) {
|
|
6295
|
+
const { meta } = element;
|
|
6296
|
+
const tabindex = element.getAttribute("tabindex");
|
|
6297
|
+
if (meta && !meta.focusable && !tabindex) {
|
|
6298
|
+
return;
|
|
6299
|
+
}
|
|
6300
|
+
const attribute = element.getAttribute("aria-hidden");
|
|
6301
|
+
const message = attribute ? `aria-hidden cannot be used on focusable elements` : `aria-hidden cannot be used on focusable elements (hidden by ancestor element)`;
|
|
6302
|
+
const location = attribute ? attribute.keyLocation : element.location;
|
|
6303
|
+
const context = attribute ? "self" : "parent";
|
|
6304
|
+
this.report({
|
|
6305
|
+
node: element,
|
|
6306
|
+
message,
|
|
6307
|
+
location,
|
|
6308
|
+
context
|
|
6309
|
+
});
|
|
6310
|
+
}
|
|
6311
|
+
}
|
|
6312
|
+
|
|
6180
6313
|
const defaults$g = {
|
|
6181
6314
|
pattern: "kebabcase"
|
|
6182
6315
|
};
|
|
@@ -6362,7 +6495,7 @@ function isHidden(node, context) {
|
|
|
6362
6495
|
if (reference && reference.isSameNode(node)) {
|
|
6363
6496
|
return false;
|
|
6364
6497
|
} else {
|
|
6365
|
-
return
|
|
6498
|
+
return !inAccessibilityTree(node);
|
|
6366
6499
|
}
|
|
6367
6500
|
}
|
|
6368
6501
|
function hasImgAltText(node, context) {
|
|
@@ -7312,7 +7445,7 @@ class NoRawCharacters extends Rule {
|
|
|
7312
7445
|
}
|
|
7313
7446
|
}
|
|
7314
7447
|
|
|
7315
|
-
const selectors = ["input[aria-label]", "textarea[aria-label]", "select[aria-label]"];
|
|
7448
|
+
const selectors$1 = ["input[aria-label]", "textarea[aria-label]", "select[aria-label]"];
|
|
7316
7449
|
class NoRedundantAriaLabel extends Rule {
|
|
7317
7450
|
documentation() {
|
|
7318
7451
|
return {
|
|
@@ -7323,7 +7456,7 @@ class NoRedundantAriaLabel extends Rule {
|
|
|
7323
7456
|
setup() {
|
|
7324
7457
|
this.on("dom:ready", (event) => {
|
|
7325
7458
|
const { document } = event;
|
|
7326
|
-
const elements = document.querySelectorAll(selectors.join(","));
|
|
7459
|
+
const elements = document.querySelectorAll(selectors$1.join(","));
|
|
7327
7460
|
for (const element of elements) {
|
|
7328
7461
|
const ariaLabel = element.getAttribute("aria-label");
|
|
7329
7462
|
const id = element.id;
|
|
@@ -7939,7 +8072,7 @@ class RequireSri extends Rule {
|
|
|
7939
8072
|
}
|
|
7940
8073
|
documentation() {
|
|
7941
8074
|
return {
|
|
7942
|
-
description: `Subresource Integrity (SRI) \`integrity\` attribute is required to prevent manipulation from Content Delivery Networks
|
|
8075
|
+
description: `Subresource Integrity (SRI) \`integrity\` attribute is required to prevent tampering or manipulation from Content Delivery Networks (CDN), rouge proxies, malicious entities, etc.`,
|
|
7943
8076
|
url: "https://html-validate.org/rules/require-sri.html"
|
|
7944
8077
|
};
|
|
7945
8078
|
}
|
|
@@ -8352,6 +8485,130 @@ class TextContent extends Rule {
|
|
|
8352
8485
|
}
|
|
8353
8486
|
}
|
|
8354
8487
|
|
|
8488
|
+
const roles = ["complementary", "contentinfo", "form", "banner", "main", "navigation", "region"];
|
|
8489
|
+
const selectors = [
|
|
8490
|
+
"aside",
|
|
8491
|
+
"footer",
|
|
8492
|
+
"form",
|
|
8493
|
+
"header",
|
|
8494
|
+
"main",
|
|
8495
|
+
"nav",
|
|
8496
|
+
"section",
|
|
8497
|
+
...roles.map((it) => `[role="${it}"]`)
|
|
8498
|
+
/* <search> does not (yet?) require a unique name */
|
|
8499
|
+
];
|
|
8500
|
+
function getTextFromReference(document, id) {
|
|
8501
|
+
if (!id || id instanceof DynamicValue) {
|
|
8502
|
+
return id;
|
|
8503
|
+
}
|
|
8504
|
+
const selector = `#${id}`;
|
|
8505
|
+
const ref = document.querySelector(selector);
|
|
8506
|
+
if (ref) {
|
|
8507
|
+
return ref.textContent;
|
|
8508
|
+
} else {
|
|
8509
|
+
return selector;
|
|
8510
|
+
}
|
|
8511
|
+
}
|
|
8512
|
+
function groupBy(values, callback) {
|
|
8513
|
+
const result = {};
|
|
8514
|
+
for (const value of values) {
|
|
8515
|
+
const key = callback(value);
|
|
8516
|
+
if (key in result) {
|
|
8517
|
+
result[key].push(value);
|
|
8518
|
+
} else {
|
|
8519
|
+
result[key] = [value];
|
|
8520
|
+
}
|
|
8521
|
+
}
|
|
8522
|
+
return result;
|
|
8523
|
+
}
|
|
8524
|
+
function getTextEntryFromElement(document, node) {
|
|
8525
|
+
const ariaLabel = node.getAttribute("aria-label");
|
|
8526
|
+
if (ariaLabel) {
|
|
8527
|
+
return {
|
|
8528
|
+
node,
|
|
8529
|
+
text: ariaLabel.value,
|
|
8530
|
+
location: ariaLabel.keyLocation
|
|
8531
|
+
};
|
|
8532
|
+
}
|
|
8533
|
+
const ariaLabelledby = node.getAttribute("aria-labelledby");
|
|
8534
|
+
if (ariaLabelledby) {
|
|
8535
|
+
const text = getTextFromReference(document, ariaLabelledby.value);
|
|
8536
|
+
return {
|
|
8537
|
+
node,
|
|
8538
|
+
text,
|
|
8539
|
+
location: ariaLabelledby.keyLocation
|
|
8540
|
+
};
|
|
8541
|
+
}
|
|
8542
|
+
return {
|
|
8543
|
+
node,
|
|
8544
|
+
text: null,
|
|
8545
|
+
location: node.location
|
|
8546
|
+
};
|
|
8547
|
+
}
|
|
8548
|
+
function isExcluded(entry) {
|
|
8549
|
+
const { node, text } = entry;
|
|
8550
|
+
if (text === null) {
|
|
8551
|
+
return !(node.is("form") || node.is("section"));
|
|
8552
|
+
}
|
|
8553
|
+
return true;
|
|
8554
|
+
}
|
|
8555
|
+
class UniqueLandmark extends Rule {
|
|
8556
|
+
documentation() {
|
|
8557
|
+
return {
|
|
8558
|
+
description: [
|
|
8559
|
+
"When the same type of landmark is present more than once in the same document each must be uniquely identifiable with a non-empty and unique name.",
|
|
8560
|
+
"For instance, if the document has two `<nav>` elements each of them need an accessible name to be distinguished from each other.",
|
|
8561
|
+
"",
|
|
8562
|
+
"The following elements / roles are considered landmarks:",
|
|
8563
|
+
"",
|
|
8564
|
+
' - `aside` or `[role="complementary"]`',
|
|
8565
|
+
' - `footer` or `[role="contentinfo"]`',
|
|
8566
|
+
' - `form` or `[role="form"]`',
|
|
8567
|
+
' - `header` or `[role="banner"]`',
|
|
8568
|
+
' - `main` or `[role="main"]`',
|
|
8569
|
+
' - `nav` or `[role="navigation"]`',
|
|
8570
|
+
' - `section` or `[role="region"]`',
|
|
8571
|
+
"",
|
|
8572
|
+
"To fix this either:",
|
|
8573
|
+
"",
|
|
8574
|
+
" - Add `aria-label`.",
|
|
8575
|
+
" - Add `aria-labelledby`.",
|
|
8576
|
+
" - Remove one of the landmarks."
|
|
8577
|
+
].join("\n"),
|
|
8578
|
+
url: "https://html-validate.org/rules/unique-landmark.html"
|
|
8579
|
+
};
|
|
8580
|
+
}
|
|
8581
|
+
setup() {
|
|
8582
|
+
this.on("dom:ready", (event) => {
|
|
8583
|
+
const { document } = event;
|
|
8584
|
+
const elements = document.querySelectorAll(selectors.join(",")).filter((it) => typeof it.role === "string" && roles.includes(it.role));
|
|
8585
|
+
const grouped = groupBy(elements, (it) => it.role);
|
|
8586
|
+
for (const nodes of Object.values(grouped)) {
|
|
8587
|
+
if (nodes.length <= 1) {
|
|
8588
|
+
continue;
|
|
8589
|
+
}
|
|
8590
|
+
const entries = nodes.map((it) => getTextEntryFromElement(document, it));
|
|
8591
|
+
const filteredEntries = entries.filter(isExcluded);
|
|
8592
|
+
for (const entry of filteredEntries) {
|
|
8593
|
+
if (entry.text instanceof DynamicValue) {
|
|
8594
|
+
continue;
|
|
8595
|
+
}
|
|
8596
|
+
const dup = entries.filter((it) => it.text === entry.text).length > 1;
|
|
8597
|
+
if (!entry.text || dup) {
|
|
8598
|
+
const message = `Landmarks must have a non-empty and unique accessible name (aria-label or aria-labelledby)`;
|
|
8599
|
+
const location = entry.location;
|
|
8600
|
+
this.report({
|
|
8601
|
+
node: entry.node,
|
|
8602
|
+
message,
|
|
8603
|
+
location
|
|
8604
|
+
});
|
|
8605
|
+
}
|
|
8606
|
+
}
|
|
8607
|
+
}
|
|
8608
|
+
});
|
|
8609
|
+
}
|
|
8610
|
+
}
|
|
8611
|
+
|
|
8355
8612
|
const defaults$4 = {
|
|
8356
8613
|
ignoreCase: false,
|
|
8357
8614
|
requireSemicolon: true
|
|
@@ -8741,7 +8998,7 @@ class H32 extends Rule {
|
|
|
8741
8998
|
}
|
|
8742
8999
|
function isSubmit(node) {
|
|
8743
9000
|
const type = node.getAttribute("type");
|
|
8744
|
-
return Boolean(type
|
|
9001
|
+
return Boolean(!type || type.valueMatches(/submit|image/));
|
|
8745
9002
|
}
|
|
8746
9003
|
function isAssociated(id, node) {
|
|
8747
9004
|
const form = node.getAttribute("form");
|
|
@@ -8833,29 +9090,35 @@ class H37 extends Rule {
|
|
|
8833
9090
|
};
|
|
8834
9091
|
}
|
|
8835
9092
|
setup() {
|
|
8836
|
-
this.on("
|
|
8837
|
-
const
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
if (!inAccessibilityTree(node)) {
|
|
8842
|
-
return;
|
|
9093
|
+
this.on("dom:ready", (event) => {
|
|
9094
|
+
const { document } = event;
|
|
9095
|
+
const nodes = document.querySelectorAll("img, input");
|
|
9096
|
+
for (const node of nodes) {
|
|
9097
|
+
this.validateNode(node);
|
|
8843
9098
|
}
|
|
8844
|
-
|
|
9099
|
+
});
|
|
9100
|
+
}
|
|
9101
|
+
validateNode(node) {
|
|
9102
|
+
if (!needsAlt(node)) {
|
|
9103
|
+
return;
|
|
9104
|
+
}
|
|
9105
|
+
if (!inAccessibilityTree(node)) {
|
|
9106
|
+
return;
|
|
9107
|
+
}
|
|
9108
|
+
if (Boolean(node.getAttributeValue("alt")) || Boolean(node.hasAttribute("alt") && this.options.allowEmpty)) {
|
|
9109
|
+
return;
|
|
9110
|
+
}
|
|
9111
|
+
for (const attr of this.options.alias) {
|
|
9112
|
+
if (node.getAttribute(attr)) {
|
|
8845
9113
|
return;
|
|
8846
9114
|
}
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
this.report(node, `${getTag(node)} cannot have empty "alt" attribute`, attr == null ? void 0 : attr.keyLocation);
|
|
8855
|
-
} else {
|
|
8856
|
-
this.report(node, `${getTag(node)} is missing required "alt" attribute`, node.location);
|
|
8857
|
-
}
|
|
8858
|
-
});
|
|
9115
|
+
}
|
|
9116
|
+
if (node.hasAttribute("alt")) {
|
|
9117
|
+
const attr = node.getAttribute("alt");
|
|
9118
|
+
this.report(node, `${getTag(node)} cannot have empty "alt" attribute`, attr == null ? void 0 : attr.keyLocation);
|
|
9119
|
+
} else {
|
|
9120
|
+
this.report(node, `${getTag(node)} is missing required "alt" attribute`, node.location);
|
|
9121
|
+
}
|
|
8859
9122
|
}
|
|
8860
9123
|
}
|
|
8861
9124
|
|
|
@@ -8946,7 +9209,7 @@ class H71 extends Rule {
|
|
|
8946
9209
|
}
|
|
8947
9210
|
}
|
|
8948
9211
|
|
|
8949
|
-
const bundledRules$
|
|
9212
|
+
const bundledRules$1 = {
|
|
8950
9213
|
"wcag/h30": H30,
|
|
8951
9214
|
"wcag/h32": H32,
|
|
8952
9215
|
"wcag/h36": H36,
|
|
@@ -8955,7 +9218,7 @@ const bundledRules$2 = {
|
|
|
8955
9218
|
"wcag/h67": H67,
|
|
8956
9219
|
"wcag/h71": H71
|
|
8957
9220
|
};
|
|
8958
|
-
var WCAG = bundledRules$
|
|
9221
|
+
var WCAG = bundledRules$1;
|
|
8959
9222
|
|
|
8960
9223
|
const bundledRules = {
|
|
8961
9224
|
"allowed-links": AllowedLinks,
|
|
@@ -8991,6 +9254,7 @@ const bundledRules = {
|
|
|
8991
9254
|
"empty-title": EmptyTitle,
|
|
8992
9255
|
"form-dup-name": FormDupName,
|
|
8993
9256
|
"heading-level": HeadingLevel,
|
|
9257
|
+
"hidden-focusable": HiddenFocusable,
|
|
8994
9258
|
"id-pattern": IdPattern,
|
|
8995
9259
|
"input-attributes": InputAttributes,
|
|
8996
9260
|
"input-missing-label": InputMissingLabel,
|
|
@@ -9031,13 +9295,13 @@ const bundledRules = {
|
|
|
9031
9295
|
"svg-focusable": SvgFocusable,
|
|
9032
9296
|
"tel-non-breaking": TelNonBreaking,
|
|
9033
9297
|
"text-content": TextContent,
|
|
9298
|
+
"unique-landmark": UniqueLandmark,
|
|
9034
9299
|
"unrecognized-char-ref": UnknownCharReference,
|
|
9035
9300
|
"valid-id": ValidID,
|
|
9036
9301
|
"void-content": VoidContent,
|
|
9037
9302
|
"void-style": VoidStyle,
|
|
9038
9303
|
...WCAG
|
|
9039
9304
|
};
|
|
9040
|
-
var bundledRules$1 = bundledRules;
|
|
9041
9305
|
|
|
9042
9306
|
var defaultConfig = {};
|
|
9043
9307
|
|
|
@@ -9049,6 +9313,7 @@ const config$4 = {
|
|
|
9049
9313
|
"deprecated-rule": "warn",
|
|
9050
9314
|
"empty-heading": "error",
|
|
9051
9315
|
"empty-title": "error",
|
|
9316
|
+
"hidden-focusable": "error",
|
|
9052
9317
|
"meta-refresh": "error",
|
|
9053
9318
|
"multiple-labeled-controls": "error",
|
|
9054
9319
|
"no-autoplay": ["error", { include: ["audio", "video"] }],
|
|
@@ -9060,6 +9325,7 @@ const config$4 = {
|
|
|
9060
9325
|
"prefer-native-element": "error",
|
|
9061
9326
|
"svg-focusable": "off",
|
|
9062
9327
|
"text-content": "error",
|
|
9328
|
+
"unique-landmark": "error",
|
|
9063
9329
|
"wcag/h30": "error",
|
|
9064
9330
|
"wcag/h32": "error",
|
|
9065
9331
|
"wcag/h36": "error",
|
|
@@ -9122,6 +9388,7 @@ const config$1 = {
|
|
|
9122
9388
|
"empty-heading": "error",
|
|
9123
9389
|
"empty-title": "error",
|
|
9124
9390
|
"form-dup-name": "error",
|
|
9391
|
+
"hidden-focusable": "error",
|
|
9125
9392
|
"input-attributes": "error",
|
|
9126
9393
|
"long-title": "error",
|
|
9127
9394
|
"map-dup-name": "error",
|
|
@@ -9154,6 +9421,7 @@ const config$1 = {
|
|
|
9154
9421
|
"svg-focusable": "off",
|
|
9155
9422
|
"tel-non-breaking": "error",
|
|
9156
9423
|
"text-content": "error",
|
|
9424
|
+
"unique-landmark": "error",
|
|
9157
9425
|
"unrecognized-char-ref": "error",
|
|
9158
9426
|
"valid-id": ["error", { relaxed: false }],
|
|
9159
9427
|
void: "off",
|
|
@@ -9498,7 +9766,7 @@ class Config {
|
|
|
9498
9766
|
if (configData.rules) {
|
|
9499
9767
|
const normalizedRules = Config.getRulesObject(configData.rules);
|
|
9500
9768
|
for (const [ruleId, [, ruleOptions]] of normalizedRules.entries()) {
|
|
9501
|
-
const cls = bundledRules
|
|
9769
|
+
const cls = bundledRules[ruleId];
|
|
9502
9770
|
const path = `/rules/${ruleId}/1`;
|
|
9503
9771
|
Rule.validateOptions(cls, ruleId, path, ruleOptions, filename, configData);
|
|
9504
9772
|
}
|
|
@@ -9871,16 +10139,17 @@ class EventHandler {
|
|
|
9871
10139
|
* @returns Unregistration function.
|
|
9872
10140
|
*/
|
|
9873
10141
|
on(event, callback) {
|
|
9874
|
-
const
|
|
10142
|
+
const { listeners } = this;
|
|
10143
|
+
const names = event.split(",").map((it) => it.trim());
|
|
9875
10144
|
for (const name of names) {
|
|
9876
|
-
|
|
9877
|
-
|
|
10145
|
+
const list = listeners[name] ?? [];
|
|
10146
|
+
listeners[name] = list;
|
|
10147
|
+
list.push(callback);
|
|
9878
10148
|
}
|
|
9879
10149
|
return () => {
|
|
9880
10150
|
for (const name of names) {
|
|
9881
|
-
|
|
9882
|
-
|
|
9883
|
-
});
|
|
10151
|
+
const list = listeners[name];
|
|
10152
|
+
this.listeners[name] = list.filter((fn) => fn !== callback);
|
|
9884
10153
|
}
|
|
9885
10154
|
};
|
|
9886
10155
|
}
|
|
@@ -9906,10 +10175,15 @@ class EventHandler {
|
|
|
9906
10175
|
* @param data - Event data.
|
|
9907
10176
|
*/
|
|
9908
10177
|
trigger(event, data) {
|
|
9909
|
-
const
|
|
9910
|
-
callbacks.forEach((listener) => {
|
|
10178
|
+
for (const listener of this.getCallbacks(event)) {
|
|
9911
10179
|
listener.call(null, event, data);
|
|
9912
|
-
}
|
|
10180
|
+
}
|
|
10181
|
+
}
|
|
10182
|
+
getCallbacks(event) {
|
|
10183
|
+
const { listeners } = this;
|
|
10184
|
+
const callbacks = listeners[event] ?? [];
|
|
10185
|
+
const global = listeners["*"] ?? [];
|
|
10186
|
+
return [...callbacks, ...global];
|
|
9913
10187
|
}
|
|
9914
10188
|
}
|
|
9915
10189
|
|
|
@@ -9979,10 +10253,10 @@ class Parser {
|
|
|
9979
10253
|
location: null
|
|
9980
10254
|
});
|
|
9981
10255
|
this.dom = new DOMTree({
|
|
9982
|
-
filename: source.filename
|
|
9983
|
-
offset: source.offset
|
|
9984
|
-
line: source.line
|
|
9985
|
-
column: source.column
|
|
10256
|
+
filename: source.filename,
|
|
10257
|
+
offset: source.offset,
|
|
10258
|
+
line: source.line,
|
|
10259
|
+
column: source.column,
|
|
9986
10260
|
size: 0
|
|
9987
10261
|
});
|
|
9988
10262
|
this.trigger("dom:load", {
|
|
@@ -10506,7 +10780,7 @@ function isThenable(value) {
|
|
|
10506
10780
|
return value && typeof value === "object" && "then" in value && typeof value.then === "function";
|
|
10507
10781
|
}
|
|
10508
10782
|
|
|
10509
|
-
const ruleIds = new Set(Object.keys(bundledRules
|
|
10783
|
+
const ruleIds = new Set(Object.keys(bundledRules));
|
|
10510
10784
|
function ruleExists(ruleId) {
|
|
10511
10785
|
return ruleIds.has(ruleId);
|
|
10512
10786
|
}
|
|
@@ -10595,9 +10869,7 @@ class Reporter {
|
|
|
10595
10869
|
valid: this.isValid(),
|
|
10596
10870
|
results: Object.keys(this.result).map((filePath) => {
|
|
10597
10871
|
const messages = Array.from(this.result[filePath], freeze).sort(messageSort);
|
|
10598
|
-
const source = (sources ?? []).find(
|
|
10599
|
-
(source2) => filePath === (source2.filename ?? "")
|
|
10600
|
-
);
|
|
10872
|
+
const source = (sources ?? []).find((source2) => filePath === source2.filename);
|
|
10601
10873
|
return {
|
|
10602
10874
|
filePath,
|
|
10603
10875
|
messages,
|
|
@@ -10665,7 +10937,7 @@ class Engine {
|
|
|
10665
10937
|
this.ParserClass = ParserClass;
|
|
10666
10938
|
const result = this.initPlugins(this.config);
|
|
10667
10939
|
this.availableRules = {
|
|
10668
|
-
...bundledRules
|
|
10940
|
+
...bundledRules,
|
|
10669
10941
|
...result.availableRules
|
|
10670
10942
|
};
|
|
10671
10943
|
}
|
|
@@ -11094,10 +11366,11 @@ class HtmlValidate {
|
|
|
11094
11366
|
* @returns Report output.
|
|
11095
11367
|
*/
|
|
11096
11368
|
async validateSource(input, configOverride) {
|
|
11097
|
-
const
|
|
11098
|
-
const
|
|
11369
|
+
const source = normalizeSource(input);
|
|
11370
|
+
const config = await this.getConfigFor(source.filename, configOverride);
|
|
11371
|
+
const transformedSource = config.transformSource(source);
|
|
11099
11372
|
const engine = new Engine(config, Parser);
|
|
11100
|
-
return engine.lint(
|
|
11373
|
+
return engine.lint(transformedSource);
|
|
11101
11374
|
}
|
|
11102
11375
|
/**
|
|
11103
11376
|
* Parse and validate HTML from [[Source]].
|
|
@@ -11107,10 +11380,11 @@ class HtmlValidate {
|
|
|
11107
11380
|
* @returns Report output.
|
|
11108
11381
|
*/
|
|
11109
11382
|
validateSourceSync(input, configOverride) {
|
|
11110
|
-
const
|
|
11111
|
-
const
|
|
11383
|
+
const source = normalizeSource(input);
|
|
11384
|
+
const config = this.getConfigForSync(source.filename, configOverride);
|
|
11385
|
+
const transformedSource = config.transformSource(source);
|
|
11112
11386
|
const engine = new Engine(config, Parser);
|
|
11113
|
-
return engine.lint(
|
|
11387
|
+
return engine.lint(transformedSource);
|
|
11114
11388
|
}
|
|
11115
11389
|
/**
|
|
11116
11390
|
* Parse and validate HTML from file.
|
|
@@ -11419,7 +11693,7 @@ class HtmlValidate {
|
|
|
11419
11693
|
}
|
|
11420
11694
|
|
|
11421
11695
|
const name = "html-validate";
|
|
11422
|
-
const version = "8.
|
|
11696
|
+
const version = "8.9.1";
|
|
11423
11697
|
const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
|
|
11424
11698
|
|
|
11425
11699
|
function definePlugin(plugin) {
|