@markuplint/rules 5.4.0 → 5.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/CHANGELOG.md +34 -0
- package/CLAUDE.md +4 -3
- package/lib/aria-prop-requires-role/index.js +2 -2
- package/lib/attr-eligibility.d.ts +21 -0
- package/lib/attr-eligibility.js +86 -6
- package/lib/attr-order/index.js +2 -1
- package/lib/attr-value-quotes/index.js +2 -2
- package/lib/consistent-table-row-length/index.js +1 -1
- package/lib/create-message.js +6 -5
- package/lib/element-supports-aria-prop/index.js +2 -2
- package/lib/form-attr-references-form/index.js +3 -3
- package/lib/helpers.d.ts +96 -5
- package/lib/helpers.js +187 -29
- package/lib/input-list-references-datalist/index.js +3 -3
- package/lib/itemprop-requires-itemscope/index.js +28 -11
- package/lib/label-for-references-labelable/index.js +3 -3
- package/lib/label-has-control/index.d.ts +17 -0
- package/lib/label-has-control/index.js +28 -1
- package/lib/label-no-multiple-controls/index.d.ts +4 -5
- package/lib/label-no-multiple-controls/index.js +8 -8
- package/lib/landmark-roles/roles.d.ts +13 -1
- package/lib/landmark-roles/roles.js +22 -0
- package/lib/link-types/index.js +8 -8
- package/lib/map-id-name-match/index.js +1 -1
- package/lib/meter-value-bounds/index.js +3 -3
- package/lib/no-ambiguous-navigable-target-names/index.js +5 -1
- package/lib/no-aria-hidden-on-hidden-until-found/index.js +1 -1
- package/lib/no-aria-on-presentational-children/index.js +4 -0
- package/lib/no-aria-on-unsupported-element/index.js +2 -2
- package/lib/no-boolean-attr-value/index.js +2 -1
- package/lib/no-broken-fragment-link/index.d.ts +5 -0
- package/lib/no-broken-fragment-link/index.js +8 -2
- package/lib/no-consecutive-br/index.js +1 -1
- package/lib/no-content-after-body/index.js +2 -2
- package/lib/no-contradictory-aria-prop/index.js +2 -2
- package/lib/no-default-aria-value/index.js +2 -2
- package/lib/no-default-value/index.js +4 -0
- package/lib/no-deprecated-aria-prop/index.js +2 -2
- package/lib/no-disallowed-ancestor/index.d.ts +16 -1
- package/lib/no-disallowed-ancestor/index.js +68 -5
- package/lib/no-disallowed-attr/index.js +5 -1
- package/lib/no-duplicate-autofocus/index.js +1 -1
- package/lib/no-duplicate-dt/index.js +1 -1
- package/lib/no-duplicate-h1/index.js +18 -14
- package/lib/no-duplicate-id/index.d.ts +9 -0
- package/lib/no-duplicate-id/index.js +23 -4
- package/lib/no-duplicate-sibling-attr/index.js +1 -1
- package/lib/no-duplicate-visible-main/index.js +1 -1
- package/lib/no-empty-table-track/index.js +1 -1
- package/lib/no-extra-selected-options/index.js +1 -1
- package/lib/no-ineffective-attr/index.js +4 -0
- package/lib/no-input-file-value/index.js +1 -1
- package/lib/no-invalid-aria-prop-value/index.js +3 -3
- package/lib/no-invalid-attr-value/index.js +12 -0
- package/lib/no-malformed-character-reference/index.js +1 -1
- package/lib/no-nested-top-level-landmark/index.js +20 -17
- package/lib/no-prohibited-naming/index.js +2 -2
- package/lib/no-redundant-accessible-name/index.js +15 -8
- package/lib/no-redundant-aria-prop/index.js +2 -2
- package/lib/no-refer-to-non-existent-id/index.d.ts +15 -0
- package/lib/no-refer-to-non-existent-id/index.js +47 -16
- package/lib/no-restricted-attr/index.js +2 -1
- package/lib/no-restricted-element/index.js +4 -2
- package/lib/no-skipped-heading-level/index.js +9 -4
- package/lib/no-stray-head-or-body-tag/index.d.ts +2 -1
- package/lib/no-stray-head-or-body-tag/index.js +4 -3
- package/lib/no-table-cell-overlap/index.js +3 -2
- package/lib/no-table-span-overflow/index.js +1 -1
- package/lib/no-translate.d.ts +17 -0
- package/lib/no-translate.js +29 -0
- package/lib/no-unclosed-element-at-eof/index.js +1 -1
- package/lib/no-unescaped-char/index.js +1 -1
- package/lib/no-unsupported-browser-features/index.d.ts +9 -0
- package/lib/no-unsupported-browser-features/index.js +10 -7
- package/lib/permitted-contents/content-model.d.ts +14 -0
- package/lib/permitted-contents/content-model.js +21 -1
- package/lib/permitted-contents/index.js +6 -8
- package/lib/permitted-contents/matches-selector.js +32 -5
- package/lib/permitted-contents/represent-transparent-nodes.js +100 -68
- package/lib/permitted-contents/slot-content.d.ts +18 -1
- package/lib/permitted-contents/slot-content.js +49 -2
- package/lib/permitted-contents/start.d.ts +8 -1
- package/lib/permitted-contents/start.js +39 -6
- package/lib/permitted-contents/utils.d.ts +11 -0
- package/lib/permitted-contents/utils.js +15 -0
- package/lib/placeholder-label-option/index.js +4 -26
- package/lib/progress-value-bounds/index.js +2 -2
- package/lib/require-accessible-name/index.js +2 -2
- package/lib/require-adjacent-popover/index.js +4 -2
- package/lib/require-ancestor/index.d.ts +37 -1
- package/lib/require-ancestor/index.js +82 -9
- package/lib/require-ancestor/meta.d.ts +5 -0
- package/lib/require-ancestor/meta.js +5 -0
- package/lib/require-attr/index.js +4 -1
- package/lib/require-datetime/index.js +3 -2
- package/lib/require-dialog-autofocus/index.js +8 -6
- package/lib/require-element/index.js +8 -2
- package/lib/require-h1/index.js +4 -8
- package/lib/require-landmark-label/index.js +17 -15
- package/lib/role-supports-aria-prop/index.js +2 -2
- package/lib/sizes-auto-requires-lazy-loading/index.d.ts +4 -2
- package/lib/sizes-auto-requires-lazy-loading/index.js +4 -2
- package/lib/usemap-references-map/index.js +12 -3
- package/lib/valid-speculation-rules/index.js +1 -1
- package/lib/wai-aria/checkings/contradictory-aria-prop.js +2 -2
- package/lib/wai-aria/checkings/interaction-in-hidden.js +6 -2
- package/lib/wai-aria/checkings/presentational-children.js +38 -17
- package/lib/wai-aria/checkings/redundant-aria-prop.js +1 -1
- package/lib/wai-aria/checkings/required-accessibility-parent-role.js +4 -2
- package/lib/wai-aria/checkings/required-owned-elements.d.ts +2 -0
- package/lib/wai-aria/checkings/required-owned-elements.js +175 -21
- package/lib/wai-aria/checkings/required-prop.js +2 -1
- package/lib/wai-aria/checkings/tab-requires-tabpanel.js +10 -6
- package/lib/wai-aria/checkings/value.d.ts +1 -4
- package/lib/wai-aria/checkings/value.js +8 -19
- package/package.json +14 -11
package/lib/helpers.js
CHANGED
|
@@ -1,5 +1,100 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { asciiLowerCase } from '@markuplint/shared';
|
|
1
|
+
import { getDOMValue, getRenderedChildNodes } from '@markuplint/ml-core';
|
|
2
|
+
import { asciiLowerCase, decodeEntities } from '@markuplint/shared';
|
|
3
|
+
export { getDOMValue };
|
|
4
|
+
/**
|
|
5
|
+
* The element that an id reference written in an attribute of `el` resolves to: the first element
|
|
6
|
+
* of the tree `el` is in whose id is the reference. The content of a `template` element and a
|
|
7
|
+
* declarative shadow root are trees of their own, so a reference there does not reach an id of the
|
|
8
|
+
* document, nor the other way around.
|
|
9
|
+
*
|
|
10
|
+
* The reference is compared with the character references in the markup resolved, as the id is.
|
|
11
|
+
*
|
|
12
|
+
* @template T - The rule configuration value type
|
|
13
|
+
* @template O - The rule options type
|
|
14
|
+
* @param el - The element that has the attribute
|
|
15
|
+
* @param reference - The value of the attribute, or one token of it, as written in the markup
|
|
16
|
+
* @param isExpression - Whether the value is written as code, which has no character reference in it
|
|
17
|
+
* @returns The element, or `null` when the tree has none, or `el` is in no tree
|
|
18
|
+
* @see https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute
|
|
19
|
+
*/
|
|
20
|
+
export function getElementByIdInTree(
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
22
|
+
el, reference, isExpression = false) {
|
|
23
|
+
const root = el.getRootNode();
|
|
24
|
+
if (root.is(root.DOCUMENT_NODE) || root.is(root.DOCUMENT_FRAGMENT_NODE)) {
|
|
25
|
+
return root.getElementById(isExpression ? reference : decodeEntities(reference));
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Whether the element is not an element of the output, so that what is written in it is rendered in
|
|
31
|
+
* place of it:
|
|
32
|
+
*
|
|
33
|
+
* - a fragment (`<>` of JSX), which `childNodes` already flattens;
|
|
34
|
+
* - a `template` element with a directive attribute (Vue's `<template v-if>`, `v-for`, `v-slot`).
|
|
35
|
+
* It is not an HTML `template` element: the template engine renders its children in place of it
|
|
36
|
+
* and drops it. A spread attribute (`{...props}`) is not such a directive: it sets attributes of
|
|
37
|
+
* the element, which stays.
|
|
38
|
+
*
|
|
39
|
+
* `isTransparent` of `@markuplint/ml-core` (`src/ml-dom/helper/tree-scope.ts`), which is not exported,
|
|
40
|
+
* decides the same of an element for the tree boundaries (`getRootNode()`, the shadow host): a kind
|
|
41
|
+
* added here belongs to it too.
|
|
42
|
+
*
|
|
43
|
+
* @template T - The rule configuration value type
|
|
44
|
+
* @template O - The rule options type
|
|
45
|
+
* @param el - An element
|
|
46
|
+
* @returns `true` if the output has no element for it
|
|
47
|
+
*/
|
|
48
|
+
export function isRenderedAway(
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
50
|
+
el) {
|
|
51
|
+
if (el.isFragment) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (el.localName !== 'template') {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
for (const attr of el.attributes) {
|
|
58
|
+
if (attr.isDirective && attr.localName !== '#spread') {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Whether the element is a component whose output is not known, so that it may render an element that
|
|
66
|
+
* a rule looks for among the ancestors (`MLElement#isUnresolvedComponent`). What the parser sees as a
|
|
67
|
+
* component but renders its children in place is not one: what {@link isRenderedAway} says (a
|
|
68
|
+
* fragment such as `React.Fragment` or Astro's `Fragment`, a `template` with a directive).
|
|
69
|
+
*
|
|
70
|
+
* @template T - The rule configuration value type
|
|
71
|
+
* @template O - The rule options type
|
|
72
|
+
* @param el - An ancestor of the element
|
|
73
|
+
* @returns `true` if the element may render what is not known
|
|
74
|
+
*/
|
|
75
|
+
export function isUnknownOutput(
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
77
|
+
el) {
|
|
78
|
+
return !isRenderedAway(el) && el.isUnresolvedComponent();
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The elements that match the selector in the tree `el` is in (see {@link getElementByIdInTree}).
|
|
82
|
+
*
|
|
83
|
+
* @template T - The rule configuration value type
|
|
84
|
+
* @template O - The rule options type
|
|
85
|
+
* @param el - An element of the tree
|
|
86
|
+
* @param selectors - A selector
|
|
87
|
+
* @returns The elements in the order of the markup, none when `el` is in no tree
|
|
88
|
+
*/
|
|
89
|
+
export function querySelectorAllInTree(
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
91
|
+
el, selectors) {
|
|
92
|
+
const root = el.getRootNode();
|
|
93
|
+
if (root.is(root.DOCUMENT_NODE) || root.is(root.DOCUMENT_FRAGMENT_NODE)) {
|
|
94
|
+
return [...root.querySelectorAll(selectors)];
|
|
95
|
+
}
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
3
98
|
/** A `null`/`undefined` condition means the attribute applies unconditionally. */
|
|
4
99
|
export function attrMatches(
|
|
5
100
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -30,9 +125,11 @@ node, condition) {
|
|
|
30
125
|
}
|
|
31
126
|
}
|
|
32
127
|
}
|
|
33
|
-
// Whether the attribute is the one that the selector names is up to the attribute (`isNamedBy`)
|
|
128
|
+
// Whether the attribute is the one that the selector names is up to the attribute (`isNamedBy`).
|
|
129
|
+
// An attribute that the framework does not output (`type={null}`) is known not to be there, so the
|
|
130
|
+
// condition is decided: it is not met.
|
|
34
131
|
const names = [...referencedNames];
|
|
35
|
-
return node.attributes.some(attr => attr.isDynamicValue && names.some(name => attr.isNamedBy(name)));
|
|
132
|
+
return node.attributes.some(attr => attr.isDynamicValue && !attr.isAbsent && names.some(name => attr.isNamedBy(name)));
|
|
36
133
|
}
|
|
37
134
|
/**
|
|
38
135
|
* Memoizes `fn` by the identity of its argument. Use it for an object that is one object for every
|
|
@@ -118,6 +215,20 @@ attr) {
|
|
|
118
215
|
export function namespaceKeyOf(attr) {
|
|
119
216
|
return attr.ownerElement.elementType === 'authored' ? asciiLowerCase(attr.name) : attr.nameKey;
|
|
120
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* The `aria-*` attributes that the element has. An attribute that the framework does not output
|
|
220
|
+
* (`aria-label={null}`, `MLAttr#isAbsent`) is not one of them: the DOM does not have it, so what a rule
|
|
221
|
+
* says of an attribute of the element (it is unsupported, deprecated, redundant, or its value is
|
|
222
|
+
* invalid) is not said of it.
|
|
223
|
+
*
|
|
224
|
+
* @param el - The element
|
|
225
|
+
* @returns The `aria-*` attributes that are output
|
|
226
|
+
*/
|
|
227
|
+
export function getAriaAttrs(
|
|
228
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
229
|
+
el) {
|
|
230
|
+
return el.attributes.filter(attr => !attr.isAbsent && isAriaAttr(attr));
|
|
231
|
+
}
|
|
121
232
|
/**
|
|
122
233
|
* Whether the attribute is an `aria-*` one, told by {@link namespaceKeyOf}.
|
|
123
234
|
*
|
|
@@ -259,20 +370,60 @@ export function toNormalizedValue(value, spec) {
|
|
|
259
370
|
}
|
|
260
371
|
return normalized;
|
|
261
372
|
}
|
|
262
|
-
|
|
373
|
+
/**
|
|
374
|
+
* Whether a child of an element may stand for children that are not known: a block of a preprocessor
|
|
375
|
+
* that has no `blockBehavior` (an output such as `{value}` or an `include`), a `slot`, or a component
|
|
376
|
+
* whose output is not known (`MLElement#isUnresolvedComponent`).
|
|
377
|
+
*
|
|
378
|
+
* It is for a rule that asks which children an element has (the required children, the elements that
|
|
379
|
+
* satisfy a query). It looks at the child alone: a descendant of the child is not a child of the
|
|
380
|
+
* element, so what it renders cannot become the child that the rule asks for, and the attributes of
|
|
381
|
+
* a child do not decide whether the child exists. `MLElement#hasMutableChildren` looks at every
|
|
382
|
+
* descendant, which is the question for a rule that asks what the descendants are or have.
|
|
383
|
+
* `require-datetime` asks about the text, which any descendant may change, and looks at every
|
|
384
|
+
* descendant for an unresolved component itself.
|
|
385
|
+
*
|
|
386
|
+
* @template T - The rule configuration value type
|
|
387
|
+
* @template O - The rule options type
|
|
388
|
+
* @param node - A child node of the element
|
|
389
|
+
* @param components - Whether a component whose output is not known counts. It does not when the
|
|
390
|
+
* rule evaluates the components by the name that they are written with
|
|
391
|
+
* @returns True if the node may stand for children that are not known
|
|
392
|
+
*/
|
|
393
|
+
export function mayRenderUnknownChildren(
|
|
263
394
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
264
|
-
|
|
395
|
+
node, components = true) {
|
|
396
|
+
if (node.is(node.MARKUPLINT_PREPROCESSOR_BLOCK)) {
|
|
397
|
+
return !node.blockBehavior;
|
|
398
|
+
}
|
|
399
|
+
return node.is(node.ELEMENT_NODE) && (node.localName === 'slot' || (components && node.isUnresolvedComponent()));
|
|
400
|
+
}
|
|
401
|
+
export function accnameMayBeMutable(
|
|
265
402
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
266
|
-
|
|
267
|
-
if (el.hasMutableAttributes() || el.hasMutableChildren(true)) {
|
|
403
|
+
el) {
|
|
404
|
+
if (el.hasMutableAttributes() || el.hasMutableChildren(true, isSlotOfUnknownAssignment)) {
|
|
268
405
|
return true;
|
|
269
406
|
}
|
|
270
|
-
const ownedLabel = getOwnedLabel(el
|
|
271
|
-
if (ownedLabel &&
|
|
407
|
+
const ownedLabel = getOwnedLabel(el);
|
|
408
|
+
if (ownedLabel &&
|
|
409
|
+
(ownedLabel.hasMutableAttributes() || ownedLabel.hasMutableChildren(true, isSlotOfUnknownAssignment))) {
|
|
272
410
|
return true;
|
|
273
411
|
}
|
|
274
412
|
return false;
|
|
275
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* Whether a `slot` renders what the markup does not tell: the nodes assigned to it, or its children, are
|
|
416
|
+
* not known (`MLElement#getAssignedNodes` is `null`). A slot whose assignment is known renders them, which
|
|
417
|
+
* the rule reads itself.
|
|
418
|
+
*
|
|
419
|
+
* @param slot - A `slot` element
|
|
420
|
+
* @returns `true` if what the slot renders is not known
|
|
421
|
+
*/
|
|
422
|
+
function isSlotOfUnknownAssignment(
|
|
423
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
424
|
+
slot) {
|
|
425
|
+
return slot.getAssignedNodes() === null;
|
|
426
|
+
}
|
|
276
427
|
/**
|
|
277
428
|
* Whether the accessible name of `el` may be rendered by content the pretender
|
|
278
429
|
* does not know: an unknown entry (`{ dynamic: true }`) in the `contents` of
|
|
@@ -286,41 +437,48 @@ document) {
|
|
|
286
437
|
*/
|
|
287
438
|
export function accnameMayComeFromUnknownContent(
|
|
288
439
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
289
|
-
el
|
|
290
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
291
|
-
document) {
|
|
440
|
+
el) {
|
|
292
441
|
if (hasUnknownContent(el)) {
|
|
293
442
|
return true;
|
|
294
443
|
}
|
|
295
|
-
const
|
|
444
|
+
const labelledbyAttr = el.getAttributeNode('aria-labelledby');
|
|
445
|
+
const labelledby = labelledbyAttr?.value;
|
|
296
446
|
if (labelledby) {
|
|
297
447
|
const referenced = labelledby
|
|
298
448
|
.trim()
|
|
299
449
|
.split(/\s+/)
|
|
300
|
-
.map(id =>
|
|
450
|
+
.map(id => getElementByIdInTree(el, id, labelledbyAttr.isExpressionValue));
|
|
301
451
|
if (referenced.some(target => target && hasUnknownContent(target))) {
|
|
302
452
|
return true;
|
|
303
453
|
}
|
|
304
454
|
}
|
|
305
|
-
const ownedLabel = getOwnedLabel(el
|
|
455
|
+
const ownedLabel = getOwnedLabel(el);
|
|
306
456
|
return !!ownedLabel && hasUnknownContent(ownedLabel);
|
|
307
457
|
}
|
|
308
458
|
/**
|
|
309
|
-
* Walks the
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
* `
|
|
313
|
-
*
|
|
459
|
+
* Walks the children {@link getRenderedChildNodes} returns. A hidden child is
|
|
460
|
+
* skipped because the name computation never reads it (`isHidden` in that resolver).
|
|
461
|
+
*
|
|
462
|
+
* A `template` whose shadow root is only maybe attached returns `true` before
|
|
463
|
+
* that walk. Its content is omitted from the computed name, so an ancestor
|
|
464
|
+
* whose computed name is empty is not reported: the rule calls this only after
|
|
465
|
+
* the computed name is empty. Light DOM text that remains in the computed name
|
|
466
|
+
* is still a name, and the rule does not reach this function.
|
|
467
|
+
*
|
|
468
|
+
* Unknown `contents` of a pretended component count only when no declarative
|
|
469
|
+
* shadow root is attached to it: the shadow root is rendered in their place.
|
|
314
470
|
*/
|
|
315
471
|
function hasUnknownContent(
|
|
316
472
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
317
473
|
el) {
|
|
318
474
|
const context = el.pretenderContext;
|
|
319
|
-
if (context?.type === 'pretender' && context.slotContent?.mutable) {
|
|
475
|
+
if (context?.type === 'pretender' && context.slotContent?.mutable && !el.declarativeShadowRoot) {
|
|
320
476
|
return true;
|
|
321
477
|
}
|
|
322
|
-
|
|
323
|
-
|
|
478
|
+
if (context?.type !== 'pretender' && el.isHTMLTemplateElement && el.declarativeShadowRootAttachment === 'unknown') {
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
const children = [...getRenderedChildNodes(el)];
|
|
324
482
|
return children.some(child => child.is(child.ELEMENT_NODE) &&
|
|
325
483
|
child.getAttribute('aria-hidden') !== 'true' &&
|
|
326
484
|
!child.hasAttribute('hidden') &&
|
|
@@ -329,17 +487,17 @@ el) {
|
|
|
329
487
|
export const labelable = ['button', 'input:not([type=hidden])', 'meter', 'output', 'progress', 'select', 'textarea'];
|
|
330
488
|
export function getOwnedLabel(
|
|
331
489
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
332
|
-
el
|
|
333
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
334
|
-
document) {
|
|
490
|
+
el) {
|
|
335
491
|
if (!labelable.some(cond => el.matches(cond))) {
|
|
336
492
|
return null;
|
|
337
493
|
}
|
|
338
494
|
let ownedLabel = el.closest('label');
|
|
339
495
|
if (!ownedLabel) {
|
|
340
|
-
const id = el.
|
|
496
|
+
const id = getDOMValue(el.getAttributeNode('id'));
|
|
341
497
|
if (id) {
|
|
342
|
-
|
|
498
|
+
// The label is looked for in the tree of the element, and the two values are compared as the DOM has them
|
|
499
|
+
ownedLabel =
|
|
500
|
+
querySelectorAllInTree(el, 'label[for]').find(label => getDOMValue(label.getAttributeNode('for')) === id) ?? null;
|
|
343
501
|
}
|
|
344
502
|
}
|
|
345
503
|
return ownedLabel;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
+
import { getElementByIdInTree } from '../helpers.js';
|
|
2
3
|
import meta from './meta.js';
|
|
3
4
|
/**
|
|
4
5
|
* Require the `list` attribute on an `<input>` element to reference an
|
|
@@ -28,8 +29,7 @@ export default createRule({
|
|
|
28
29
|
return;
|
|
29
30
|
if (listAttr.isDynamicValue)
|
|
30
31
|
return;
|
|
31
|
-
const
|
|
32
|
-
const target = [...document.querySelectorAll('[id]')].find(candidate => candidate.getAttribute('id') === targetId);
|
|
32
|
+
const target = getElementByIdInTree(el, listAttr.value, listAttr.isExpressionValue);
|
|
33
33
|
// Existence is checked by `no-refer-to-non-existent-id`. If the ID does not resolve
|
|
34
34
|
// here, leave that diagnostic to the dedicated rule and stay silent.
|
|
35
35
|
if (!target)
|
|
@@ -40,7 +40,7 @@ export default createRule({
|
|
|
40
40
|
line: listAttr.valueNode?.startLine,
|
|
41
41
|
col: listAttr.valueNode?.startCol,
|
|
42
42
|
raw: listAttr.valueNode?.raw,
|
|
43
|
-
message: t('The "{0}" attribute of the "{1}" element must reference a "{2}" element', 'list', 'input', 'datalist'),
|
|
43
|
+
message: t('The "{0*}" attribute of the "{1*}" element must reference a "{2*}" element', 'list', 'input', 'datalist'),
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
+
import { getDOMValue } from '../helpers.js';
|
|
2
3
|
import meta from './meta.js';
|
|
3
4
|
/**
|
|
4
5
|
* Rule that requires every `itemprop` to belong to an item.
|
|
@@ -28,24 +29,40 @@ export default createRule({
|
|
|
28
29
|
// once and then check each `[itemprop]` against the union set.
|
|
29
30
|
// `itemref=" "` (whitespace-only) yields zero tokens and contributes
|
|
30
31
|
// no entries to the set; orphans referencing such a host stay orphans.
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
// The ids are collected by tree: the content of a `template` element and a declarative shadow
|
|
33
|
+
// root are trees of their own, which an `itemref` does not reach out of, and the elements of
|
|
34
|
+
// every tree are checked.
|
|
35
|
+
// An `itemref` that is not known until it is rendered may reach any element of its tree, so
|
|
36
|
+
// that tree is not checked.
|
|
37
|
+
const itemrefIdsOfTree = new Map();
|
|
38
|
+
const treesWithDynamicItemref = new Set();
|
|
39
|
+
const itemrefHosts = document.deepQuerySelectorAll('[itemscope][itemref]');
|
|
33
40
|
for (const host of itemrefHosts) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
const root = host.getRootNode();
|
|
42
|
+
let itemrefIds = itemrefIdsOfTree.get(root);
|
|
43
|
+
if (!itemrefIds) {
|
|
44
|
+
itemrefIds = new Set();
|
|
45
|
+
itemrefIdsOfTree.set(root, itemrefIds);
|
|
46
|
+
}
|
|
47
|
+
const itemref = host.getAttributeNode('itemref');
|
|
48
|
+
if (itemref?.isDynamicValue) {
|
|
49
|
+
treesWithDynamicItemref.add(root);
|
|
50
|
+
}
|
|
51
|
+
for (const token of itemref?.value.split(/\s+/) ?? []) {
|
|
52
|
+
const id = token ? getDOMValue(itemref, token) : null;
|
|
53
|
+
if (id != null)
|
|
54
|
+
itemrefIds.add(id);
|
|
38
55
|
}
|
|
39
56
|
}
|
|
40
|
-
const itempropEls = document.
|
|
57
|
+
const itempropEls = document.deepQuerySelectorAll('[itemprop]');
|
|
41
58
|
for (const el of itempropEls) {
|
|
42
59
|
const hasAncestorScope = el.closest('[itemscope]') != null;
|
|
43
|
-
const id = el.
|
|
44
|
-
const isItemrefTarget = id != null && id !== '' &&
|
|
45
|
-
if (!hasAncestorScope && !isItemrefTarget) {
|
|
60
|
+
const id = getDOMValue(el.getAttributeNode('id'));
|
|
61
|
+
const isItemrefTarget = id != null && id !== '' && !!itemrefIdsOfTree.get(el.getRootNode())?.has(id);
|
|
62
|
+
if (!hasAncestorScope && !isItemrefTarget && !treesWithDynamicItemref.has(el.getRootNode())) {
|
|
46
63
|
report({
|
|
47
64
|
scope: el,
|
|
48
|
-
message: t('{0} must
|
|
65
|
+
message: t('{0} must belong to an item via an ancestor `itemscope` or be referenced by an `itemref`', t('the {0}', 'element')),
|
|
49
66
|
});
|
|
50
67
|
}
|
|
51
68
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
+
import { getElementByIdInTree } from '../helpers.js';
|
|
2
3
|
import meta from './meta.js';
|
|
3
4
|
/**
|
|
4
5
|
* Labelable elements per HTML LS §4.10.2 *Categories: Labelable elements*.
|
|
@@ -51,8 +52,7 @@ export default createRule({
|
|
|
51
52
|
return;
|
|
52
53
|
if (forAttr.isDynamicValue)
|
|
53
54
|
return;
|
|
54
|
-
const
|
|
55
|
-
const target = [...document.querySelectorAll('[id]')].find(candidate => candidate.getAttribute('id') === targetId);
|
|
55
|
+
const target = getElementByIdInTree(el, forAttr.value, forAttr.isExpressionValue);
|
|
56
56
|
// Existence is checked by `no-refer-to-non-existent-id`. If the ID does not resolve
|
|
57
57
|
// here, leave that diagnostic to the dedicated rule and stay silent.
|
|
58
58
|
if (!target)
|
|
@@ -63,7 +63,7 @@ export default createRule({
|
|
|
63
63
|
line: forAttr.valueNode?.startLine,
|
|
64
64
|
col: forAttr.valueNode?.startCol,
|
|
65
65
|
raw: forAttr.valueNode?.raw,
|
|
66
|
-
message: t('The "{0}" attribute of the "{1}" element must reference a labelable element', 'for', 'label'),
|
|
66
|
+
message: t('The "{0*}" attribute of the "{1*}" element must reference a labelable element', 'for', 'label'),
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
});
|
|
@@ -4,6 +4,23 @@
|
|
|
4
4
|
* this rule performs — a `<label>` with an excess of descendant controls is
|
|
5
5
|
* `label-no-multiple-controls`'s responsibility, which also accounts for the
|
|
6
6
|
* `for`-references-an-external-control case that this rule does not see.
|
|
7
|
+
*
|
|
8
|
+
* An element inside a conditional preprocessor block counts, because the
|
|
9
|
+
* branch that renders it puts that element in the label's tree:
|
|
10
|
+
*
|
|
11
|
+
* > If the `for` attribute is not specified, but the `label` element has a labelable element descendant, then the first such descendant in tree order is the `label` element's labeled control.
|
|
12
|
+
*
|
|
13
|
+
* @see https://html.spec.whatwg.org/multipage/forms.html#labeled-control
|
|
14
|
+
*
|
|
15
|
+
* `children` does not list that element, because a conditional block is not
|
|
16
|
+
* flattened into `childNodes`. This rule still treats any element in
|
|
17
|
+
* `children` or in a `conditionalChildNodes()` branch as enough; it does not
|
|
18
|
+
* check that the element is labelable.
|
|
19
|
+
*
|
|
20
|
+
* A block that is not a fragment and has no `blockBehavior` is neither
|
|
21
|
+
* flattened nor a branch, so an element that exists only inside it is not
|
|
22
|
+
* counted. A Svelte `{#snippet}` is that kind of block: its body is a
|
|
23
|
+
* definition, not a branch rendered where the block is written.
|
|
7
24
|
*/
|
|
8
25
|
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<import("@markuplint/ml-core").RuleConfigValue, undefined>>;
|
|
9
26
|
export default _default;
|
|
@@ -6,6 +6,23 @@ import meta from './meta.js';
|
|
|
6
6
|
* this rule performs — a `<label>` with an excess of descendant controls is
|
|
7
7
|
* `label-no-multiple-controls`'s responsibility, which also accounts for the
|
|
8
8
|
* `for`-references-an-external-control case that this rule does not see.
|
|
9
|
+
*
|
|
10
|
+
* An element inside a conditional preprocessor block counts, because the
|
|
11
|
+
* branch that renders it puts that element in the label's tree:
|
|
12
|
+
*
|
|
13
|
+
* > If the `for` attribute is not specified, but the `label` element has a labelable element descendant, then the first such descendant in tree order is the `label` element's labeled control.
|
|
14
|
+
*
|
|
15
|
+
* @see https://html.spec.whatwg.org/multipage/forms.html#labeled-control
|
|
16
|
+
*
|
|
17
|
+
* `children` does not list that element, because a conditional block is not
|
|
18
|
+
* flattened into `childNodes`. This rule still treats any element in
|
|
19
|
+
* `children` or in a `conditionalChildNodes()` branch as enough; it does not
|
|
20
|
+
* check that the element is labelable.
|
|
21
|
+
*
|
|
22
|
+
* A block that is not a fragment and has no `blockBehavior` is neither
|
|
23
|
+
* flattened nor a branch, so an element that exists only inside it is not
|
|
24
|
+
* counted. A Svelte `{#snippet}` is that kind of block: its body is a
|
|
25
|
+
* definition, not a branch rendered where the block is written.
|
|
9
26
|
*/
|
|
10
27
|
export default createRule({
|
|
11
28
|
meta: meta,
|
|
@@ -21,7 +38,17 @@ export default createRule({
|
|
|
21
38
|
if (el.pretenderContext?.type === 'pretender' && !el.hasAttribute('as')) {
|
|
22
39
|
return;
|
|
23
40
|
}
|
|
24
|
-
|
|
41
|
+
// `for` already associates the label, so the branch walk is wasted there.
|
|
42
|
+
// `children` already includes elements of an `each` block. A conditional
|
|
43
|
+
// block is not flattened, so look through its branches only when
|
|
44
|
+
// `children` is empty. One branch that contains an element is enough:
|
|
45
|
+
// the branches that never render together stay out of `children`.
|
|
46
|
+
if (el.hasAttribute('for')) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const hasElementInSomeBranch = el.children.length > 0 ||
|
|
50
|
+
el.conditionalChildNodes().some(branch => [...branch].some(node => node.is(node.ELEMENT_NODE)));
|
|
51
|
+
if (!hasElementInSomeBranch) {
|
|
25
52
|
report({
|
|
26
53
|
scope: el,
|
|
27
54
|
message: t('{0} should associate with {1}', t('The "{0*}" {1}', 'label', 'element'), t('a {0}', 'control')),
|
|
@@ -21,11 +21,10 @@
|
|
|
21
21
|
* `label-for-references-labelable`. This rule only inspects the label
|
|
22
22
|
* subtree once the target's category has been classified.
|
|
23
23
|
*
|
|
24
|
-
* The `for` target
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* amortize poorly and split rule ownership across a helper module.
|
|
24
|
+
* The `for` target is resolved as `label-for-references-labelable` does it: in the tree of the
|
|
25
|
+
* label, so an id in the content of a `template` element is not the target of a label outside of
|
|
26
|
+
* it. The controls under the label are the ones of its own tree as well: what is in the content of
|
|
27
|
+
* a `template` element under the label is not a descendant of it.
|
|
29
28
|
*/
|
|
30
29
|
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, null>>;
|
|
31
30
|
export default _default;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
+
import { getElementByIdInTree } from '../helpers.js';
|
|
2
3
|
import meta from './meta.js';
|
|
3
4
|
/**
|
|
4
5
|
* The HTML LS list of form-control descendants that may appear under a
|
|
@@ -48,11 +49,10 @@ const LABELABLE_SELECTOR = [
|
|
|
48
49
|
* `label-for-references-labelable`. This rule only inspects the label
|
|
49
50
|
* subtree once the target's category has been classified.
|
|
50
51
|
*
|
|
51
|
-
* The `for` target
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* amortize poorly and split rule ownership across a helper module.
|
|
52
|
+
* The `for` target is resolved as `label-for-references-labelable` does it: in the tree of the
|
|
53
|
+
* label, so an id in the content of a `template` element is not the target of a label outside of
|
|
54
|
+
* it. The controls under the label are the ones of its own tree as well: what is in the content of
|
|
55
|
+
* a `template` element under the label is not a descendant of it.
|
|
56
56
|
*/
|
|
57
57
|
export default createRule({
|
|
58
58
|
meta: meta,
|
|
@@ -72,7 +72,7 @@ export default createRule({
|
|
|
72
72
|
const hasExternalLabeledControl = (() => {
|
|
73
73
|
if (!forAttr || forAttr.isDynamicValue)
|
|
74
74
|
return false;
|
|
75
|
-
const target =
|
|
75
|
+
const target = getElementByIdInTree(el, forAttr.value, forAttr.isExpressionValue);
|
|
76
76
|
if (!target)
|
|
77
77
|
return false;
|
|
78
78
|
// A descendant with a matching id is still the label's labeled control
|
|
@@ -86,7 +86,7 @@ export default createRule({
|
|
|
86
86
|
for (const control of controls) {
|
|
87
87
|
report({
|
|
88
88
|
scope: control,
|
|
89
|
-
message: t('The "{0}" element must not contain a form-control descendant when the "{1}" attribute references an external labelable element', 'label', 'for'),
|
|
89
|
+
message: t('The "{0*}" element must not contain a form-control descendant when the "{1*}" attribute references an external labelable element', 'label', 'for'),
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
92
|
return;
|
|
@@ -96,7 +96,7 @@ export default createRule({
|
|
|
96
96
|
for (const control of controls.slice(1)) {
|
|
97
97
|
report({
|
|
98
98
|
scope: control,
|
|
99
|
-
message: t('The "{0}" element may contain at most one form-control descendant ({1})', 'label', 'button, input, meter, output, progress, select, or textarea'),
|
|
99
|
+
message: t('The "{0*}" element may contain at most one form-control descendant ({1})', 'label', 'button, input, meter, output, progress, select, or textarea'),
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Element } from '@markuplint/ml-core';
|
|
1
|
+
import type { Document, Element } from '@markuplint/ml-core';
|
|
2
2
|
import type { PlainData } from '@markuplint/ml-config';
|
|
3
3
|
/**
|
|
4
4
|
* Shared by the two rules split out of the former `landmark-roles` rule:
|
|
@@ -23,6 +23,18 @@ export type RoleSet<O extends PlainData> = {
|
|
|
23
23
|
export declare function computeLandmarkRoles<O extends PlainData>(document: {
|
|
24
24
|
querySelectorAll(selector: string): Iterable<Element<boolean, O>>;
|
|
25
25
|
}): RoleSet<O>;
|
|
26
|
+
/**
|
|
27
|
+
* Collects the landmark-role elements of every tree of the document: the document itself, and the
|
|
28
|
+
* content of each `template` element and each declarative shadow root, which are trees of their own.
|
|
29
|
+
* Where the content of a `template` ends up in a document is not known from the markup, so its
|
|
30
|
+
* landmarks are not counted with the ones of the document, nor does an ancestor of the document make
|
|
31
|
+
* them nested.
|
|
32
|
+
*
|
|
33
|
+
* @template O - The rule options type
|
|
34
|
+
* @param document - The document to search.
|
|
35
|
+
* @returns The mapping from role to its matched elements, one for each tree.
|
|
36
|
+
*/
|
|
37
|
+
export declare function computeLandmarkRolesOfEachTree<O extends PlainData>(document: Document<boolean, O>): RoleSet<O>[];
|
|
26
38
|
/**
|
|
27
39
|
* Collects all UUIDs of elements that have landmark roles.
|
|
28
40
|
*
|
|
@@ -60,6 +60,28 @@ document) {
|
|
|
60
60
|
roles.contentinfo.push(...footers);
|
|
61
61
|
return roles;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Collects the landmark-role elements of every tree of the document: the document itself, and the
|
|
65
|
+
* content of each `template` element and each declarative shadow root, which are trees of their own.
|
|
66
|
+
* Where the content of a `template` ends up in a document is not known from the markup, so its
|
|
67
|
+
* landmarks are not counted with the ones of the document, nor does an ancestor of the document make
|
|
68
|
+
* them nested.
|
|
69
|
+
*
|
|
70
|
+
* @template O - The rule options type
|
|
71
|
+
* @param document - The document to search.
|
|
72
|
+
* @returns The mapping from role to its matched elements, one for each tree.
|
|
73
|
+
*/
|
|
74
|
+
export function computeLandmarkRolesOfEachTree(
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
76
|
+
document) {
|
|
77
|
+
const trees = [document];
|
|
78
|
+
for (const template of document.deepQuerySelectorAll('template')) {
|
|
79
|
+
if (template.content) {
|
|
80
|
+
trees.push(template.content);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return trees.map(tree => computeLandmarkRoles(tree));
|
|
84
|
+
}
|
|
63
85
|
/**
|
|
64
86
|
* An element already collected through its explicit `role` attribute (e.g.
|
|
65
87
|
* `<header role="banner">`) must not be added a second time by the implicit
|
package/lib/link-types/index.js
CHANGED
|
@@ -122,28 +122,28 @@ function validateWhatwgKeyword(def, context, t) {
|
|
|
122
122
|
switch (context) {
|
|
123
123
|
case 'link': {
|
|
124
124
|
if (def.link === 'not allowed') {
|
|
125
|
-
return t('The "{0*}" {1} is not allowed on
|
|
125
|
+
return t('The "{0*}" {1} is not allowed on {2}', def.keyword, 'keyword', t('the "{0*}" {1}', 'link', 'element'));
|
|
126
126
|
}
|
|
127
127
|
return null;
|
|
128
128
|
}
|
|
129
129
|
case 'body-link': {
|
|
130
130
|
if (def.link === 'not allowed') {
|
|
131
|
-
return t('The "{0*}" {1} is not allowed on
|
|
131
|
+
return t('The "{0*}" {1} is not allowed on {2}', def.keyword, 'keyword', t('the "{0*}" {1}', 'link', 'element'));
|
|
132
132
|
}
|
|
133
133
|
if (def.bodyOk !== 'Yes') {
|
|
134
|
-
return t('The "{0*}" {1} is not allowed on
|
|
134
|
+
return t('The "{0*}" {1} is not allowed on {2} inside {3}', def.keyword, 'keyword', t('the "{0*}" {1}', 'link', 'element'), t('the "{0*}" {1}', 'body', 'element'));
|
|
135
135
|
}
|
|
136
136
|
return null;
|
|
137
137
|
}
|
|
138
138
|
case 'a-area': {
|
|
139
139
|
if (def.a === 'not allowed') {
|
|
140
|
-
return t('The "{0*}" {1} is not allowed on
|
|
140
|
+
return t('The "{0*}" {1} is not allowed on {2}', def.keyword, 'keyword', t('the "{0*}" {1}', 'a', 'element'));
|
|
141
141
|
}
|
|
142
142
|
return null;
|
|
143
143
|
}
|
|
144
144
|
case 'form': {
|
|
145
145
|
if (def.form === 'not allowed') {
|
|
146
|
-
return t('The "{0*}" {1} is not allowed on
|
|
146
|
+
return t('The "{0*}" {1} is not allowed on {2}', def.keyword, 'keyword', t('the "{0*}" {1}', 'form', 'element'));
|
|
147
147
|
}
|
|
148
148
|
return null;
|
|
149
149
|
}
|
|
@@ -161,19 +161,19 @@ function validateMicroformatContext(def, context, t) {
|
|
|
161
161
|
case 'link':
|
|
162
162
|
case 'body-link': {
|
|
163
163
|
if (!def.link) {
|
|
164
|
-
return t('The "{0*}" {1} is not allowed on
|
|
164
|
+
return t('The "{0*}" {1} is not allowed on {2}', def.keyword, 'keyword', t('the "{0*}" {1}', 'link', 'element'));
|
|
165
165
|
}
|
|
166
166
|
return null;
|
|
167
167
|
}
|
|
168
168
|
case 'a-area': {
|
|
169
169
|
if (!def.a) {
|
|
170
|
-
return t('The "{0*}" {1} is not allowed on
|
|
170
|
+
return t('The "{0*}" {1} is not allowed on {2}', def.keyword, 'keyword', t('the "{0*}" {1}', 'a', 'element'));
|
|
171
171
|
}
|
|
172
172
|
return null;
|
|
173
173
|
}
|
|
174
174
|
case 'form': {
|
|
175
175
|
// Microformats don't define form context; reject
|
|
176
|
-
return t('The "{0*}" {1} is not allowed on
|
|
176
|
+
return t('The "{0*}" {1} is not allowed on {2}', def.keyword, 'keyword', t('the "{0*}" {1}', 'form', 'element'));
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
}
|