html-validate 7.4.1 → 7.6.0
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/browser.d.ts +1 -1
- package/dist/cjs/browser.js +5 -0
- package/dist/cjs/browser.js.map +1 -1
- package/dist/cjs/cli.js +3 -3
- package/dist/cjs/cli.js.map +1 -1
- package/dist/cjs/core.d.ts +52 -1
- package/dist/cjs/core.js +369 -132
- package/dist/cjs/core.js.map +1 -1
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.js +5 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/jest.d.ts +1 -1
- package/dist/cjs/test-utils.d.ts +1 -1
- package/dist/es/browser.d.ts +1 -1
- package/dist/es/browser.js +1 -1
- package/dist/es/cli.js +4 -4
- package/dist/es/cli.js.map +1 -1
- package/dist/es/core.d.ts +52 -1
- package/dist/es/core.js +360 -125
- package/dist/es/core.js.map +1 -1
- package/dist/es/html-validate.js +1 -1
- package/dist/es/index.d.ts +2 -2
- package/dist/es/index.js +1 -1
- package/dist/es/jest-lib.js +1 -1
- package/dist/es/jest.d.ts +1 -1
- package/dist/es/test-utils.d.ts +1 -1
- package/package.json +11 -11
package/dist/cjs/core.js
CHANGED
|
@@ -1767,9 +1767,19 @@ function factory(name, context) {
|
|
|
1767
1767
|
function stripslashes(value) {
|
|
1768
1768
|
return value.replace(/\\(.)/g, "$1");
|
|
1769
1769
|
}
|
|
1770
|
+
/**
|
|
1771
|
+
* @internal
|
|
1772
|
+
*/
|
|
1770
1773
|
function escapeSelectorComponent(text) {
|
|
1771
1774
|
return text.toString().replace(/([^a-z0-9_-])/gi, "\\$1");
|
|
1772
1775
|
}
|
|
1776
|
+
/**
|
|
1777
|
+
* @internal
|
|
1778
|
+
*/
|
|
1779
|
+
function generateIdSelector(id) {
|
|
1780
|
+
const escaped = escapeSelectorComponent(id);
|
|
1781
|
+
return escaped.match(/^\d/) ? `[id="${escaped}"]` : `#${escaped}`;
|
|
1782
|
+
}
|
|
1773
1783
|
/**
|
|
1774
1784
|
* Returns true if the character is a delimiter for different kinds of selectors:
|
|
1775
1785
|
*
|
|
@@ -2146,6 +2156,25 @@ class HtmlElement extends DOMNode {
|
|
|
2146
2156
|
return `<${this.tagName}>`;
|
|
2147
2157
|
}
|
|
2148
2158
|
}
|
|
2159
|
+
/**
|
|
2160
|
+
* Get list of IDs referenced by `aria-labelledby`.
|
|
2161
|
+
*
|
|
2162
|
+
* If the attribute is unset or empty this getter returns null.
|
|
2163
|
+
* If the attribute is dynamic the original {@link DynamicValue} is returned.
|
|
2164
|
+
*
|
|
2165
|
+
* @public
|
|
2166
|
+
*/
|
|
2167
|
+
get ariaLabelledby() {
|
|
2168
|
+
const attr = this.getAttribute("aria-labelledby");
|
|
2169
|
+
if (!attr || !attr.value) {
|
|
2170
|
+
return null;
|
|
2171
|
+
}
|
|
2172
|
+
if (attr.value instanceof DynamicValue) {
|
|
2173
|
+
return attr.value;
|
|
2174
|
+
}
|
|
2175
|
+
const list = new DOMTokenList(attr.value, attr.valueLocation);
|
|
2176
|
+
return list.length ? Array.from(list) : null;
|
|
2177
|
+
}
|
|
2149
2178
|
/**
|
|
2150
2179
|
* Similar to childNodes but only elements.
|
|
2151
2180
|
*/
|
|
@@ -2187,8 +2216,7 @@ class HtmlElement extends DOMNode {
|
|
|
2187
2216
|
for (let cur = this; cur.parent; cur = cur.parent) {
|
|
2188
2217
|
/* if a unique id is present, use it and short-circuit */
|
|
2189
2218
|
if (cur.id) {
|
|
2190
|
-
const
|
|
2191
|
-
const selector = escaped.match(/^\d/) ? `[id="${escaped}"]` : `#${escaped}`;
|
|
2219
|
+
const selector = generateIdSelector(cur.id);
|
|
2192
2220
|
const matches = root.querySelectorAll(selector);
|
|
2193
2221
|
if (matches.length === 1) {
|
|
2194
2222
|
parts.push(selector);
|
|
@@ -3194,7 +3222,7 @@ var TRANSFORMER_API;
|
|
|
3194
3222
|
/** @public */
|
|
3195
3223
|
const name = "html-validate";
|
|
3196
3224
|
/** @public */
|
|
3197
|
-
const version = "7.
|
|
3225
|
+
const version = "7.6.0";
|
|
3198
3226
|
/** @public */
|
|
3199
3227
|
const homepage = "https://html-validate.org";
|
|
3200
3228
|
/** @public */
|
|
@@ -5417,13 +5445,122 @@ class ElementPermittedOrder extends Rule {
|
|
|
5417
5445
|
}
|
|
5418
5446
|
}
|
|
5419
5447
|
|
|
5420
|
-
const
|
|
5421
|
-
|
|
5448
|
+
const ARIA_HIDDEN_CACHE = Symbol(isAriaHidden.name);
|
|
5449
|
+
const HTML_HIDDEN_CACHE = Symbol(isHTMLHidden.name);
|
|
5450
|
+
const ROLE_PRESENTATION_CACHE = Symbol(isPresentation.name);
|
|
5451
|
+
/**
|
|
5452
|
+
* Tests if this element is present in the accessibility tree.
|
|
5453
|
+
*
|
|
5454
|
+
* In practice it tests whenever the element or its parents has
|
|
5455
|
+
* `role="presentation"` or `aria-hidden="false"`. Dynamic values counts as
|
|
5456
|
+
* visible since the element might be in the visibility tree sometimes.
|
|
5457
|
+
*/
|
|
5458
|
+
function inAccessibilityTree(node) {
|
|
5459
|
+
return !isAriaHidden(node) && !isPresentation(node);
|
|
5460
|
+
}
|
|
5461
|
+
function isAriaHiddenImpl(node) {
|
|
5462
|
+
const isHidden = (node) => {
|
|
5463
|
+
const ariaHidden = node.getAttribute("aria-hidden");
|
|
5464
|
+
return Boolean(ariaHidden && ariaHidden.value === "true");
|
|
5465
|
+
};
|
|
5466
|
+
return {
|
|
5467
|
+
byParent: node.parent ? isAriaHidden(node.parent) : false,
|
|
5468
|
+
bySelf: isHidden(node),
|
|
5469
|
+
};
|
|
5470
|
+
}
|
|
5471
|
+
function isAriaHidden(node, details) {
|
|
5472
|
+
const cached = node.cacheGet(ARIA_HIDDEN_CACHE);
|
|
5473
|
+
if (cached) {
|
|
5474
|
+
return details ? cached : cached.byParent || cached.bySelf;
|
|
5475
|
+
}
|
|
5476
|
+
const result = node.cacheSet(ARIA_HIDDEN_CACHE, isAriaHiddenImpl(node));
|
|
5477
|
+
return details ? result : result.byParent || result.bySelf;
|
|
5478
|
+
}
|
|
5479
|
+
function isHTMLHiddenImpl(node) {
|
|
5480
|
+
const isHidden = (node) => {
|
|
5481
|
+
const hidden = node.getAttribute("hidden");
|
|
5482
|
+
return hidden !== null && hidden.isStatic;
|
|
5483
|
+
};
|
|
5484
|
+
return {
|
|
5485
|
+
byParent: node.parent ? isHTMLHidden(node.parent) : false,
|
|
5486
|
+
bySelf: isHidden(node),
|
|
5487
|
+
};
|
|
5488
|
+
}
|
|
5489
|
+
function isHTMLHidden(node, details) {
|
|
5490
|
+
const cached = node.cacheGet(HTML_HIDDEN_CACHE);
|
|
5491
|
+
if (cached) {
|
|
5492
|
+
return details ? cached : cached.byParent || cached.bySelf;
|
|
5493
|
+
}
|
|
5494
|
+
const result = node.cacheSet(HTML_HIDDEN_CACHE, isHTMLHiddenImpl(node));
|
|
5495
|
+
return details ? result : result.byParent || result.bySelf;
|
|
5496
|
+
}
|
|
5497
|
+
/**
|
|
5498
|
+
* Tests if this element or a parent element has role="presentation".
|
|
5499
|
+
*
|
|
5500
|
+
* Dynamic values yields `false` just as if the attribute wasn't present.
|
|
5501
|
+
*/
|
|
5502
|
+
function isPresentation(node) {
|
|
5503
|
+
if (node.cacheExists(ROLE_PRESENTATION_CACHE)) {
|
|
5504
|
+
return Boolean(node.cacheGet(ROLE_PRESENTATION_CACHE));
|
|
5505
|
+
}
|
|
5506
|
+
let cur = node;
|
|
5507
|
+
do {
|
|
5508
|
+
const role = cur.getAttribute("role");
|
|
5509
|
+
/* role="presentation" */
|
|
5510
|
+
if (role && role.value === "presentation") {
|
|
5511
|
+
return cur.cacheSet(ROLE_PRESENTATION_CACHE, true);
|
|
5512
|
+
}
|
|
5513
|
+
/* sanity check: break if no parent is present, normally not an issue as the
|
|
5514
|
+
* root element should be found first */
|
|
5515
|
+
if (!cur.parent) {
|
|
5516
|
+
break;
|
|
5517
|
+
}
|
|
5518
|
+
/* check parents */
|
|
5519
|
+
cur = cur.parent;
|
|
5520
|
+
} while (!cur.isRootElement());
|
|
5521
|
+
return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
|
|
5522
|
+
}
|
|
5523
|
+
|
|
5524
|
+
const cachePrefix = classifyNodeText.name;
|
|
5525
|
+
const HTML_CACHE_KEY = Symbol(`${cachePrefix}|html`);
|
|
5526
|
+
const A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y`);
|
|
5527
|
+
const IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY = Symbol(`${cachePrefix}|html|ignore-hidden-root`);
|
|
5528
|
+
const IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY = Symbol(`${cachePrefix}|a11y|ignore-hidden-root`);
|
|
5529
|
+
/**
|
|
5530
|
+
* @public
|
|
5531
|
+
*/
|
|
5532
|
+
exports.TextClassification = void 0;
|
|
5422
5533
|
(function (TextClassification) {
|
|
5423
5534
|
TextClassification[TextClassification["EMPTY_TEXT"] = 0] = "EMPTY_TEXT";
|
|
5424
5535
|
TextClassification[TextClassification["DYNAMIC_TEXT"] = 1] = "DYNAMIC_TEXT";
|
|
5425
5536
|
TextClassification[TextClassification["STATIC_TEXT"] = 2] = "STATIC_TEXT";
|
|
5426
|
-
})(TextClassification || (TextClassification = {}));
|
|
5537
|
+
})(exports.TextClassification || (exports.TextClassification = {}));
|
|
5538
|
+
function getCachekey(options = {}) {
|
|
5539
|
+
const { accessible = false, ignoreHiddenRoot = false } = options;
|
|
5540
|
+
if (accessible && ignoreHiddenRoot) {
|
|
5541
|
+
return IGNORE_HIDDEN_ROOT_A11Y_CACHE_KEY;
|
|
5542
|
+
}
|
|
5543
|
+
else if (ignoreHiddenRoot) {
|
|
5544
|
+
return IGNORE_HIDDEN_ROOT_HTML_CACHE_KEY;
|
|
5545
|
+
}
|
|
5546
|
+
else if (accessible) {
|
|
5547
|
+
return A11Y_CACHE_KEY;
|
|
5548
|
+
}
|
|
5549
|
+
else {
|
|
5550
|
+
return HTML_CACHE_KEY;
|
|
5551
|
+
}
|
|
5552
|
+
}
|
|
5553
|
+
/* While I cannot find a reference about this in the standard the <select>
|
|
5554
|
+
* element kinda acts as if there is no text content, most particularly it
|
|
5555
|
+
* doesn't receive and accessible name. The `.textContent` property does
|
|
5556
|
+
* however include the <option> childrens text. But for the sake of the
|
|
5557
|
+
* validator it is probably best if the classification acts as if there is no
|
|
5558
|
+
* text as I think that is what is expected of the return values. Might have
|
|
5559
|
+
* to revisit this at some point or if someone could clarify what section of
|
|
5560
|
+
* the standard deals with this. */
|
|
5561
|
+
function isSpecialEmpty(node) {
|
|
5562
|
+
return node.is("select") || node.is("textarea");
|
|
5563
|
+
}
|
|
5427
5564
|
/**
|
|
5428
5565
|
* Checks text content of an element.
|
|
5429
5566
|
*
|
|
@@ -5431,33 +5568,54 @@ var TextClassification;
|
|
|
5431
5568
|
* ignored.
|
|
5432
5569
|
*
|
|
5433
5570
|
* If any text is dynamic `TextClassification.DYNAMIC_TEXT` is returned.
|
|
5571
|
+
*
|
|
5572
|
+
* @public
|
|
5434
5573
|
*/
|
|
5435
|
-
function classifyNodeText(node) {
|
|
5436
|
-
|
|
5437
|
-
|
|
5574
|
+
function classifyNodeText(node, options = {}) {
|
|
5575
|
+
const { accessible = false, ignoreHiddenRoot = false } = options;
|
|
5576
|
+
const cacheKey = getCachekey(options);
|
|
5577
|
+
if (node.cacheExists(cacheKey)) {
|
|
5578
|
+
return node.cacheGet(cacheKey);
|
|
5579
|
+
}
|
|
5580
|
+
if (!ignoreHiddenRoot && isHTMLHidden(node)) {
|
|
5581
|
+
return node.cacheSet(cacheKey, exports.TextClassification.EMPTY_TEXT);
|
|
5582
|
+
}
|
|
5583
|
+
if (!ignoreHiddenRoot && accessible && isAriaHidden(node)) {
|
|
5584
|
+
return node.cacheSet(cacheKey, exports.TextClassification.EMPTY_TEXT);
|
|
5585
|
+
}
|
|
5586
|
+
if (isSpecialEmpty(node)) {
|
|
5587
|
+
return node.cacheSet(cacheKey, exports.TextClassification.EMPTY_TEXT);
|
|
5438
5588
|
}
|
|
5439
|
-
const text = findTextNodes(node
|
|
5589
|
+
const text = findTextNodes(node, {
|
|
5590
|
+
...options,
|
|
5591
|
+
ignoreHiddenRoot: false,
|
|
5592
|
+
});
|
|
5440
5593
|
/* if any text is dynamic classify as dynamic */
|
|
5441
5594
|
if (text.some((cur) => cur.isDynamic)) {
|
|
5442
|
-
return node.cacheSet(
|
|
5595
|
+
return node.cacheSet(cacheKey, exports.TextClassification.DYNAMIC_TEXT);
|
|
5443
5596
|
}
|
|
5444
5597
|
/* if any text has non-whitespace character classify as static */
|
|
5445
5598
|
if (text.some((cur) => cur.textContent.match(/\S/) !== null)) {
|
|
5446
|
-
return node.cacheSet(
|
|
5599
|
+
return node.cacheSet(cacheKey, exports.TextClassification.STATIC_TEXT);
|
|
5447
5600
|
}
|
|
5448
5601
|
/* default to empty */
|
|
5449
|
-
return node.cacheSet(
|
|
5602
|
+
return node.cacheSet(cacheKey, exports.TextClassification.EMPTY_TEXT);
|
|
5450
5603
|
}
|
|
5451
|
-
function findTextNodes(node) {
|
|
5604
|
+
function findTextNodes(node, options) {
|
|
5605
|
+
const { accessible = false } = options;
|
|
5452
5606
|
let text = [];
|
|
5453
5607
|
for (const child of node.childNodes) {
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5608
|
+
if (isTextNode(child)) {
|
|
5609
|
+
text.push(child);
|
|
5610
|
+
}
|
|
5611
|
+
else if (isElementNode(child)) {
|
|
5612
|
+
if (isHTMLHidden(child, true).bySelf) {
|
|
5613
|
+
continue;
|
|
5614
|
+
}
|
|
5615
|
+
if (accessible && isAriaHidden(child, true).bySelf) {
|
|
5616
|
+
continue;
|
|
5617
|
+
}
|
|
5618
|
+
text = text.concat(findTextNodes(child, options));
|
|
5461
5619
|
}
|
|
5462
5620
|
}
|
|
5463
5621
|
return text;
|
|
@@ -5733,6 +5891,17 @@ class ElementRequiredContent extends Rule {
|
|
|
5733
5891
|
}
|
|
5734
5892
|
|
|
5735
5893
|
const selector = ["h1", "h2", "h3", "h4", "h5", "h6"].join(",");
|
|
5894
|
+
function hasImgAltText$1(node) {
|
|
5895
|
+
if (node.is("img")) {
|
|
5896
|
+
return hasAltText(node);
|
|
5897
|
+
}
|
|
5898
|
+
else if (node.is("svg")) {
|
|
5899
|
+
return node.textContent.trim() !== "";
|
|
5900
|
+
}
|
|
5901
|
+
else {
|
|
5902
|
+
return false;
|
|
5903
|
+
}
|
|
5904
|
+
}
|
|
5736
5905
|
class EmptyHeading extends Rule {
|
|
5737
5906
|
documentation() {
|
|
5738
5907
|
return {
|
|
@@ -5744,19 +5913,28 @@ class EmptyHeading extends Rule {
|
|
|
5744
5913
|
this.on("dom:ready", ({ document }) => {
|
|
5745
5914
|
const headings = document.querySelectorAll(selector);
|
|
5746
5915
|
for (const heading of headings) {
|
|
5747
|
-
|
|
5748
|
-
case TextClassification.DYNAMIC_TEXT:
|
|
5749
|
-
case TextClassification.STATIC_TEXT:
|
|
5750
|
-
/* have some text content, consider ok */
|
|
5751
|
-
break;
|
|
5752
|
-
case TextClassification.EMPTY_TEXT:
|
|
5753
|
-
/* no content or whitespace only */
|
|
5754
|
-
this.report(heading, `<${heading.tagName}> cannot be empty, must have text content`);
|
|
5755
|
-
break;
|
|
5756
|
-
}
|
|
5916
|
+
this.validateHeading(heading);
|
|
5757
5917
|
}
|
|
5758
5918
|
});
|
|
5759
5919
|
}
|
|
5920
|
+
validateHeading(heading) {
|
|
5921
|
+
const images = heading.querySelectorAll("img, svg");
|
|
5922
|
+
for (const child of images) {
|
|
5923
|
+
if (hasImgAltText$1(child)) {
|
|
5924
|
+
return;
|
|
5925
|
+
}
|
|
5926
|
+
}
|
|
5927
|
+
switch (classifyNodeText(heading)) {
|
|
5928
|
+
case exports.TextClassification.DYNAMIC_TEXT:
|
|
5929
|
+
case exports.TextClassification.STATIC_TEXT:
|
|
5930
|
+
/* have some text content, consider ok */
|
|
5931
|
+
break;
|
|
5932
|
+
case exports.TextClassification.EMPTY_TEXT:
|
|
5933
|
+
/* no content or whitespace only */
|
|
5934
|
+
this.report(heading, `<${heading.tagName}> cannot be empty, must have text content`);
|
|
5935
|
+
break;
|
|
5936
|
+
}
|
|
5937
|
+
}
|
|
5760
5938
|
}
|
|
5761
5939
|
|
|
5762
5940
|
class EmptyTitle extends Rule {
|
|
@@ -5779,11 +5957,11 @@ class EmptyTitle extends Rule {
|
|
|
5779
5957
|
if (node.tagName !== "title")
|
|
5780
5958
|
return;
|
|
5781
5959
|
switch (classifyNodeText(node)) {
|
|
5782
|
-
case TextClassification.DYNAMIC_TEXT:
|
|
5783
|
-
case TextClassification.STATIC_TEXT:
|
|
5960
|
+
case exports.TextClassification.DYNAMIC_TEXT:
|
|
5961
|
+
case exports.TextClassification.STATIC_TEXT:
|
|
5784
5962
|
/* have some text content, consider ok */
|
|
5785
5963
|
break;
|
|
5786
|
-
case TextClassification.EMPTY_TEXT:
|
|
5964
|
+
case exports.TextClassification.EMPTY_TEXT:
|
|
5787
5965
|
/* no content or whitespace only */
|
|
5788
5966
|
{
|
|
5789
5967
|
const message = `<${node.tagName}> cannot be empty, must have text content`;
|
|
@@ -6177,104 +6355,148 @@ class InputAttributes extends Rule {
|
|
|
6177
6355
|
}
|
|
6178
6356
|
}
|
|
6179
6357
|
|
|
6180
|
-
const
|
|
6181
|
-
|
|
6182
|
-
const
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
*/
|
|
6190
|
-
function inAccessibilityTree(node) {
|
|
6191
|
-
return !isAriaHidden(node) && !isPresentation(node);
|
|
6358
|
+
const HAS_ACCESSIBLE_TEXT_CACHE = Symbol(hasAccessibleName.name);
|
|
6359
|
+
function isHidden(node, context) {
|
|
6360
|
+
const { reference } = context;
|
|
6361
|
+
if (reference && reference.isSameNode(node)) {
|
|
6362
|
+
return false;
|
|
6363
|
+
}
|
|
6364
|
+
else {
|
|
6365
|
+
return isHTMLHidden(node) || !inAccessibilityTree(node);
|
|
6366
|
+
}
|
|
6192
6367
|
}
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
* Dynamic values yields `false` since the element will conditionally be in the
|
|
6197
|
-
* accessibility tree and must fulfill it's conditions.
|
|
6198
|
-
*/
|
|
6199
|
-
function isAriaHidden(node) {
|
|
6200
|
-
if (node.cacheExists(ARIA_HIDDEN_CACHE)) {
|
|
6201
|
-
return Boolean(node.cacheGet(ARIA_HIDDEN_CACHE));
|
|
6368
|
+
function hasImgAltText(node, context) {
|
|
6369
|
+
if (node.is("img")) {
|
|
6370
|
+
return hasAltText(node);
|
|
6202
6371
|
}
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
if (!cur.parent) {
|
|
6213
|
-
break;
|
|
6372
|
+
else if (node.is("svg")) {
|
|
6373
|
+
return node.textContent.trim() !== "";
|
|
6374
|
+
}
|
|
6375
|
+
else {
|
|
6376
|
+
for (const img of node.querySelectorAll("img, svg")) {
|
|
6377
|
+
const hasName = hasAccessibleNameImpl(img, context);
|
|
6378
|
+
if (hasName) {
|
|
6379
|
+
return true;
|
|
6380
|
+
}
|
|
6214
6381
|
}
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6382
|
+
return false;
|
|
6383
|
+
}
|
|
6384
|
+
}
|
|
6385
|
+
function hasLabel(node) {
|
|
6386
|
+
var _a;
|
|
6387
|
+
const value = (_a = node.getAttributeValue("aria-label")) !== null && _a !== void 0 ? _a : "";
|
|
6388
|
+
return Boolean(value.trim());
|
|
6389
|
+
}
|
|
6390
|
+
function isLabelledby(node, context) {
|
|
6391
|
+
const { document, reference } = context;
|
|
6392
|
+
/* if we already have resolved one level of reference we don't resolve another
|
|
6393
|
+
* level (as per accname step 2B) */
|
|
6394
|
+
if (reference) {
|
|
6395
|
+
return false;
|
|
6396
|
+
}
|
|
6397
|
+
const ariaLabelledby = node.ariaLabelledby;
|
|
6398
|
+
/* consider dynamic aria-labelledby as having a name as we cannot resolve it
|
|
6399
|
+
* so no way to prove correctness */
|
|
6400
|
+
if (ariaLabelledby instanceof DynamicValue) {
|
|
6401
|
+
return true;
|
|
6402
|
+
}
|
|
6403
|
+
/* ignore elements without aria-labelledby */
|
|
6404
|
+
if (ariaLabelledby === null) {
|
|
6405
|
+
return false;
|
|
6406
|
+
}
|
|
6407
|
+
return ariaLabelledby.some((id) => {
|
|
6408
|
+
const selector = generateIdSelector(id);
|
|
6409
|
+
return document.querySelectorAll(selector).some((child) => {
|
|
6410
|
+
return hasAccessibleNameImpl(child, {
|
|
6411
|
+
document,
|
|
6412
|
+
reference: child,
|
|
6413
|
+
});
|
|
6414
|
+
});
|
|
6415
|
+
});
|
|
6219
6416
|
}
|
|
6220
6417
|
/**
|
|
6221
|
-
*
|
|
6418
|
+
* This algorithm is based on ["Accessible Name and Description Computation
|
|
6419
|
+
* 1.2"][accname] with some exceptions:
|
|
6420
|
+
*
|
|
6421
|
+
* It doesn't compute the actual name but only the presence of one, e.g. if a
|
|
6422
|
+
* non-empty flat string is present the algorithm terminates with a positive
|
|
6423
|
+
* result.
|
|
6222
6424
|
*
|
|
6223
|
-
*
|
|
6224
|
-
*
|
|
6425
|
+
* It takes some optimization shortcuts such as starting with step F as it
|
|
6426
|
+
* would be more common usage and as there is no actual name being computed
|
|
6427
|
+
* the order wont matter.
|
|
6428
|
+
*
|
|
6429
|
+
* [accname]: https://w3c.github.io/accname
|
|
6225
6430
|
*/
|
|
6226
|
-
function
|
|
6227
|
-
|
|
6228
|
-
|
|
6431
|
+
function hasAccessibleNameImpl(current, context) {
|
|
6432
|
+
const { reference } = context;
|
|
6433
|
+
/* if this element is hidden (see function for exceptions) it does not have an accessible name */
|
|
6434
|
+
if (isHidden(current, context)) {
|
|
6435
|
+
return false;
|
|
6229
6436
|
}
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6437
|
+
/* special case: when this element is directly referenced by aria-labelledby
|
|
6438
|
+
* we ignore `hidden` */
|
|
6439
|
+
const ignoreHiddenRoot = Boolean(reference && reference.isSameNode(current));
|
|
6440
|
+
const text = classifyNodeText(current, { accessible: true, ignoreHiddenRoot });
|
|
6441
|
+
if (text !== exports.TextClassification.EMPTY_TEXT) {
|
|
6442
|
+
return true;
|
|
6443
|
+
}
|
|
6444
|
+
if (hasImgAltText(current, context)) {
|
|
6445
|
+
return true;
|
|
6446
|
+
}
|
|
6447
|
+
if (hasLabel(current)) {
|
|
6448
|
+
return true;
|
|
6449
|
+
}
|
|
6450
|
+
if (isLabelledby(current, context)) {
|
|
6451
|
+
return true;
|
|
6452
|
+
}
|
|
6453
|
+
return false;
|
|
6246
6454
|
}
|
|
6247
6455
|
/**
|
|
6248
|
-
*
|
|
6456
|
+
* Returns `true` if the element has an accessible name.
|
|
6249
6457
|
*
|
|
6250
|
-
*
|
|
6458
|
+
* It does not yet consider if the elements role prohibits naming, e.g. a `<p>`
|
|
6459
|
+
* element will still show up as having an accessible name.
|
|
6460
|
+
*
|
|
6461
|
+
* @public
|
|
6462
|
+
* @param document - Document element.
|
|
6463
|
+
* @param current - The element to get accessible name for
|
|
6464
|
+
* @returns `true` if the element has an accessible name.
|
|
6251
6465
|
*/
|
|
6252
|
-
function
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
}
|
|
6263
|
-
/* sanity check: break if no parent is present, normally not an issue as the
|
|
6264
|
-
* root element should be found first */
|
|
6265
|
-
if (!cur.parent) {
|
|
6266
|
-
break;
|
|
6267
|
-
}
|
|
6268
|
-
/* check parents */
|
|
6269
|
-
cur = cur.parent;
|
|
6270
|
-
} while (!cur.isRootElement());
|
|
6271
|
-
return node.cacheSet(ROLE_PRESENTATION_CACHE, false);
|
|
6466
|
+
function hasAccessibleName(document, current) {
|
|
6467
|
+
/* istanbul ignore next: we're not testing cache */
|
|
6468
|
+
if (current.cacheExists(HAS_ACCESSIBLE_TEXT_CACHE)) {
|
|
6469
|
+
return Boolean(current.cacheGet(HAS_ACCESSIBLE_TEXT_CACHE));
|
|
6470
|
+
}
|
|
6471
|
+
const result = hasAccessibleNameImpl(current, {
|
|
6472
|
+
document,
|
|
6473
|
+
reference: null,
|
|
6474
|
+
});
|
|
6475
|
+
return current.cacheSet(HAS_ACCESSIBLE_TEXT_CACHE, result);
|
|
6272
6476
|
}
|
|
6273
6477
|
|
|
6478
|
+
function isIgnored(node) {
|
|
6479
|
+
var _a;
|
|
6480
|
+
if (node.is("input")) {
|
|
6481
|
+
const type = (_a = node.getAttributeValue("type")) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
6482
|
+
const ignored = ["hidden", "submit", "reset", "button"];
|
|
6483
|
+
return Boolean(type && ignored.includes(type));
|
|
6484
|
+
}
|
|
6485
|
+
return false;
|
|
6486
|
+
}
|
|
6274
6487
|
class InputMissingLabel extends Rule {
|
|
6275
6488
|
documentation() {
|
|
6276
6489
|
return {
|
|
6277
|
-
description:
|
|
6490
|
+
description: [
|
|
6491
|
+
"Each form element must have an a label or accessible name.",
|
|
6492
|
+
'Typically this is implemented using a `<label for="..">` element describing the purpose of the form element.',
|
|
6493
|
+
"",
|
|
6494
|
+
"This can be resolved in one of the following ways:",
|
|
6495
|
+
"",
|
|
6496
|
+
' - Use an associated `<label for="..">` element.',
|
|
6497
|
+
" - Use a nested `<label>` as parent element.",
|
|
6498
|
+
" - Use `aria-label` or `aria-labelledby` attributes.",
|
|
6499
|
+
].join("\n"),
|
|
6278
6500
|
url: ruleDocumentationUrl("@/rules/input-missing-label.ts"),
|
|
6279
6501
|
};
|
|
6280
6502
|
}
|
|
@@ -6287,38 +6509,48 @@ class InputMissingLabel extends Rule {
|
|
|
6287
6509
|
});
|
|
6288
6510
|
}
|
|
6289
6511
|
validateInput(root, elem) {
|
|
6290
|
-
var _a;
|
|
6291
6512
|
if (isHTMLHidden(elem) || isAriaHidden(elem)) {
|
|
6292
6513
|
return;
|
|
6293
6514
|
}
|
|
6294
6515
|
/* hidden, submit, reset or button should not have label */
|
|
6295
|
-
if (elem
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
}
|
|
6516
|
+
if (isIgnored(elem)) {
|
|
6517
|
+
return;
|
|
6518
|
+
}
|
|
6519
|
+
if (hasAccessibleName(root, elem)) {
|
|
6520
|
+
return;
|
|
6301
6521
|
}
|
|
6302
6522
|
let label = [];
|
|
6303
6523
|
/* try to find label by id */
|
|
6304
6524
|
if ((label = findLabelById(root, elem.id)).length > 0) {
|
|
6305
|
-
this.validateLabel(elem, label);
|
|
6525
|
+
this.validateLabel(root, elem, label);
|
|
6306
6526
|
return;
|
|
6307
6527
|
}
|
|
6308
6528
|
/* try to find parent label (input nested in label) */
|
|
6309
6529
|
if ((label = findLabelByParent(elem)).length > 0) {
|
|
6310
|
-
this.validateLabel(elem, label);
|
|
6530
|
+
this.validateLabel(root, elem, label);
|
|
6311
6531
|
return;
|
|
6312
6532
|
}
|
|
6313
|
-
|
|
6533
|
+
if (elem.hasAttribute("aria-label")) {
|
|
6534
|
+
this.report(elem, `<${elem.tagName}> element has aria-label but label has no text`);
|
|
6535
|
+
}
|
|
6536
|
+
else if (elem.hasAttribute("aria-labelledby")) {
|
|
6537
|
+
this.report(elem, `<${elem.tagName}> element has aria-labelledby but referenced element has no text`);
|
|
6538
|
+
}
|
|
6539
|
+
else {
|
|
6540
|
+
this.report(elem, `<${elem.tagName}> element does not have a <label>`);
|
|
6541
|
+
}
|
|
6314
6542
|
}
|
|
6315
6543
|
/**
|
|
6316
6544
|
* Reports error if none of the labels are accessible.
|
|
6317
6545
|
*/
|
|
6318
|
-
validateLabel(elem, labels) {
|
|
6546
|
+
validateLabel(root, elem, labels) {
|
|
6319
6547
|
const visible = labels.filter(isVisible);
|
|
6320
6548
|
if (visible.length === 0) {
|
|
6321
|
-
this.report(elem, `<${elem.tagName}> element has label but <label> element is hidden`);
|
|
6549
|
+
this.report(elem, `<${elem.tagName}> element has <label> but <label> element is hidden`);
|
|
6550
|
+
return;
|
|
6551
|
+
}
|
|
6552
|
+
if (!labels.some((label) => hasAccessibleName(root, label))) {
|
|
6553
|
+
this.report(elem, `<${elem.tagName}> element has <label> but <label> has no text`);
|
|
6322
6554
|
}
|
|
6323
6555
|
}
|
|
6324
6556
|
}
|
|
@@ -8038,7 +8270,7 @@ class TextContent extends Rule {
|
|
|
8038
8270
|
* Validate element has empty text (inter-element whitespace is not considered text)
|
|
8039
8271
|
*/
|
|
8040
8272
|
validateNone(node) {
|
|
8041
|
-
if (classifyNodeText(node) === TextClassification.EMPTY_TEXT) {
|
|
8273
|
+
if (classifyNodeText(node) === exports.TextClassification.EMPTY_TEXT) {
|
|
8042
8274
|
return;
|
|
8043
8275
|
}
|
|
8044
8276
|
this.reportError(node, node.meta, `${node.annotatedName} must not have text content`);
|
|
@@ -8047,7 +8279,7 @@ class TextContent extends Rule {
|
|
|
8047
8279
|
* Validate element has any text (inter-element whitespace is not considered text)
|
|
8048
8280
|
*/
|
|
8049
8281
|
validateRequired(node) {
|
|
8050
|
-
if (classifyNodeText(node) !== TextClassification.EMPTY_TEXT) {
|
|
8282
|
+
if (classifyNodeText(node) !== exports.TextClassification.EMPTY_TEXT) {
|
|
8051
8283
|
return;
|
|
8052
8284
|
}
|
|
8053
8285
|
this.reportError(node, node.meta, `${node.annotatedName} must have text content`);
|
|
@@ -10152,7 +10384,7 @@ class H30 extends Rule {
|
|
|
10152
10384
|
}
|
|
10153
10385
|
/* check if text content is present (or dynamic) */
|
|
10154
10386
|
const textClassification = classifyNodeText(link);
|
|
10155
|
-
if (textClassification !== TextClassification.EMPTY_TEXT) {
|
|
10387
|
+
if (textClassification !== exports.TextClassification.EMPTY_TEXT) {
|
|
10156
10388
|
continue;
|
|
10157
10389
|
}
|
|
10158
10390
|
/* check if image with alt-text is present */
|
|
@@ -10749,7 +10981,10 @@ function mergeInternal(base, rhs) {
|
|
|
10749
10981
|
}
|
|
10750
10982
|
return dst;
|
|
10751
10983
|
}
|
|
10752
|
-
|
|
10984
|
+
/**
|
|
10985
|
+
* @internal
|
|
10986
|
+
*/
|
|
10987
|
+
function configDataFromFile(filename) {
|
|
10753
10988
|
let json;
|
|
10754
10989
|
try {
|
|
10755
10990
|
/* load using require as it can process both js and json */
|
|
@@ -10837,7 +11072,7 @@ class Config {
|
|
|
10837
11072
|
* `html-validate:recommended`.
|
|
10838
11073
|
*/
|
|
10839
11074
|
static fromFile(filename) {
|
|
10840
|
-
const configdata =
|
|
11075
|
+
const configdata = configDataFromFile(filename);
|
|
10841
11076
|
return Config.fromObject(configdata, filename);
|
|
10842
11077
|
}
|
|
10843
11078
|
/**
|
|
@@ -13144,8 +13379,10 @@ exports.TextNode = TextNode;
|
|
|
13144
13379
|
exports.UserError = UserError;
|
|
13145
13380
|
exports.WrappedError = WrappedError;
|
|
13146
13381
|
exports.bugs = bugs;
|
|
13382
|
+
exports.classifyNodeText = classifyNodeText;
|
|
13147
13383
|
exports.codeframe = codeframe;
|
|
13148
13384
|
exports.compatibilityCheck = compatibilityCheck;
|
|
13385
|
+
exports.configDataFromFile = configDataFromFile;
|
|
13149
13386
|
exports.ensureError = ensureError;
|
|
13150
13387
|
exports.getFormatter = getFormatter;
|
|
13151
13388
|
exports.legacyRequire = legacyRequire;
|