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/cjs/core.js
CHANGED
|
@@ -601,6 +601,18 @@ const patternProperties = {
|
|
|
601
601
|
description: "Script-supporting elements are elements which can be inserted where othersise not permitted to assist in templating",
|
|
602
602
|
type: "boolean"
|
|
603
603
|
},
|
|
604
|
+
focusable: {
|
|
605
|
+
title: "Mark this element as focusable",
|
|
606
|
+
description: "This element may contain an associated label element.",
|
|
607
|
+
anyOf: [
|
|
608
|
+
{
|
|
609
|
+
type: "boolean"
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
"function": true
|
|
613
|
+
}
|
|
614
|
+
]
|
|
615
|
+
},
|
|
604
616
|
form: {
|
|
605
617
|
title: "Mark element as a submittable form element",
|
|
606
618
|
type: "boolean"
|
|
@@ -975,6 +987,7 @@ const MetaCopyableProperty = [
|
|
|
975
987
|
"embedded",
|
|
976
988
|
"interactive",
|
|
977
989
|
"transparent",
|
|
990
|
+
"focusable",
|
|
978
991
|
"form",
|
|
979
992
|
"formAssociated",
|
|
980
993
|
"labelable",
|
|
@@ -1047,6 +1060,7 @@ function migrateElement(src) {
|
|
|
1047
1060
|
},
|
|
1048
1061
|
attributes: migrateAttributes(src),
|
|
1049
1062
|
textContent: src.textContent,
|
|
1063
|
+
focusable: src.focusable ?? false,
|
|
1050
1064
|
implicitRole: src.implicitRole ?? (() => null)
|
|
1051
1065
|
};
|
|
1052
1066
|
delete result.deprecatedAttributes;
|
|
@@ -1302,6 +1316,9 @@ function expandProperties(node, entry) {
|
|
|
1302
1316
|
setMetaProperty(entry, key, evaluateProperty(node, property));
|
|
1303
1317
|
}
|
|
1304
1318
|
}
|
|
1319
|
+
if (typeof entry.focusable === "function") {
|
|
1320
|
+
setMetaProperty(entry, "focusable", entry.focusable(node._adapter));
|
|
1321
|
+
}
|
|
1305
1322
|
}
|
|
1306
1323
|
function expandRegexValue(value) {
|
|
1307
1324
|
if (value instanceof RegExp) {
|
|
@@ -1516,10 +1533,10 @@ class Context {
|
|
|
1516
1533
|
constructor(source) {
|
|
1517
1534
|
this.state = State.INITIAL;
|
|
1518
1535
|
this.string = source.data;
|
|
1519
|
-
this.filename = source.filename
|
|
1520
|
-
this.offset = source.offset
|
|
1521
|
-
this.line = source.line
|
|
1522
|
-
this.column = source.column
|
|
1536
|
+
this.filename = source.filename;
|
|
1537
|
+
this.offset = source.offset;
|
|
1538
|
+
this.line = source.line;
|
|
1539
|
+
this.column = source.column;
|
|
1523
1540
|
this.contentModel = 1 /* TEXT */;
|
|
1524
1541
|
}
|
|
1525
1542
|
getTruncatedLine(n = 13) {
|
|
@@ -1552,6 +1569,16 @@ class Context {
|
|
|
1552
1569
|
}
|
|
1553
1570
|
}
|
|
1554
1571
|
|
|
1572
|
+
function normalizeSource(source) {
|
|
1573
|
+
return {
|
|
1574
|
+
filename: "",
|
|
1575
|
+
offset: 0,
|
|
1576
|
+
line: 1,
|
|
1577
|
+
column: 1,
|
|
1578
|
+
...source
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1555
1582
|
var NodeType = /* @__PURE__ */ ((NodeType2) => {
|
|
1556
1583
|
NodeType2[NodeType2["ELEMENT_NODE"] = 1] = "ELEMENT_NODE";
|
|
1557
1584
|
NodeType2[NodeType2["TEXT_NODE"] = 3] = "TEXT_NODE";
|
|
@@ -2132,6 +2159,7 @@ class TextNode extends DOMNode {
|
|
|
2132
2159
|
}
|
|
2133
2160
|
}
|
|
2134
2161
|
|
|
2162
|
+
const ROLE = Symbol("role");
|
|
2135
2163
|
var NodeClosed = /* @__PURE__ */ ((NodeClosed2) => {
|
|
2136
2164
|
NodeClosed2[NodeClosed2["Open"] = 0] = "Open";
|
|
2137
2165
|
NodeClosed2[NodeClosed2["EndTag"] = 1] = "EndTag";
|
|
@@ -2367,6 +2395,27 @@ class HtmlElement extends DOMNode {
|
|
|
2367
2395
|
get meta() {
|
|
2368
2396
|
return this.metaElement;
|
|
2369
2397
|
}
|
|
2398
|
+
/**
|
|
2399
|
+
* Get current role for this element (explicit with `role` attribute or mapped
|
|
2400
|
+
* with implicit role).
|
|
2401
|
+
*
|
|
2402
|
+
* @since 8.9.1
|
|
2403
|
+
*/
|
|
2404
|
+
get role() {
|
|
2405
|
+
const cached = this.cacheGet(ROLE);
|
|
2406
|
+
if (cached !== void 0) {
|
|
2407
|
+
return cached;
|
|
2408
|
+
}
|
|
2409
|
+
const role = this.getAttribute("role");
|
|
2410
|
+
if (role) {
|
|
2411
|
+
return this.cacheSet(ROLE, role.value);
|
|
2412
|
+
}
|
|
2413
|
+
if (this.metaElement) {
|
|
2414
|
+
const implicitRole = this.metaElement.implicitRole(this._adapter);
|
|
2415
|
+
return this.cacheSet(ROLE, implicitRole);
|
|
2416
|
+
}
|
|
2417
|
+
return this.cacheSet(ROLE, null);
|
|
2418
|
+
}
|
|
2370
2419
|
/**
|
|
2371
2420
|
* Set annotation for this element.
|
|
2372
2421
|
*/
|
|
@@ -3145,8 +3194,21 @@ function isKeywordIgnored(options, keyword, matcher = (list, it) => list.include
|
|
|
3145
3194
|
const ARIA_HIDDEN_CACHE = Symbol(isAriaHidden.name);
|
|
3146
3195
|
const HTML_HIDDEN_CACHE = Symbol(isHTMLHidden.name);
|
|
3147
3196
|
const ROLE_PRESENTATION_CACHE = Symbol(isPresentation.name);
|
|
3197
|
+
const STYLE_HIDDEN_CACHE = Symbol(isStyleHidden.name);
|
|
3148
3198
|
function inAccessibilityTree(node) {
|
|
3149
|
-
|
|
3199
|
+
if (isAriaHidden(node)) {
|
|
3200
|
+
return false;
|
|
3201
|
+
}
|
|
3202
|
+
if (isPresentation(node)) {
|
|
3203
|
+
return false;
|
|
3204
|
+
}
|
|
3205
|
+
if (isHTMLHidden(node)) {
|
|
3206
|
+
return false;
|
|
3207
|
+
}
|
|
3208
|
+
if (isStyleHidden(node)) {
|
|
3209
|
+
return false;
|
|
3210
|
+
}
|
|
3211
|
+
return true;
|
|
3150
3212
|
}
|
|
3151
3213
|
function isAriaHiddenImpl(node) {
|
|
3152
3214
|
const isHidden = (node2) => {
|
|
@@ -3184,22 +3246,41 @@ function isHTMLHidden(node, details) {
|
|
|
3184
3246
|
const result = node.cacheSet(HTML_HIDDEN_CACHE, isHTMLHiddenImpl(node));
|
|
3185
3247
|
return details ? result : result.byParent || result.bySelf;
|
|
3186
3248
|
}
|
|
3249
|
+
function isStyleHiddenImpl(node) {
|
|
3250
|
+
const isHidden = (node2) => {
|
|
3251
|
+
const style = node2.getAttribute("style");
|
|
3252
|
+
const { display, visibility } = parseCssDeclaration(style == null ? void 0 : style.value);
|
|
3253
|
+
return display === "none" || visibility === "hidden";
|
|
3254
|
+
};
|
|
3255
|
+
const byParent = node.parent ? isStyleHidden(node.parent) : false;
|
|
3256
|
+
const bySelf = isHidden(node);
|
|
3257
|
+
return byParent || bySelf;
|
|
3258
|
+
}
|
|
3259
|
+
function isStyleHidden(node) {
|
|
3260
|
+
const cached = node.cacheGet(STYLE_HIDDEN_CACHE);
|
|
3261
|
+
if (cached) {
|
|
3262
|
+
return cached;
|
|
3263
|
+
}
|
|
3264
|
+
return node.cacheSet(STYLE_HIDDEN_CACHE, isStyleHiddenImpl(node));
|
|
3265
|
+
}
|
|
3187
3266
|
function isPresentation(node) {
|
|
3188
3267
|
if (node.cacheExists(ROLE_PRESENTATION_CACHE)) {
|
|
3189
3268
|
return Boolean(node.cacheGet(ROLE_PRESENTATION_CACHE));
|
|
3190
3269
|
}
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3270
|
+
const meta = node.meta;
|
|
3271
|
+
if (meta && meta.interactive) {
|
|
3272
|
+
return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
|
|
3273
|
+
}
|
|
3274
|
+
const tabindex = node.getAttribute("tabindex");
|
|
3275
|
+
if (tabindex) {
|
|
3276
|
+
return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
|
|
3277
|
+
}
|
|
3278
|
+
const role = node.getAttribute("role");
|
|
3279
|
+
if (role && (role.value === "presentation" || role.value === "none")) {
|
|
3280
|
+
return node.cacheSet(ROLE_PRESENTATION_CACHE, true);
|
|
3281
|
+
} else {
|
|
3282
|
+
return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
|
|
3283
|
+
}
|
|
3203
3284
|
}
|
|
3204
3285
|
|
|
3205
3286
|
const cachePrefix = classifyNodeText.name;
|
|
@@ -6187,6 +6268,58 @@ class HeadingLevel extends Rule {
|
|
|
6187
6268
|
}
|
|
6188
6269
|
}
|
|
6189
6270
|
|
|
6271
|
+
class HiddenFocusable extends Rule {
|
|
6272
|
+
documentation(context) {
|
|
6273
|
+
const byParent = context === "parent" ? " In this case it is being hidden by an ancestor with `aria-hidden.`" : "";
|
|
6274
|
+
return {
|
|
6275
|
+
description: [
|
|
6276
|
+
`\`aria-hidden\` cannot be used on focusable elements.${byParent}`,
|
|
6277
|
+
"",
|
|
6278
|
+
"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).",
|
|
6279
|
+
"This is often confusing for users of AT such as screenreaders.",
|
|
6280
|
+
"",
|
|
6281
|
+
"To fix this either:",
|
|
6282
|
+
" - Remove `aria-hidden`.",
|
|
6283
|
+
" - Remove the element from the DOM instead.",
|
|
6284
|
+
" - Use the `hidden` attribute or similar means to hide the element."
|
|
6285
|
+
].join("\n"),
|
|
6286
|
+
url: "https://html-validate.org/rules/hidden-focusable.html"
|
|
6287
|
+
};
|
|
6288
|
+
}
|
|
6289
|
+
setup() {
|
|
6290
|
+
const focusable = this.getTagsWithProperty("focusable");
|
|
6291
|
+
const selector = ["[tabindex]", ...focusable].join(",");
|
|
6292
|
+
this.on("dom:ready", (event) => {
|
|
6293
|
+
const { document } = event;
|
|
6294
|
+
for (const element of document.querySelectorAll(selector)) {
|
|
6295
|
+
if (isHTMLHidden(element) || isStyleHidden(element)) {
|
|
6296
|
+
continue;
|
|
6297
|
+
}
|
|
6298
|
+
if (isAriaHidden(element)) {
|
|
6299
|
+
this.validateElement(element);
|
|
6300
|
+
}
|
|
6301
|
+
}
|
|
6302
|
+
});
|
|
6303
|
+
}
|
|
6304
|
+
validateElement(element) {
|
|
6305
|
+
const { meta } = element;
|
|
6306
|
+
const tabindex = element.getAttribute("tabindex");
|
|
6307
|
+
if (meta && !meta.focusable && !tabindex) {
|
|
6308
|
+
return;
|
|
6309
|
+
}
|
|
6310
|
+
const attribute = element.getAttribute("aria-hidden");
|
|
6311
|
+
const message = attribute ? `aria-hidden cannot be used on focusable elements` : `aria-hidden cannot be used on focusable elements (hidden by ancestor element)`;
|
|
6312
|
+
const location = attribute ? attribute.keyLocation : element.location;
|
|
6313
|
+
const context = attribute ? "self" : "parent";
|
|
6314
|
+
this.report({
|
|
6315
|
+
node: element,
|
|
6316
|
+
message,
|
|
6317
|
+
location,
|
|
6318
|
+
context
|
|
6319
|
+
});
|
|
6320
|
+
}
|
|
6321
|
+
}
|
|
6322
|
+
|
|
6190
6323
|
const defaults$g = {
|
|
6191
6324
|
pattern: "kebabcase"
|
|
6192
6325
|
};
|
|
@@ -6372,7 +6505,7 @@ function isHidden(node, context) {
|
|
|
6372
6505
|
if (reference && reference.isSameNode(node)) {
|
|
6373
6506
|
return false;
|
|
6374
6507
|
} else {
|
|
6375
|
-
return
|
|
6508
|
+
return !inAccessibilityTree(node);
|
|
6376
6509
|
}
|
|
6377
6510
|
}
|
|
6378
6511
|
function hasImgAltText(node, context) {
|
|
@@ -7322,7 +7455,7 @@ class NoRawCharacters extends Rule {
|
|
|
7322
7455
|
}
|
|
7323
7456
|
}
|
|
7324
7457
|
|
|
7325
|
-
const selectors = ["input[aria-label]", "textarea[aria-label]", "select[aria-label]"];
|
|
7458
|
+
const selectors$1 = ["input[aria-label]", "textarea[aria-label]", "select[aria-label]"];
|
|
7326
7459
|
class NoRedundantAriaLabel extends Rule {
|
|
7327
7460
|
documentation() {
|
|
7328
7461
|
return {
|
|
@@ -7333,7 +7466,7 @@ class NoRedundantAriaLabel extends Rule {
|
|
|
7333
7466
|
setup() {
|
|
7334
7467
|
this.on("dom:ready", (event) => {
|
|
7335
7468
|
const { document } = event;
|
|
7336
|
-
const elements = document.querySelectorAll(selectors.join(","));
|
|
7469
|
+
const elements = document.querySelectorAll(selectors$1.join(","));
|
|
7337
7470
|
for (const element of elements) {
|
|
7338
7471
|
const ariaLabel = element.getAttribute("aria-label");
|
|
7339
7472
|
const id = element.id;
|
|
@@ -7949,7 +8082,7 @@ class RequireSri extends Rule {
|
|
|
7949
8082
|
}
|
|
7950
8083
|
documentation() {
|
|
7951
8084
|
return {
|
|
7952
|
-
description: `Subresource Integrity (SRI) \`integrity\` attribute is required to prevent manipulation from Content Delivery Networks
|
|
8085
|
+
description: `Subresource Integrity (SRI) \`integrity\` attribute is required to prevent tampering or manipulation from Content Delivery Networks (CDN), rouge proxies, malicious entities, etc.`,
|
|
7953
8086
|
url: "https://html-validate.org/rules/require-sri.html"
|
|
7954
8087
|
};
|
|
7955
8088
|
}
|
|
@@ -8362,6 +8495,130 @@ class TextContent extends Rule {
|
|
|
8362
8495
|
}
|
|
8363
8496
|
}
|
|
8364
8497
|
|
|
8498
|
+
const roles = ["complementary", "contentinfo", "form", "banner", "main", "navigation", "region"];
|
|
8499
|
+
const selectors = [
|
|
8500
|
+
"aside",
|
|
8501
|
+
"footer",
|
|
8502
|
+
"form",
|
|
8503
|
+
"header",
|
|
8504
|
+
"main",
|
|
8505
|
+
"nav",
|
|
8506
|
+
"section",
|
|
8507
|
+
...roles.map((it) => `[role="${it}"]`)
|
|
8508
|
+
/* <search> does not (yet?) require a unique name */
|
|
8509
|
+
];
|
|
8510
|
+
function getTextFromReference(document, id) {
|
|
8511
|
+
if (!id || id instanceof DynamicValue) {
|
|
8512
|
+
return id;
|
|
8513
|
+
}
|
|
8514
|
+
const selector = `#${id}`;
|
|
8515
|
+
const ref = document.querySelector(selector);
|
|
8516
|
+
if (ref) {
|
|
8517
|
+
return ref.textContent;
|
|
8518
|
+
} else {
|
|
8519
|
+
return selector;
|
|
8520
|
+
}
|
|
8521
|
+
}
|
|
8522
|
+
function groupBy(values, callback) {
|
|
8523
|
+
const result = {};
|
|
8524
|
+
for (const value of values) {
|
|
8525
|
+
const key = callback(value);
|
|
8526
|
+
if (key in result) {
|
|
8527
|
+
result[key].push(value);
|
|
8528
|
+
} else {
|
|
8529
|
+
result[key] = [value];
|
|
8530
|
+
}
|
|
8531
|
+
}
|
|
8532
|
+
return result;
|
|
8533
|
+
}
|
|
8534
|
+
function getTextEntryFromElement(document, node) {
|
|
8535
|
+
const ariaLabel = node.getAttribute("aria-label");
|
|
8536
|
+
if (ariaLabel) {
|
|
8537
|
+
return {
|
|
8538
|
+
node,
|
|
8539
|
+
text: ariaLabel.value,
|
|
8540
|
+
location: ariaLabel.keyLocation
|
|
8541
|
+
};
|
|
8542
|
+
}
|
|
8543
|
+
const ariaLabelledby = node.getAttribute("aria-labelledby");
|
|
8544
|
+
if (ariaLabelledby) {
|
|
8545
|
+
const text = getTextFromReference(document, ariaLabelledby.value);
|
|
8546
|
+
return {
|
|
8547
|
+
node,
|
|
8548
|
+
text,
|
|
8549
|
+
location: ariaLabelledby.keyLocation
|
|
8550
|
+
};
|
|
8551
|
+
}
|
|
8552
|
+
return {
|
|
8553
|
+
node,
|
|
8554
|
+
text: null,
|
|
8555
|
+
location: node.location
|
|
8556
|
+
};
|
|
8557
|
+
}
|
|
8558
|
+
function isExcluded(entry) {
|
|
8559
|
+
const { node, text } = entry;
|
|
8560
|
+
if (text === null) {
|
|
8561
|
+
return !(node.is("form") || node.is("section"));
|
|
8562
|
+
}
|
|
8563
|
+
return true;
|
|
8564
|
+
}
|
|
8565
|
+
class UniqueLandmark extends Rule {
|
|
8566
|
+
documentation() {
|
|
8567
|
+
return {
|
|
8568
|
+
description: [
|
|
8569
|
+
"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.",
|
|
8570
|
+
"For instance, if the document has two `<nav>` elements each of them need an accessible name to be distinguished from each other.",
|
|
8571
|
+
"",
|
|
8572
|
+
"The following elements / roles are considered landmarks:",
|
|
8573
|
+
"",
|
|
8574
|
+
' - `aside` or `[role="complementary"]`',
|
|
8575
|
+
' - `footer` or `[role="contentinfo"]`',
|
|
8576
|
+
' - `form` or `[role="form"]`',
|
|
8577
|
+
' - `header` or `[role="banner"]`',
|
|
8578
|
+
' - `main` or `[role="main"]`',
|
|
8579
|
+
' - `nav` or `[role="navigation"]`',
|
|
8580
|
+
' - `section` or `[role="region"]`',
|
|
8581
|
+
"",
|
|
8582
|
+
"To fix this either:",
|
|
8583
|
+
"",
|
|
8584
|
+
" - Add `aria-label`.",
|
|
8585
|
+
" - Add `aria-labelledby`.",
|
|
8586
|
+
" - Remove one of the landmarks."
|
|
8587
|
+
].join("\n"),
|
|
8588
|
+
url: "https://html-validate.org/rules/unique-landmark.html"
|
|
8589
|
+
};
|
|
8590
|
+
}
|
|
8591
|
+
setup() {
|
|
8592
|
+
this.on("dom:ready", (event) => {
|
|
8593
|
+
const { document } = event;
|
|
8594
|
+
const elements = document.querySelectorAll(selectors.join(",")).filter((it) => typeof it.role === "string" && roles.includes(it.role));
|
|
8595
|
+
const grouped = groupBy(elements, (it) => it.role);
|
|
8596
|
+
for (const nodes of Object.values(grouped)) {
|
|
8597
|
+
if (nodes.length <= 1) {
|
|
8598
|
+
continue;
|
|
8599
|
+
}
|
|
8600
|
+
const entries = nodes.map((it) => getTextEntryFromElement(document, it));
|
|
8601
|
+
const filteredEntries = entries.filter(isExcluded);
|
|
8602
|
+
for (const entry of filteredEntries) {
|
|
8603
|
+
if (entry.text instanceof DynamicValue) {
|
|
8604
|
+
continue;
|
|
8605
|
+
}
|
|
8606
|
+
const dup = entries.filter((it) => it.text === entry.text).length > 1;
|
|
8607
|
+
if (!entry.text || dup) {
|
|
8608
|
+
const message = `Landmarks must have a non-empty and unique accessible name (aria-label or aria-labelledby)`;
|
|
8609
|
+
const location = entry.location;
|
|
8610
|
+
this.report({
|
|
8611
|
+
node: entry.node,
|
|
8612
|
+
message,
|
|
8613
|
+
location
|
|
8614
|
+
});
|
|
8615
|
+
}
|
|
8616
|
+
}
|
|
8617
|
+
}
|
|
8618
|
+
});
|
|
8619
|
+
}
|
|
8620
|
+
}
|
|
8621
|
+
|
|
8365
8622
|
const defaults$4 = {
|
|
8366
8623
|
ignoreCase: false,
|
|
8367
8624
|
requireSemicolon: true
|
|
@@ -8751,7 +9008,7 @@ class H32 extends Rule {
|
|
|
8751
9008
|
}
|
|
8752
9009
|
function isSubmit(node) {
|
|
8753
9010
|
const type = node.getAttribute("type");
|
|
8754
|
-
return Boolean(type
|
|
9011
|
+
return Boolean(!type || type.valueMatches(/submit|image/));
|
|
8755
9012
|
}
|
|
8756
9013
|
function isAssociated(id, node) {
|
|
8757
9014
|
const form = node.getAttribute("form");
|
|
@@ -8843,29 +9100,35 @@ class H37 extends Rule {
|
|
|
8843
9100
|
};
|
|
8844
9101
|
}
|
|
8845
9102
|
setup() {
|
|
8846
|
-
this.on("
|
|
8847
|
-
const
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
if (!inAccessibilityTree(node)) {
|
|
8852
|
-
return;
|
|
9103
|
+
this.on("dom:ready", (event) => {
|
|
9104
|
+
const { document } = event;
|
|
9105
|
+
const nodes = document.querySelectorAll("img, input");
|
|
9106
|
+
for (const node of nodes) {
|
|
9107
|
+
this.validateNode(node);
|
|
8853
9108
|
}
|
|
8854
|
-
|
|
9109
|
+
});
|
|
9110
|
+
}
|
|
9111
|
+
validateNode(node) {
|
|
9112
|
+
if (!needsAlt(node)) {
|
|
9113
|
+
return;
|
|
9114
|
+
}
|
|
9115
|
+
if (!inAccessibilityTree(node)) {
|
|
9116
|
+
return;
|
|
9117
|
+
}
|
|
9118
|
+
if (Boolean(node.getAttributeValue("alt")) || Boolean(node.hasAttribute("alt") && this.options.allowEmpty)) {
|
|
9119
|
+
return;
|
|
9120
|
+
}
|
|
9121
|
+
for (const attr of this.options.alias) {
|
|
9122
|
+
if (node.getAttribute(attr)) {
|
|
8855
9123
|
return;
|
|
8856
9124
|
}
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
this.report(node, `${getTag(node)} cannot have empty "alt" attribute`, attr == null ? void 0 : attr.keyLocation);
|
|
8865
|
-
} else {
|
|
8866
|
-
this.report(node, `${getTag(node)} is missing required "alt" attribute`, node.location);
|
|
8867
|
-
}
|
|
8868
|
-
});
|
|
9125
|
+
}
|
|
9126
|
+
if (node.hasAttribute("alt")) {
|
|
9127
|
+
const attr = node.getAttribute("alt");
|
|
9128
|
+
this.report(node, `${getTag(node)} cannot have empty "alt" attribute`, attr == null ? void 0 : attr.keyLocation);
|
|
9129
|
+
} else {
|
|
9130
|
+
this.report(node, `${getTag(node)} is missing required "alt" attribute`, node.location);
|
|
9131
|
+
}
|
|
8869
9132
|
}
|
|
8870
9133
|
}
|
|
8871
9134
|
|
|
@@ -8956,7 +9219,7 @@ class H71 extends Rule {
|
|
|
8956
9219
|
}
|
|
8957
9220
|
}
|
|
8958
9221
|
|
|
8959
|
-
const bundledRules$
|
|
9222
|
+
const bundledRules$1 = {
|
|
8960
9223
|
"wcag/h30": H30,
|
|
8961
9224
|
"wcag/h32": H32,
|
|
8962
9225
|
"wcag/h36": H36,
|
|
@@ -8965,7 +9228,7 @@ const bundledRules$2 = {
|
|
|
8965
9228
|
"wcag/h67": H67,
|
|
8966
9229
|
"wcag/h71": H71
|
|
8967
9230
|
};
|
|
8968
|
-
var WCAG = bundledRules$
|
|
9231
|
+
var WCAG = bundledRules$1;
|
|
8969
9232
|
|
|
8970
9233
|
const bundledRules = {
|
|
8971
9234
|
"allowed-links": AllowedLinks,
|
|
@@ -9001,6 +9264,7 @@ const bundledRules = {
|
|
|
9001
9264
|
"empty-title": EmptyTitle,
|
|
9002
9265
|
"form-dup-name": FormDupName,
|
|
9003
9266
|
"heading-level": HeadingLevel,
|
|
9267
|
+
"hidden-focusable": HiddenFocusable,
|
|
9004
9268
|
"id-pattern": IdPattern,
|
|
9005
9269
|
"input-attributes": InputAttributes,
|
|
9006
9270
|
"input-missing-label": InputMissingLabel,
|
|
@@ -9041,13 +9305,13 @@ const bundledRules = {
|
|
|
9041
9305
|
"svg-focusable": SvgFocusable,
|
|
9042
9306
|
"tel-non-breaking": TelNonBreaking,
|
|
9043
9307
|
"text-content": TextContent,
|
|
9308
|
+
"unique-landmark": UniqueLandmark,
|
|
9044
9309
|
"unrecognized-char-ref": UnknownCharReference,
|
|
9045
9310
|
"valid-id": ValidID,
|
|
9046
9311
|
"void-content": VoidContent,
|
|
9047
9312
|
"void-style": VoidStyle,
|
|
9048
9313
|
...WCAG
|
|
9049
9314
|
};
|
|
9050
|
-
var bundledRules$1 = bundledRules;
|
|
9051
9315
|
|
|
9052
9316
|
var defaultConfig = {};
|
|
9053
9317
|
|
|
@@ -9059,6 +9323,7 @@ const config$4 = {
|
|
|
9059
9323
|
"deprecated-rule": "warn",
|
|
9060
9324
|
"empty-heading": "error",
|
|
9061
9325
|
"empty-title": "error",
|
|
9326
|
+
"hidden-focusable": "error",
|
|
9062
9327
|
"meta-refresh": "error",
|
|
9063
9328
|
"multiple-labeled-controls": "error",
|
|
9064
9329
|
"no-autoplay": ["error", { include: ["audio", "video"] }],
|
|
@@ -9070,6 +9335,7 @@ const config$4 = {
|
|
|
9070
9335
|
"prefer-native-element": "error",
|
|
9071
9336
|
"svg-focusable": "off",
|
|
9072
9337
|
"text-content": "error",
|
|
9338
|
+
"unique-landmark": "error",
|
|
9073
9339
|
"wcag/h30": "error",
|
|
9074
9340
|
"wcag/h32": "error",
|
|
9075
9341
|
"wcag/h36": "error",
|
|
@@ -9132,6 +9398,7 @@ const config$1 = {
|
|
|
9132
9398
|
"empty-heading": "error",
|
|
9133
9399
|
"empty-title": "error",
|
|
9134
9400
|
"form-dup-name": "error",
|
|
9401
|
+
"hidden-focusable": "error",
|
|
9135
9402
|
"input-attributes": "error",
|
|
9136
9403
|
"long-title": "error",
|
|
9137
9404
|
"map-dup-name": "error",
|
|
@@ -9164,6 +9431,7 @@ const config$1 = {
|
|
|
9164
9431
|
"svg-focusable": "off",
|
|
9165
9432
|
"tel-non-breaking": "error",
|
|
9166
9433
|
"text-content": "error",
|
|
9434
|
+
"unique-landmark": "error",
|
|
9167
9435
|
"unrecognized-char-ref": "error",
|
|
9168
9436
|
"valid-id": ["error", { relaxed: false }],
|
|
9169
9437
|
void: "off",
|
|
@@ -9508,7 +9776,7 @@ class Config {
|
|
|
9508
9776
|
if (configData.rules) {
|
|
9509
9777
|
const normalizedRules = Config.getRulesObject(configData.rules);
|
|
9510
9778
|
for (const [ruleId, [, ruleOptions]] of normalizedRules.entries()) {
|
|
9511
|
-
const cls = bundledRules
|
|
9779
|
+
const cls = bundledRules[ruleId];
|
|
9512
9780
|
const path = `/rules/${ruleId}/1`;
|
|
9513
9781
|
Rule.validateOptions(cls, ruleId, path, ruleOptions, filename, configData);
|
|
9514
9782
|
}
|
|
@@ -9881,16 +10149,17 @@ class EventHandler {
|
|
|
9881
10149
|
* @returns Unregistration function.
|
|
9882
10150
|
*/
|
|
9883
10151
|
on(event, callback) {
|
|
9884
|
-
const
|
|
10152
|
+
const { listeners } = this;
|
|
10153
|
+
const names = event.split(",").map((it) => it.trim());
|
|
9885
10154
|
for (const name of names) {
|
|
9886
|
-
|
|
9887
|
-
|
|
10155
|
+
const list = listeners[name] ?? [];
|
|
10156
|
+
listeners[name] = list;
|
|
10157
|
+
list.push(callback);
|
|
9888
10158
|
}
|
|
9889
10159
|
return () => {
|
|
9890
10160
|
for (const name of names) {
|
|
9891
|
-
|
|
9892
|
-
|
|
9893
|
-
});
|
|
10161
|
+
const list = listeners[name];
|
|
10162
|
+
this.listeners[name] = list.filter((fn) => fn !== callback);
|
|
9894
10163
|
}
|
|
9895
10164
|
};
|
|
9896
10165
|
}
|
|
@@ -9916,10 +10185,15 @@ class EventHandler {
|
|
|
9916
10185
|
* @param data - Event data.
|
|
9917
10186
|
*/
|
|
9918
10187
|
trigger(event, data) {
|
|
9919
|
-
const
|
|
9920
|
-
callbacks.forEach((listener) => {
|
|
10188
|
+
for (const listener of this.getCallbacks(event)) {
|
|
9921
10189
|
listener.call(null, event, data);
|
|
9922
|
-
}
|
|
10190
|
+
}
|
|
10191
|
+
}
|
|
10192
|
+
getCallbacks(event) {
|
|
10193
|
+
const { listeners } = this;
|
|
10194
|
+
const callbacks = listeners[event] ?? [];
|
|
10195
|
+
const global = listeners["*"] ?? [];
|
|
10196
|
+
return [...callbacks, ...global];
|
|
9923
10197
|
}
|
|
9924
10198
|
}
|
|
9925
10199
|
|
|
@@ -9989,10 +10263,10 @@ class Parser {
|
|
|
9989
10263
|
location: null
|
|
9990
10264
|
});
|
|
9991
10265
|
this.dom = new DOMTree({
|
|
9992
|
-
filename: source.filename
|
|
9993
|
-
offset: source.offset
|
|
9994
|
-
line: source.line
|
|
9995
|
-
column: source.column
|
|
10266
|
+
filename: source.filename,
|
|
10267
|
+
offset: source.offset,
|
|
10268
|
+
line: source.line,
|
|
10269
|
+
column: source.column,
|
|
9996
10270
|
size: 0
|
|
9997
10271
|
});
|
|
9998
10272
|
this.trigger("dom:load", {
|
|
@@ -10516,7 +10790,7 @@ function isThenable(value) {
|
|
|
10516
10790
|
return value && typeof value === "object" && "then" in value && typeof value.then === "function";
|
|
10517
10791
|
}
|
|
10518
10792
|
|
|
10519
|
-
const ruleIds = new Set(Object.keys(bundledRules
|
|
10793
|
+
const ruleIds = new Set(Object.keys(bundledRules));
|
|
10520
10794
|
function ruleExists(ruleId) {
|
|
10521
10795
|
return ruleIds.has(ruleId);
|
|
10522
10796
|
}
|
|
@@ -10605,9 +10879,7 @@ class Reporter {
|
|
|
10605
10879
|
valid: this.isValid(),
|
|
10606
10880
|
results: Object.keys(this.result).map((filePath) => {
|
|
10607
10881
|
const messages = Array.from(this.result[filePath], freeze).sort(messageSort);
|
|
10608
|
-
const source = (sources ?? []).find(
|
|
10609
|
-
(source2) => filePath === (source2.filename ?? "")
|
|
10610
|
-
);
|
|
10882
|
+
const source = (sources ?? []).find((source2) => filePath === source2.filename);
|
|
10611
10883
|
return {
|
|
10612
10884
|
filePath,
|
|
10613
10885
|
messages,
|
|
@@ -10675,7 +10947,7 @@ class Engine {
|
|
|
10675
10947
|
this.ParserClass = ParserClass;
|
|
10676
10948
|
const result = this.initPlugins(this.config);
|
|
10677
10949
|
this.availableRules = {
|
|
10678
|
-
...bundledRules
|
|
10950
|
+
...bundledRules,
|
|
10679
10951
|
...result.availableRules
|
|
10680
10952
|
};
|
|
10681
10953
|
}
|
|
@@ -11104,10 +11376,11 @@ class HtmlValidate {
|
|
|
11104
11376
|
* @returns Report output.
|
|
11105
11377
|
*/
|
|
11106
11378
|
async validateSource(input, configOverride) {
|
|
11107
|
-
const
|
|
11108
|
-
const
|
|
11379
|
+
const source = normalizeSource(input);
|
|
11380
|
+
const config = await this.getConfigFor(source.filename, configOverride);
|
|
11381
|
+
const transformedSource = config.transformSource(source);
|
|
11109
11382
|
const engine = new Engine(config, Parser);
|
|
11110
|
-
return engine.lint(
|
|
11383
|
+
return engine.lint(transformedSource);
|
|
11111
11384
|
}
|
|
11112
11385
|
/**
|
|
11113
11386
|
* Parse and validate HTML from [[Source]].
|
|
@@ -11117,10 +11390,11 @@ class HtmlValidate {
|
|
|
11117
11390
|
* @returns Report output.
|
|
11118
11391
|
*/
|
|
11119
11392
|
validateSourceSync(input, configOverride) {
|
|
11120
|
-
const
|
|
11121
|
-
const
|
|
11393
|
+
const source = normalizeSource(input);
|
|
11394
|
+
const config = this.getConfigForSync(source.filename, configOverride);
|
|
11395
|
+
const transformedSource = config.transformSource(source);
|
|
11122
11396
|
const engine = new Engine(config, Parser);
|
|
11123
|
-
return engine.lint(
|
|
11397
|
+
return engine.lint(transformedSource);
|
|
11124
11398
|
}
|
|
11125
11399
|
/**
|
|
11126
11400
|
* Parse and validate HTML from file.
|
|
@@ -11429,7 +11703,7 @@ class HtmlValidate {
|
|
|
11429
11703
|
}
|
|
11430
11704
|
|
|
11431
11705
|
const name = "html-validate";
|
|
11432
|
-
const version = "8.
|
|
11706
|
+
const version = "8.9.1";
|
|
11433
11707
|
const bugs = "https://gitlab.com/html-validate/html-validate/issues/new";
|
|
11434
11708
|
|
|
11435
11709
|
function definePlugin(plugin) {
|