@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
|
@@ -32,7 +32,7 @@ export default createRule({
|
|
|
32
32
|
line: idAttr.valueNode?.startLine,
|
|
33
33
|
col: idAttr.valueNode?.startCol,
|
|
34
34
|
raw: idAttr.valueNode?.raw,
|
|
35
|
-
message: t('The "{0}" attribute on a "{1}" element must have the same value as the "{2}" attribute', 'id', 'map', 'name'),
|
|
35
|
+
message: t('The "{0*}" attribute on a "{1*}" element must have the same value as the "{2*}" attribute', 'id', 'map', 'name'),
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
38
|
},
|
|
@@ -65,9 +65,9 @@ export default createRule({
|
|
|
65
65
|
const valueValue = valueNum ?? minValue;
|
|
66
66
|
const reportLE = (lowerName, lowerAttr, upperName, upperAttr, upperDefault) => {
|
|
67
67
|
const upperDesc = upperAttr
|
|
68
|
-
? t('the value of the "{0}" attribute', upperName)
|
|
69
|
-
: t('{0} when the "{1}" attribute is absent', upperDefault === 1 ? 'one' : String(upperDefault), upperName);
|
|
70
|
-
const message = t('The value of the "{0}" attribute must be less than or equal to {1}', lowerName, upperDesc);
|
|
68
|
+
? t('the value of the "{0*}" attribute', upperName)
|
|
69
|
+
: t('{0} when the "{1*}" attribute is absent', upperDefault === 1 ? 'one' : String(upperDefault), upperName);
|
|
70
|
+
const message = t('The value of the "{0*}" attribute must be less than or equal to {1}', lowerName, upperDesc);
|
|
71
71
|
const scope = lowerAttr ?? upperAttr;
|
|
72
72
|
if (scope) {
|
|
73
73
|
report({
|
|
@@ -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;
|
|
@@ -35,7 +39,7 @@ export default createRule({
|
|
|
35
39
|
raw: attr.valueNode.raw,
|
|
36
40
|
message:
|
|
37
41
|
//
|
|
38
|
-
t("don't use {0}", t('
|
|
42
|
+
t("don't use {0}", t('an {0}', t('ambiguous {0}', 'navigable target name'))) +
|
|
39
43
|
(attrSpec.type === 'NavigableTargetNameOrKeyword'
|
|
40
44
|
? t('. ') + t('Did you mean "{0}"?', valueWithUnderscore)
|
|
41
45
|
: ''),
|
|
@@ -31,7 +31,7 @@ export default createRule({
|
|
|
31
31
|
}
|
|
32
32
|
report({
|
|
33
33
|
scope: ariaHidden,
|
|
34
|
-
message: t('"aria-hidden" must not be "{0}" on an element with "hidden" value "{1}"', 'true', 'until-found'),
|
|
34
|
+
message: t('"aria-hidden" must not be "{0*}" on an element with "hidden" value "{1*}"', 'true', 'until-found'),
|
|
35
35
|
});
|
|
36
36
|
});
|
|
37
37
|
},
|
|
@@ -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()) {
|
|
@@ -36,14 +36,14 @@ export default createRule({
|
|
|
36
36
|
if (child.is(child.ELEMENT_NODE)) {
|
|
37
37
|
report({
|
|
38
38
|
scope: child,
|
|
39
|
-
message: t('Content after the end tag of the "{0}" element is not allowed', 'body'),
|
|
39
|
+
message: t('Content after the end tag of the "{0*}" element is not allowed', 'body'),
|
|
40
40
|
});
|
|
41
41
|
continue;
|
|
42
42
|
}
|
|
43
43
|
if (child.is(child.TEXT_NODE) && child.raw.trim() !== '') {
|
|
44
44
|
report({
|
|
45
45
|
scope: child,
|
|
46
|
-
message: t('Content after the end tag of the "{0}" element is not allowed', 'body'),
|
|
46
|
+
message: t('Content after the end tag of the "{0*}" element is not allowed', 'body'),
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -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,20 +1,47 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
+
import { getSpecByTagName } from '@markuplint/ml-spec';
|
|
3
|
+
import { isRenderedAway } from '../helpers.js';
|
|
2
4
|
import meta from './meta.js';
|
|
5
|
+
/**
|
|
6
|
+
* Checks the ancestors of an element against its content model, which holds them in two forms
|
|
7
|
+
* that this rule reads together: `forbiddenAncestors` (a deny list, any one match is a violation)
|
|
8
|
+
* and `permittedAncestors` (an allow list, an ancestor that matches none is a violation).
|
|
9
|
+
* The walk goes up from the parent and reports the nearest ancestor that violates, once.
|
|
10
|
+
*
|
|
11
|
+
* It does not enter the content of a `template` element, and it does not judge a node that is not
|
|
12
|
+
* an element of the document (a fragment, a `template` that a template engine drops): see
|
|
13
|
+
* {@link getAncestorKind}.
|
|
14
|
+
*
|
|
15
|
+
* Known limitation: `:aria(has no name)` in an entry reads a dynamic value of `aria-label` or
|
|
16
|
+
* `title` as a name, as the accessible name computation does, so `<form aria-label={x}><main>`
|
|
17
|
+
* is reported although `x` may be empty at runtime and the form then has no name.
|
|
18
|
+
*/
|
|
3
19
|
export default createRule({
|
|
4
20
|
meta: meta,
|
|
5
21
|
async verify({ document, report, t }) {
|
|
6
22
|
await document.walkOn('Element', el => {
|
|
7
|
-
const
|
|
8
|
-
const forbiddenAncestors =
|
|
9
|
-
|
|
23
|
+
const contentModel = getSpecByTagName(document.specs.specs, el.localName, el.namespaceURI)?.contentModel;
|
|
24
|
+
const forbiddenAncestors = contentModel?.forbiddenAncestors ?? [];
|
|
25
|
+
const permittedAncestors = contentModel?.permittedAncestors;
|
|
26
|
+
if (forbiddenAncestors.length === 0 && !permittedAncestors) {
|
|
10
27
|
return;
|
|
11
28
|
}
|
|
12
29
|
let ancestor = el.parentElement;
|
|
13
30
|
while (ancestor) {
|
|
14
|
-
|
|
31
|
+
const kind = getAncestorKind(ancestor);
|
|
32
|
+
if (kind === 'template-content') {
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
if (kind === 'transparent') {
|
|
36
|
+
ancestor = ancestor.parentElement;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const current = ancestor;
|
|
40
|
+
if (forbiddenAncestors.some(selector => current.matches(selector)) ||
|
|
41
|
+
(permittedAncestors && !matchesPermitted(current, permittedAncestors))) {
|
|
15
42
|
report({
|
|
16
43
|
scope: el,
|
|
17
|
-
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')),
|
|
44
|
+
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')),
|
|
18
45
|
});
|
|
19
46
|
break;
|
|
20
47
|
}
|
|
@@ -23,3 +50,39 @@ export default createRule({
|
|
|
23
50
|
});
|
|
24
51
|
},
|
|
25
52
|
});
|
|
53
|
+
/**
|
|
54
|
+
* What `ancestor` is for the walk up the tree.
|
|
55
|
+
*
|
|
56
|
+
* - `template-content`: an HTML `template` element (`isHTMLTemplateElement`). The elements in its
|
|
57
|
+
* content belong to a separate `DocumentFragment`, so they have no ancestors in the document:
|
|
58
|
+
* `<section><template><main></main></template></section>` has a `main` element with none.
|
|
59
|
+
* markuplint holds the content as children of the `template` element, so the walk stops here.
|
|
60
|
+
* A `template` of a framework that puts the children in the element itself (React, Vue: the
|
|
61
|
+
* `templateContent` of its spec) is an `element` below: the children are in the document.
|
|
62
|
+
* - `transparent`: a node that is not an element of the document ({@link isRenderedAway}), which
|
|
63
|
+
* the walk skips as if it were not there and does not judge.
|
|
64
|
+
* - `element`: any other element.
|
|
65
|
+
*
|
|
66
|
+
* @see https://html.spec.whatwg.org/multipage/scripting.html#the-template-element
|
|
67
|
+
*/
|
|
68
|
+
function getAncestorKind(
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
70
|
+
ancestor) {
|
|
71
|
+
if (isRenderedAway(ancestor)) {
|
|
72
|
+
return 'transparent';
|
|
73
|
+
}
|
|
74
|
+
return ancestor.isHTMLTemplateElement ? 'template-content' : 'element';
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Whether `ancestor` matches an entry of `permittedAncestors`.
|
|
78
|
+
*
|
|
79
|
+
* `#custom` is not a selector: the element decides it, as the `:model()` pseudo-class does
|
|
80
|
+
* for the `#custom` of a content model category (`isCustomElement` follows the pretender).
|
|
81
|
+
* It is read only here: in `forbiddenAncestors` every entry is a selector, so `#custom` there
|
|
82
|
+
* is an id selector.
|
|
83
|
+
*/
|
|
84
|
+
function matchesPermitted(
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
86
|
+
ancestor, selectors) {
|
|
87
|
+
return selectors.some(selector => (selector === '#custom' ? ancestor.isCustomElement : ancestor.matches(selector)));
|
|
88
|
+
}
|
|
@@ -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
|
|
@@ -25,7 +29,7 @@ export default createRule({
|
|
|
25
29
|
line: attrName?.startLine,
|
|
26
30
|
col: attrName?.startCol,
|
|
27
31
|
raw: attrName?.raw,
|
|
28
|
-
message: t('The "{0}" attribute must not be specified on an autonomous custom element', 'is'),
|
|
32
|
+
message: t('The "{0*}" attribute must not be specified on an autonomous custom element', 'is'),
|
|
29
33
|
});
|
|
30
34
|
return;
|
|
31
35
|
}
|
|
@@ -31,7 +31,7 @@ export default createRule({
|
|
|
31
31
|
if (seenScopingRoots.has(scopingRoot)) {
|
|
32
32
|
report({
|
|
33
33
|
scope: el,
|
|
34
|
-
message: t('The "{0}" attribute must be unique in the document', 'autofocus'),
|
|
34
|
+
message: t('The "{0*}" attribute must be unique in the document', 'autofocus'),
|
|
35
35
|
});
|
|
36
36
|
continue;
|
|
37
37
|
}
|
|
@@ -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
|
},
|
|
@@ -26,7 +26,7 @@ export default createRule({
|
|
|
26
26
|
if (hasPrecedingDuplicate) {
|
|
27
27
|
report({
|
|
28
28
|
scope: el,
|
|
29
|
-
message: t('The "{0}" attribute must not appear on more than one "{1}" element within the same parent', attrName, el.localName),
|
|
29
|
+
message: t('The "{0*}" attribute must not appear on more than one "{1*}" element within the same parent', attrName, el.localName),
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
}
|
|
@@ -20,7 +20,7 @@ export default createRule({
|
|
|
20
20
|
for (const el of visibleMains.slice(1)) {
|
|
21
21
|
report({
|
|
22
22
|
scope: el,
|
|
23
|
-
message: t('There must not be more than one visible "{0}" element in a document', 'main'),
|
|
23
|
+
message: t('There must not be more than one visible "{0*}" element in a document', 'main'),
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
},
|
|
@@ -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()) {
|
|
@@ -29,7 +29,7 @@ export default createRule({
|
|
|
29
29
|
for (const opt of selected.slice(1)) {
|
|
30
30
|
report({
|
|
31
31
|
scope: opt,
|
|
32
|
-
message: t('The "{0}" element cannot have more than one selected "{1}" descendant unless the "{2}" attribute is specified', 'select', 'option', 'multiple'),
|
|
32
|
+
message: t('The "{0*}" element cannot have more than one selected "{1*}" descendant unless the "{2*}" attribute is specified', 'select', 'option', 'multiple'),
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
});
|
|
@@ -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;
|
|
@@ -40,7 +40,7 @@ export default createRule({
|
|
|
40
40
|
line: valueAttr.valueNode?.startLine,
|
|
41
41
|
col: valueAttr.valueNode?.startCol,
|
|
42
42
|
raw: valueAttr.valueNode?.raw,
|
|
43
|
-
message: t('The "{0}" attribute on a "{1}" element with "{2}={3}" must be the empty string', 'value', 'input', 'type', 'file'),
|
|
43
|
+
message: t('The "{0*}" attribute on a "{1*}" element with "{2*}={3*}" must be the empty string', 'value', 'input', 'type', 'file'),
|
|
44
44
|
});
|
|
45
45
|
});
|
|
46
46
|
},
|
|
@@ -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 { checkingValue } from '../wai-aria/checkings/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,
|
|
@@ -14,14 +14,14 @@ export default createRule({
|
|
|
14
14
|
return;
|
|
15
15
|
if (!elSpec.globalAttrs['#ARIAAttrs'])
|
|
16
16
|
return;
|
|
17
|
-
const propAttrs = el
|
|
17
|
+
const propAttrs = getAriaAttrs(el);
|
|
18
18
|
if (propAttrs.length === 0)
|
|
19
19
|
return;
|
|
20
20
|
const ariaVersion = el.rule.options?.version ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
21
21
|
const computed = getComputedRole(document.specs, el, ariaVersion);
|
|
22
22
|
const { props: propSpecs } = ariaSpecs(document.specs, ariaVersion);
|
|
23
23
|
for (const attr of propAttrs) {
|
|
24
|
-
report(checkingValue({ attr, role: computed.role, propSpecs
|
|
24
|
+
report(checkingValue({ attr, role: computed.role, propSpecs }));
|
|
25
25
|
}
|
|
26
26
|
});
|
|
27
27
|
},
|
|
@@ -53,6 +53,18 @@ export default createRule({
|
|
|
53
53
|
if (one === false || one.invalidType !== 'invalid-value') {
|
|
54
54
|
continue;
|
|
55
55
|
}
|
|
56
|
+
// The value of an expression is what the code results in, so a position in it is not a position
|
|
57
|
+
// in the source (an escape, a quote): the whole value that is written is reported
|
|
58
|
+
if (attr.isExpressionValue && valueNode) {
|
|
59
|
+
report({
|
|
60
|
+
scope: attr,
|
|
61
|
+
message: one.message,
|
|
62
|
+
line: valueNode.startLine,
|
|
63
|
+
col: valueNode.startCol,
|
|
64
|
+
raw: valueNode.raw,
|
|
65
|
+
});
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
56
68
|
report({
|
|
57
69
|
scope: attr,
|
|
58
70
|
message: one.message,
|