@markuplint/rules 5.4.0 → 5.5.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 +21 -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 +1 -1
- package/lib/consistent-table-row-length/index.js +1 -1
- package/lib/element-supports-aria-prop/index.js +2 -2
- package/lib/form-attr-references-form/index.js +2 -2
- package/lib/helpers.d.ts +43 -5
- package/lib/helpers.js +88 -26
- package/lib/input-list-references-datalist/index.js +2 -2
- package/lib/itemprop-requires-itemscope/index.js +27 -10
- package/lib/label-for-references-labelable/index.js +2 -2
- 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 +6 -6
- package/lib/landmark-roles/roles.d.ts +13 -1
- package/lib/landmark-roles/roles.js +22 -0
- package/lib/no-ambiguous-navigable-target-names/index.js +4 -0
- 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-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 +78 -4
- package/lib/no-disallowed-attr/index.js +4 -0
- 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-empty-table-track/index.js +1 -1
- package/lib/no-ineffective-attr/index.js +4 -0
- package/lib/no-invalid-aria-prop-value/index.js +3 -3
- package/lib/no-invalid-attr-value/index.js +12 -0
- 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 +8 -7
- 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-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 +2 -1
- package/lib/no-table-cell-overlap/index.js +1 -1
- package/lib/no-table-span-overflow/index.js +1 -1
- package/lib/no-unescaped-char/index.js +1 -1
- package/lib/permitted-contents/index.js +2 -4
- package/lib/permitted-contents/matches-selector.js +28 -5
- package/lib/permitted-contents/represent-transparent-nodes.js +100 -68
- package/lib/permitted-contents/slot-content.d.ts +2 -1
- package/lib/permitted-contents/slot-content.js +28 -2
- package/lib/permitted-contents/start.js +4 -2
- 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/require-accessible-name/index.js +2 -2
- package/lib/require-adjacent-popover/index.js +4 -2
- package/lib/require-ancestor/index.d.ts +20 -0
- package/lib/require-ancestor/index.js +33 -7
- package/lib/require-datetime/index.js +1 -1
- package/lib/require-dialog-autofocus/index.js +8 -6
- 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 +11 -2
- package/lib/valid-speculation-rules/index.js +1 -1
- 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/required-accessibility-parent-role.js +2 -1
- 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 +5 -17
- package/package.json +11 -11
|
@@ -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
|
|
@@ -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
|
|
@@ -10,6 +10,10 @@ export default createRule({
|
|
|
10
10
|
if (!attr.valueNode) {
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
|
+
// The value of an attribute that is not known, or that is not output, is not a name to judge
|
|
14
|
+
if (attr.isDynamicValue) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
13
17
|
const attrSpecs = getAttrSpecs(attr.ownerElement, document.specs);
|
|
14
18
|
if (!attrSpecs) {
|
|
15
19
|
return;
|
|
@@ -14,6 +14,10 @@ export default createRule({
|
|
|
14
14
|
if (!elSpec.globalAttrs['#ARIAAttrs'])
|
|
15
15
|
return;
|
|
16
16
|
for (const attr of el.attributes) {
|
|
17
|
+
// An attribute that the framework does not output is not an attribute of the element
|
|
18
|
+
if (attr.isAbsent) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
17
21
|
report(checkingPresentationalChildren({ attr, el }));
|
|
18
22
|
}
|
|
19
23
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRule, getSpec } from '@markuplint/ml-core';
|
|
2
|
-
import { Collection,
|
|
2
|
+
import { Collection, getAriaAttrs } from '../helpers.js';
|
|
3
3
|
import meta from './meta.js';
|
|
4
4
|
/**
|
|
5
5
|
* Extracted from the former `wai-aria` umbrella rule (#3989), which always
|
|
@@ -22,7 +22,7 @@ export default createRule({
|
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
const roleAttr = el.getAttributeNode('role');
|
|
25
|
-
const propAttrs = el
|
|
25
|
+
const propAttrs = getAriaAttrs(el);
|
|
26
26
|
const ariaAttrs = new Collection(roleAttr, ...propAttrs);
|
|
27
27
|
for (const ariaAttr of ariaAttrs) {
|
|
28
28
|
report({
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* The target of a fragment link is an element of the document tree: the content of a `template`
|
|
3
|
+
* element and a shadow root are not where the navigation scrolls to. So the ids and names are the ones
|
|
4
|
+
* of the document (`document.querySelectorAll`, not the other trees), and a link in any tree is checked
|
|
5
|
+
* against them. A dynamic id in the document, not in another tree, stops the check.
|
|
6
|
+
*
|
|
2
7
|
* Split out of the former `no-refer-to-non-existent-id` rule. Unlike the
|
|
3
8
|
* `DOMID`-typed attributes and ARIA ID references that rule covers, HTML LS
|
|
4
9
|
* does not treat a broken hyperlink fragment as a conformance violation —
|
|
@@ -4,6 +4,11 @@ import meta from './meta.js';
|
|
|
4
4
|
/** CSS selector matching hyperlink elements that have an `href` attribute. */
|
|
5
5
|
const HYPERLINK_SELECTOR = 'a[href], area[href]';
|
|
6
6
|
/**
|
|
7
|
+
* The target of a fragment link is an element of the document tree: the content of a `template`
|
|
8
|
+
* element and a shadow root are not where the navigation scrolls to. So the ids and names are the ones
|
|
9
|
+
* of the document (`document.querySelectorAll`, not the other trees), and a link in any tree is checked
|
|
10
|
+
* against them. A dynamic id in the document, not in another tree, stops the check.
|
|
11
|
+
*
|
|
7
12
|
* Split out of the former `no-refer-to-non-existent-id` rule. Unlike the
|
|
8
13
|
* `DOMID`-typed attributes and ARIA ID references that rule covers, HTML LS
|
|
9
14
|
* does not treat a broken hyperlink fragment as a conformance violation —
|
|
@@ -38,7 +43,8 @@ export default createRule({
|
|
|
38
43
|
hasDynamicId = true;
|
|
39
44
|
}
|
|
40
45
|
if (attr.valueType !== 'code') {
|
|
41
|
-
|
|
46
|
+
// An expression is code, not markup: its string has no character reference to decode
|
|
47
|
+
idList.add(attr.isExpressionValue ? attr.value : decodeEntities(attr.value));
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
50
|
if (hasDynamicId) {
|
|
@@ -53,7 +59,7 @@ export default createRule({
|
|
|
53
59
|
hasDynamicName = true;
|
|
54
60
|
}
|
|
55
61
|
if (attr.valueType !== 'code') {
|
|
56
|
-
nameList.add(decodeEntities(attr.value));
|
|
62
|
+
nameList.add(attr.isExpressionValue ? attr.value : decodeEntities(attr.value));
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
await document.walkOn('Element', el => {
|
|
@@ -4,7 +4,7 @@ export default createRule({
|
|
|
4
4
|
meta: meta,
|
|
5
5
|
defaultSeverity: 'warning',
|
|
6
6
|
verify({ document, report, t }) {
|
|
7
|
-
const brList = [...document.
|
|
7
|
+
const brList = [...document.deepQuerySelectorAll('br')];
|
|
8
8
|
for (const br of brList) {
|
|
9
9
|
let next = br.nextSibling;
|
|
10
10
|
while (next && next.is(next.TEXT_NODE) && !next.nodeValue?.trim()) {
|
|
@@ -2,7 +2,7 @@ import { createRule, ariaSpecs, getAttrSpecs, getSpec } from '@markuplint/ml-cor
|
|
|
2
2
|
import { ARIA_RECOMMENDED_VERSION } from '@markuplint/ml-spec';
|
|
3
3
|
import { checkingContradictoryAriaProp } from '../wai-aria/checkings/contradictory-aria-prop.js';
|
|
4
4
|
import { defaultOptions } from '../wai-aria/default-options.js';
|
|
5
|
-
import {
|
|
5
|
+
import { getAriaAttrs } from '../helpers.js';
|
|
6
6
|
import meta from './meta.js';
|
|
7
7
|
/**
|
|
8
8
|
* Split from the former `wai-aria-implicit-props` rule (#3989): the
|
|
@@ -21,7 +21,7 @@ export default createRule({
|
|
|
21
21
|
return;
|
|
22
22
|
if (!elSpec.globalAttrs['#ARIAAttrs'])
|
|
23
23
|
return;
|
|
24
|
-
const propAttrs = el
|
|
24
|
+
const propAttrs = getAriaAttrs(el);
|
|
25
25
|
if (propAttrs.length === 0)
|
|
26
26
|
return;
|
|
27
27
|
const ariaVersion = el.rule.options?.version ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
@@ -2,7 +2,7 @@ import { createRule, ariaSpecs, getSpec } from '@markuplint/ml-core';
|
|
|
2
2
|
import { ARIA_RECOMMENDED_VERSION } from '@markuplint/ml-spec';
|
|
3
3
|
import { checkingDefaultValue } from '../wai-aria/checkings/default-value.js';
|
|
4
4
|
import { defaultOptions } from '../wai-aria/default-options.js';
|
|
5
|
-
import {
|
|
5
|
+
import { getAriaAttrs } from '../helpers.js';
|
|
6
6
|
import meta from './meta.js';
|
|
7
7
|
export default createRule({
|
|
8
8
|
meta,
|
|
@@ -15,7 +15,7 @@ export default createRule({
|
|
|
15
15
|
return;
|
|
16
16
|
if (!elSpec.globalAttrs['#ARIAAttrs'])
|
|
17
17
|
return;
|
|
18
|
-
const propAttrs = el
|
|
18
|
+
const propAttrs = getAriaAttrs(el);
|
|
19
19
|
if (propAttrs.length === 0)
|
|
20
20
|
return;
|
|
21
21
|
const ariaVersion = el.rule.options?.version ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
@@ -10,6 +10,10 @@ export default createRule({
|
|
|
10
10
|
if (!attrSpec) {
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
|
+
// The value of an attribute that is not known, or that is not output, is not the default value
|
|
14
|
+
if (attr.isDynamicValue) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
13
17
|
const spec = findAttrSpec(attrSpec, attr);
|
|
14
18
|
if (!spec) {
|
|
15
19
|
return;
|
|
@@ -2,7 +2,7 @@ import { createRule, getComputedRole, ariaSpecs, getSpec } from '@markuplint/ml-
|
|
|
2
2
|
import { ARIA_RECOMMENDED_VERSION } from '@markuplint/ml-spec';
|
|
3
3
|
import { checkingDeprecatedProps } from '../wai-aria/checkings/deprecated-props.js';
|
|
4
4
|
import { defaultOptions } from '../wai-aria/default-options.js';
|
|
5
|
-
import {
|
|
5
|
+
import { getAriaAttrs } from '../helpers.js';
|
|
6
6
|
import meta from './meta.js';
|
|
7
7
|
export default createRule({
|
|
8
8
|
meta,
|
|
@@ -15,7 +15,7 @@ export default createRule({
|
|
|
15
15
|
return;
|
|
16
16
|
if (!elSpec.globalAttrs['#ARIAAttrs'])
|
|
17
17
|
return;
|
|
18
|
-
const propAttrs = el
|
|
18
|
+
const propAttrs = getAriaAttrs(el);
|
|
19
19
|
if (propAttrs.length === 0)
|
|
20
20
|
return;
|
|
21
21
|
const ariaVersion = el.rule.options?.version ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
@@ -1,2 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
import type { RuleConfigValue } from '@markuplint/ml-core';
|
|
2
|
+
/**
|
|
3
|
+
* Checks the ancestors of an element against its content model, which holds them in two forms
|
|
4
|
+
* that this rule reads together: `forbiddenAncestors` (a deny list, any one match is a violation)
|
|
5
|
+
* and `permittedAncestors` (an allow list, an ancestor that matches none is a violation).
|
|
6
|
+
* The walk goes up from the parent and reports the nearest ancestor that violates, once.
|
|
7
|
+
*
|
|
8
|
+
* It does not enter the content of a `template` element, and it does not judge a node that is not
|
|
9
|
+
* an element of the document (a fragment, a `template` that a template engine drops): see
|
|
10
|
+
* {@link getAncestorKind}.
|
|
11
|
+
*
|
|
12
|
+
* Known limitation: `:aria(has no name)` in an entry reads a dynamic value of `aria-label` or
|
|
13
|
+
* `title` as a name, as the accessible name computation does, so `<form aria-label={x}><main>`
|
|
14
|
+
* is reported although `x` may be empty at runtime and the form then has no name.
|
|
15
|
+
*/
|
|
16
|
+
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<RuleConfigValue, undefined>>;
|
|
2
17
|
export default _default;
|
|
@@ -1,17 +1,43 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
+
import { getSpecByTagName } from '@markuplint/ml-spec';
|
|
2
3
|
import meta from './meta.js';
|
|
4
|
+
/**
|
|
5
|
+
* Checks the ancestors of an element against its content model, which holds them in two forms
|
|
6
|
+
* that this rule reads together: `forbiddenAncestors` (a deny list, any one match is a violation)
|
|
7
|
+
* and `permittedAncestors` (an allow list, an ancestor that matches none is a violation).
|
|
8
|
+
* The walk goes up from the parent and reports the nearest ancestor that violates, once.
|
|
9
|
+
*
|
|
10
|
+
* It does not enter the content of a `template` element, and it does not judge a node that is not
|
|
11
|
+
* an element of the document (a fragment, a `template` that a template engine drops): see
|
|
12
|
+
* {@link getAncestorKind}.
|
|
13
|
+
*
|
|
14
|
+
* Known limitation: `:aria(has no name)` in an entry reads a dynamic value of `aria-label` or
|
|
15
|
+
* `title` as a name, as the accessible name computation does, so `<form aria-label={x}><main>`
|
|
16
|
+
* is reported although `x` may be empty at runtime and the form then has no name.
|
|
17
|
+
*/
|
|
3
18
|
export default createRule({
|
|
4
19
|
meta: meta,
|
|
5
20
|
async verify({ document, report, t }) {
|
|
6
21
|
await document.walkOn('Element', el => {
|
|
7
|
-
const
|
|
8
|
-
const forbiddenAncestors =
|
|
9
|
-
|
|
22
|
+
const contentModel = getSpecByTagName(document.specs.specs, el.localName, el.namespaceURI)?.contentModel;
|
|
23
|
+
const forbiddenAncestors = contentModel?.forbiddenAncestors ?? [];
|
|
24
|
+
const permittedAncestors = contentModel?.permittedAncestors;
|
|
25
|
+
if (forbiddenAncestors.length === 0 && !permittedAncestors) {
|
|
10
26
|
return;
|
|
11
27
|
}
|
|
12
28
|
let ancestor = el.parentElement;
|
|
13
29
|
while (ancestor) {
|
|
14
|
-
|
|
30
|
+
const kind = getAncestorKind(ancestor);
|
|
31
|
+
if (kind === 'template-content') {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
if (kind === 'transparent') {
|
|
35
|
+
ancestor = ancestor.parentElement;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const current = ancestor;
|
|
39
|
+
if (forbiddenAncestors.some(selector => current.matches(selector)) ||
|
|
40
|
+
(permittedAncestors && !matchesPermitted(current, permittedAncestors))) {
|
|
15
41
|
report({
|
|
16
42
|
scope: el,
|
|
17
43
|
message: t('{0} must not appear as a descendant of {1}', t('the "{0}" {1}', el.localName, 'element'), t('the "{0}" {1}', ancestor.localName, 'element')),
|
|
@@ -23,3 +49,51 @@ export default createRule({
|
|
|
23
49
|
});
|
|
24
50
|
},
|
|
25
51
|
});
|
|
52
|
+
/**
|
|
53
|
+
* What `ancestor` is for the walk up the tree.
|
|
54
|
+
*
|
|
55
|
+
* - `template-content`: an HTML `template` element (`isHTMLTemplateElement`). The elements in its
|
|
56
|
+
* content belong to a separate `DocumentFragment`, so they have no ancestors in the document:
|
|
57
|
+
* `<section><template><main></main></template></section>` has a `main` element with none.
|
|
58
|
+
* markuplint holds the content as children of the `template` element, so the walk stops here.
|
|
59
|
+
* A `template` of a framework that puts the children in the element itself (React, Vue: the
|
|
60
|
+
* `templateContent` of its spec) is an `element` below: the children are in the document.
|
|
61
|
+
* - `transparent`: a node that is not an element of the document, which the walk skips as if it
|
|
62
|
+
* were not there and does not judge:
|
|
63
|
+
* - a fragment (`<>` of JSX), which `childNodes` already flattens;
|
|
64
|
+
* - a `template` element with a directive attribute (Vue's `<template v-if>`, `v-for`, `v-slot`).
|
|
65
|
+
* It is not an HTML `template` element: the template engine renders its children in place of
|
|
66
|
+
* it and drops it.
|
|
67
|
+
* - `element`: any other element.
|
|
68
|
+
*
|
|
69
|
+
* @see https://html.spec.whatwg.org/multipage/scripting.html#the-template-element
|
|
70
|
+
*/
|
|
71
|
+
function getAncestorKind(
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
73
|
+
ancestor) {
|
|
74
|
+
if (ancestor.isFragment) {
|
|
75
|
+
return 'transparent';
|
|
76
|
+
}
|
|
77
|
+
if (ancestor.localName !== 'template') {
|
|
78
|
+
return 'element';
|
|
79
|
+
}
|
|
80
|
+
for (const attr of ancestor.attributes) {
|
|
81
|
+
if (attr.isDirective) {
|
|
82
|
+
return 'transparent';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return ancestor.isHTMLTemplateElement ? 'template-content' : 'element';
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Whether `ancestor` matches an entry of `permittedAncestors`.
|
|
89
|
+
*
|
|
90
|
+
* `#custom` is not a selector: the element decides it, as the `:model()` pseudo-class does
|
|
91
|
+
* for the `#custom` of a content model category (`isCustomElement` follows the pretender).
|
|
92
|
+
* It is read only here: in `forbiddenAncestors` every entry is a selector, so `#custom` there
|
|
93
|
+
* is an id selector.
|
|
94
|
+
*/
|
|
95
|
+
function matchesPermitted(
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
97
|
+
ancestor, selectors) {
|
|
98
|
+
return selectors.some(selector => (selector === '#custom' ? ancestor.isCustomElement : ancestor.matches(selector)));
|
|
99
|
+
}
|
|
@@ -14,6 +14,10 @@ export default createRule({
|
|
|
14
14
|
if (attr.isDirective) {
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
|
+
// An attribute that the framework does not output (`async={false}`) is not an attribute of the element
|
|
18
|
+
if (attr.isAbsent) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
17
21
|
const name = attr.name;
|
|
18
22
|
const attrName = attr.nameNode;
|
|
19
23
|
// https://html.spec.whatwg.org/multipage/custom-elements.html#attr-is
|
|
@@ -4,7 +4,7 @@ export default createRule({
|
|
|
4
4
|
meta: meta,
|
|
5
5
|
defaultSeverity: 'warning',
|
|
6
6
|
verify({ document, report, t }) {
|
|
7
|
-
const dlList = [...document.
|
|
7
|
+
const dlList = [...document.deepQuerySelectorAll('dl')];
|
|
8
8
|
for (const dl of dlList) {
|
|
9
9
|
const dtList = [...dl.querySelectorAll(':scope > dt, :scope > div > dt')];
|
|
10
10
|
const names = new Set();
|
|
@@ -15,27 +15,31 @@ export default createRule({
|
|
|
15
15
|
defaultOptions: {
|
|
16
16
|
'in-document-fragment': false,
|
|
17
17
|
},
|
|
18
|
-
|
|
19
|
-
const h1Stack = [];
|
|
18
|
+
verify({ document, report, t }) {
|
|
20
19
|
if (document.nodeList.length === 0) {
|
|
21
20
|
return;
|
|
22
21
|
}
|
|
23
22
|
if (!document.rule.options['in-document-fragment'] && document.isFragment) {
|
|
24
23
|
return;
|
|
25
24
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
// The h1 elements are counted by tree: the content of a `template` element and a declarative
|
|
26
|
+
// shadow root are trees of their own, where it is not known from the markup where they are inserted
|
|
27
|
+
const h1sOfTree = new Map();
|
|
28
|
+
for (const h1 of document.deepQuerySelectorAll('h1')) {
|
|
29
|
+
const root = h1.getRootNode();
|
|
30
|
+
h1sOfTree.set(root, [...(h1sOfTree.get(root) ?? []), h1]);
|
|
31
|
+
}
|
|
32
|
+
for (const h1Stack of h1sOfTree.values()) {
|
|
33
|
+
const duplicated = h1Stack[1];
|
|
34
|
+
if (duplicated) {
|
|
35
|
+
const message = t('{0} is {1:c}', t('the "{0*}" {1}', 'h1', 'element'), 'duplicated');
|
|
36
|
+
report({
|
|
37
|
+
message,
|
|
38
|
+
line: duplicated.startLine,
|
|
39
|
+
col: duplicated.startCol,
|
|
40
|
+
raw: duplicated.raw,
|
|
41
|
+
});
|
|
29
42
|
}
|
|
30
|
-
});
|
|
31
|
-
if (h1Stack.length > 1 && h1Stack[1]) {
|
|
32
|
-
const message = t('{0} is {1:c}', t('the "{0*}" {1}', 'h1', 'element'), 'duplicated');
|
|
33
|
-
report({
|
|
34
|
-
message,
|
|
35
|
-
line: h1Stack[1].startLine,
|
|
36
|
-
col: h1Stack[1].startCol,
|
|
37
|
-
raw: h1Stack[1].raw,
|
|
38
|
-
});
|
|
39
43
|
}
|
|
40
44
|
},
|
|
41
45
|
});
|
|
@@ -1,2 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An id is unique in its tree, not in the file: the content of a `template` element and a
|
|
3
|
+
* declarative shadow root are trees of their own, where an id may repeat one of another tree.
|
|
4
|
+
* The ids are grouped by `getRootNode()`, and compared after the character references are
|
|
5
|
+
* resolved (`ab` is `ab`), as the DOM has them.
|
|
6
|
+
*
|
|
7
|
+
* @see https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute
|
|
8
|
+
* @see https://dom.spec.whatwg.org/#concept-tree
|
|
9
|
+
*/
|
|
1
10
|
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<import("@markuplint/ml-core").RuleConfigValue, undefined>>;
|
|
2
11
|
export default _default;
|
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
+
import { getDOMValue } from '../helpers.js';
|
|
2
3
|
import meta from './meta.js';
|
|
4
|
+
/**
|
|
5
|
+
* An id is unique in its tree, not in the file: the content of a `template` element and a
|
|
6
|
+
* declarative shadow root are trees of their own, where an id may repeat one of another tree.
|
|
7
|
+
* The ids are grouped by `getRootNode()`, and compared after the character references are
|
|
8
|
+
* resolved (`ab` is `ab`), as the DOM has them.
|
|
9
|
+
*
|
|
10
|
+
* @see https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute
|
|
11
|
+
* @see https://dom.spec.whatwg.org/#concept-tree
|
|
12
|
+
*/
|
|
3
13
|
export default createRule({
|
|
4
14
|
meta: meta,
|
|
5
15
|
async verify({ document, report, t }) {
|
|
6
16
|
const message = t('{0} is {1:c}', t('{0} of {1}', t('the {0}', 'value'), t('the "{0*}" {1}', 'id', 'attribute')), 'duplicated');
|
|
7
|
-
const
|
|
17
|
+
const idsOfTree = new Map();
|
|
8
18
|
await document.walkOn('Element', node => {
|
|
9
19
|
const idAttrs = node.getAttributeToken('id');
|
|
10
20
|
for (const idAttr of idAttrs) {
|
|
11
21
|
if (idAttr.isDynamicValue || idAttr.isDirective) {
|
|
12
22
|
continue;
|
|
13
23
|
}
|
|
14
|
-
const id = idAttr
|
|
15
|
-
if (
|
|
24
|
+
const id = getDOMValue(idAttr);
|
|
25
|
+
if (id == null) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const root = node.getRootNode();
|
|
29
|
+
let ids = idsOfTree.get(root);
|
|
30
|
+
if (!ids) {
|
|
31
|
+
ids = new Set();
|
|
32
|
+
idsOfTree.set(root, ids);
|
|
33
|
+
}
|
|
34
|
+
if (ids.has(id)) {
|
|
16
35
|
report({
|
|
17
36
|
scope: node,
|
|
18
37
|
message,
|
|
@@ -21,7 +40,7 @@ export default createRule({
|
|
|
21
40
|
raw: idAttr.raw,
|
|
22
41
|
});
|
|
23
42
|
}
|
|
24
|
-
|
|
43
|
+
ids.add(id);
|
|
25
44
|
}
|
|
26
45
|
});
|
|
27
46
|
},
|
|
@@ -18,7 +18,7 @@ import meta from './meta.js';
|
|
|
18
18
|
export default createRule({
|
|
19
19
|
meta,
|
|
20
20
|
verify({ document, report, t }) {
|
|
21
|
-
const tables = document.
|
|
21
|
+
const tables = document.deepQuerySelectorAll('table');
|
|
22
22
|
for (const table of tables) {
|
|
23
23
|
const grid = new Grid(table);
|
|
24
24
|
if (grid.hasOverlapped()) {
|
|
@@ -7,6 +7,10 @@ export default createRule({
|
|
|
7
7
|
defaultSeverity: 'warning',
|
|
8
8
|
async verify({ document, report, t }) {
|
|
9
9
|
await document.walkOn('Attr', attr => {
|
|
10
|
+
// An attribute that the framework does not output (`defer={false}`) is not an attribute of the element
|
|
11
|
+
if (attr.isAbsent) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
10
14
|
const attrSpec = getAttrSpecs(attr.ownerElement, document.specs);
|
|
11
15
|
if (!attrSpec) {
|
|
12
16
|
return;
|