@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
+
import { getSelectDisplaySize } from '@markuplint/ml-spec';
|
|
2
3
|
import { decodeEntities } from '@markuplint/shared';
|
|
3
4
|
import meta from './meta.js';
|
|
4
5
|
/**
|
|
@@ -17,7 +18,7 @@ import meta from './meta.js';
|
|
|
17
18
|
export default createRule({
|
|
18
19
|
meta: meta,
|
|
19
20
|
verify({ document, report, t }) {
|
|
20
|
-
for (const select of document.
|
|
21
|
+
for (const select of document.deepQuerySelectorAll('select')) {
|
|
21
22
|
if (!needPlaceholderLabelOption(select)) {
|
|
22
23
|
continue;
|
|
23
24
|
}
|
|
@@ -88,17 +89,10 @@ select) {
|
|
|
88
89
|
return value === '' || value === null;
|
|
89
90
|
}
|
|
90
91
|
/**
|
|
91
|
-
* The display size of a `<select
|
|
92
|
-
*
|
|
93
|
-
* > The display size of a select element is the result of applying the rules for parsing non-negative integers to the
|
|
94
|
-
* > value of the element's size attribute, if it has one and parsing it is successful. If applying those rules to the
|
|
95
|
-
* > attribute's value is not successful, or if the size attribute is absent, then the element's display size is 4 if
|
|
96
|
-
* > the element's multiple content attribute is present, and 1 otherwise.
|
|
92
|
+
* The display size of a `<select>`, as {@link getSelectDisplaySize} defines it.
|
|
97
93
|
*
|
|
98
94
|
* `null` when the size is given by an expression (`:size="n"`, `size={n}`): it
|
|
99
95
|
* is not known until the template runs, so the rule must not assume it is 1.
|
|
100
|
-
*
|
|
101
|
-
* @see https://html.spec.whatwg.org/multipage/form-elements.html#concept-select-size
|
|
102
96
|
*/
|
|
103
97
|
function displaySize(
|
|
104
98
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -107,23 +101,7 @@ select) {
|
|
|
107
101
|
if (size?.isDynamicValue) {
|
|
108
102
|
return null;
|
|
109
103
|
}
|
|
110
|
-
return
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* The rules for parsing non-negative integers: leading ASCII whitespace is
|
|
114
|
-
* skipped, an optional `+` or `-` is read, then the leading run of ASCII
|
|
115
|
-
* digits; whatever follows the digits is ignored (`"3abc"` is 3, `"01"` is 1).
|
|
116
|
-
* No digits, or a negative number, is an error. `-0` is not less than zero.
|
|
117
|
-
*
|
|
118
|
-
* @see https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-non-negative-integers
|
|
119
|
-
*/
|
|
120
|
-
function parseNonNegativeInteger(text) {
|
|
121
|
-
const match = /^[\t\n\f\r ]*([+-]?)(\d+)/.exec(text ?? '');
|
|
122
|
-
if (!match) {
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
const value = Number(match[2]);
|
|
126
|
-
return match[1] === '-' && value !== 0 ? null : value;
|
|
104
|
+
return getSelectDisplaySize(size?.value ?? null, select.hasAttribute('multiple'));
|
|
127
105
|
}
|
|
128
106
|
/**
|
|
129
107
|
* The first option of a select's list of options, and whether an optgroup lies
|
|
@@ -45,8 +45,8 @@ export default createRule({
|
|
|
45
45
|
if (valueNum <= maxValue)
|
|
46
46
|
return;
|
|
47
47
|
const message = maxAttr
|
|
48
|
-
? t('The value of the "{0}" attribute must be less than or equal to {1}', 'value', t('the value of the "{0}" attribute', 'max'))
|
|
49
|
-
: t('The value of the "{0}" attribute must be less than or equal to {1}', 'value', t('{0} when the "{1}" attribute is absent', 'one', 'max'));
|
|
48
|
+
? t('The value of the "{0*}" attribute must be less than or equal to {1}', 'value', t('the value of the "{0*}" attribute', 'max'))
|
|
49
|
+
: t('The value of the "{0*}" attribute must be less than or equal to {1}', 'value', t('{0} when the "{1*}" attribute is absent', 'one', 'max'));
|
|
50
50
|
report({
|
|
51
51
|
scope: valueAttr,
|
|
52
52
|
line: valueAttr.valueNode?.startLine,
|
|
@@ -10,7 +10,7 @@ export default createRule({
|
|
|
10
10
|
async verify({ document, report, t }) {
|
|
11
11
|
await document.walkOn('Element', el => {
|
|
12
12
|
const ariaVersion = el.rule.options?.ariaVersion ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
13
|
-
if (accnameMayBeMutable(el
|
|
13
|
+
if (accnameMayBeMutable(el)) {
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
16
|
if (!isExposed(el, document.specs, ariaVersion)) {
|
|
@@ -29,7 +29,7 @@ export default createRule({
|
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
31
|
// The static scan cannot decide whether the unknown content is the name.
|
|
32
|
-
if (accnameMayComeFromUnknownContent(el
|
|
32
|
+
if (accnameMayComeFromUnknownContent(el)) {
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
report({ scope: el, message: t('Require {0}', 'accessible name') });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
2
|
import { ARIA_RECOMMENDED_VERSION, mayBeFocusable } from '@markuplint/ml-spec';
|
|
3
|
+
import { getElementByIdInTree } from '../helpers.js';
|
|
3
4
|
import meta from './meta.js';
|
|
4
5
|
/**
|
|
5
6
|
* Command values from the Invoker Commands API that relate to popover behavior.
|
|
@@ -11,7 +12,8 @@ export default createRule({
|
|
|
11
12
|
defaultSeverity: 'warning',
|
|
12
13
|
verify({ document, report, t }) {
|
|
13
14
|
const ariaVersion = document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
14
|
-
|
|
15
|
+
// The target is looked for in the tree of the trigger, so the triggers of every tree are checked
|
|
16
|
+
const triggers = document.deepQuerySelectorAll('[popovertarget], [commandfor]');
|
|
15
17
|
Triggers: for (const trigger of triggers) {
|
|
16
18
|
let targetId = null;
|
|
17
19
|
const popovertarget = trigger.getAttribute('popovertarget');
|
|
@@ -27,7 +29,7 @@ export default createRule({
|
|
|
27
29
|
if (!targetId) {
|
|
28
30
|
continue;
|
|
29
31
|
}
|
|
30
|
-
const target =
|
|
32
|
+
const target = getElementByIdInTree(trigger, targetId, trigger.getAttributeNode(popovertarget ? 'popovertarget' : 'commandfor')?.isExpressionValue);
|
|
31
33
|
if (!target) {
|
|
32
34
|
continue;
|
|
33
35
|
}
|
|
@@ -1,2 +1,38 @@
|
|
|
1
|
-
|
|
1
|
+
import type { RuleConfigValue } from '@markuplint/ml-core';
|
|
2
|
+
/**
|
|
3
|
+
* Looks for the ancestor that the content model asks for (`descendantOf`) in the tree of the
|
|
4
|
+
* element, as the DOM does: the climb stops at the top of the content of a `template` element, and
|
|
5
|
+
* does not go on to the tree that holds the `template`.
|
|
6
|
+
*
|
|
7
|
+
* What the rule decides for an element whose climb ends there depends on where the content is placed:
|
|
8
|
+
*
|
|
9
|
+
* - The content of an ordinary `template` element is placed where the markup does not tell
|
|
10
|
+
* (a script clones it into the document), so the rule does not decide and reports nothing,
|
|
11
|
+
* whether or not a matching ancestor stands outside of the `template`.
|
|
12
|
+
* - The content of a declarative shadow root is placed in its host, and it is a tree of its own:
|
|
13
|
+
* an ancestor outside of the shadow root is not an ancestor of the element, so it is reported.
|
|
14
|
+
*
|
|
15
|
+
* A `template` of a template engine that renders its children in place of it (Vue's `<template v-if>`,
|
|
16
|
+
* or the `template` of a framework whose spec puts the children in the element itself) is not an HTML
|
|
17
|
+
* `template` element, and the climb goes through it as through any other element.
|
|
18
|
+
*
|
|
19
|
+
* A component whose output is not known (`MLElement#isUnresolvedComponent`: no pretender stands for
|
|
20
|
+
* it) may render the ancestor, so an element with such a component above it, up to the top of its
|
|
21
|
+
* tree, is not reported: the rule does not decide what the file does not tell. A pretender that
|
|
22
|
+
* says what the component renders is read as that element, and a web component is the element
|
|
23
|
+
* that it is written as. What renders its children in place is not one even where the parser sees it
|
|
24
|
+
* as a component: a fragment (Vue's `slot` is one to its parser) and a `template` with a directive
|
|
25
|
+
* ({@link isUnknownOutput}).
|
|
26
|
+
*
|
|
27
|
+
* When the content model says the ancestor is required only of an element with a parent node
|
|
28
|
+
* (`descendantOfWithParentNode`, as the `area` element's is), an element that has no parent node in
|
|
29
|
+
* the file ({@link hasParentNode}) is not reported:
|
|
30
|
+
*
|
|
31
|
+
* > An area element with a parent node must have a map element ancestor.
|
|
32
|
+
*
|
|
33
|
+
* @see https://dom.spec.whatwg.org/#parent-element
|
|
34
|
+
* @see https://html.spec.whatwg.org/multipage/scripting.html#the-template-element
|
|
35
|
+
* @see https://html.spec.whatwg.org/multipage/image-maps.html#the-area-element
|
|
36
|
+
*/
|
|
37
|
+
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<RuleConfigValue, undefined>>;
|
|
2
38
|
export default _default;
|
|
@@ -1,29 +1,102 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
+
import { getSpecByTagName, isShadowRoot } from '@markuplint/ml-spec';
|
|
3
|
+
import { isRenderedAway, isUnknownOutput } from '../helpers.js';
|
|
2
4
|
import meta from './meta.js';
|
|
5
|
+
/**
|
|
6
|
+
* Looks for the ancestor that the content model asks for (`descendantOf`) in the tree of the
|
|
7
|
+
* element, as the DOM does: the climb stops at the top of the content of a `template` element, and
|
|
8
|
+
* does not go on to the tree that holds the `template`.
|
|
9
|
+
*
|
|
10
|
+
* What the rule decides for an element whose climb ends there depends on where the content is placed:
|
|
11
|
+
*
|
|
12
|
+
* - The content of an ordinary `template` element is placed where the markup does not tell
|
|
13
|
+
* (a script clones it into the document), so the rule does not decide and reports nothing,
|
|
14
|
+
* whether or not a matching ancestor stands outside of the `template`.
|
|
15
|
+
* - The content of a declarative shadow root is placed in its host, and it is a tree of its own:
|
|
16
|
+
* an ancestor outside of the shadow root is not an ancestor of the element, so it is reported.
|
|
17
|
+
*
|
|
18
|
+
* A `template` of a template engine that renders its children in place of it (Vue's `<template v-if>`,
|
|
19
|
+
* or the `template` of a framework whose spec puts the children in the element itself) is not an HTML
|
|
20
|
+
* `template` element, and the climb goes through it as through any other element.
|
|
21
|
+
*
|
|
22
|
+
* A component whose output is not known (`MLElement#isUnresolvedComponent`: no pretender stands for
|
|
23
|
+
* it) may render the ancestor, so an element with such a component above it, up to the top of its
|
|
24
|
+
* tree, is not reported: the rule does not decide what the file does not tell. A pretender that
|
|
25
|
+
* says what the component renders is read as that element, and a web component is the element
|
|
26
|
+
* that it is written as. What renders its children in place is not one even where the parser sees it
|
|
27
|
+
* as a component: a fragment (Vue's `slot` is one to its parser) and a `template` with a directive
|
|
28
|
+
* ({@link isUnknownOutput}).
|
|
29
|
+
*
|
|
30
|
+
* When the content model says the ancestor is required only of an element with a parent node
|
|
31
|
+
* (`descendantOfWithParentNode`, as the `area` element's is), an element that has no parent node in
|
|
32
|
+
* the file ({@link hasParentNode}) is not reported:
|
|
33
|
+
*
|
|
34
|
+
* > An area element with a parent node must have a map element ancestor.
|
|
35
|
+
*
|
|
36
|
+
* @see https://dom.spec.whatwg.org/#parent-element
|
|
37
|
+
* @see https://html.spec.whatwg.org/multipage/scripting.html#the-template-element
|
|
38
|
+
* @see https://html.spec.whatwg.org/multipage/image-maps.html#the-area-element
|
|
39
|
+
*/
|
|
3
40
|
export default createRule({
|
|
4
41
|
meta: meta,
|
|
5
42
|
async verify({ document, report, t }) {
|
|
6
43
|
await document.walkOn('Element', el => {
|
|
7
|
-
const
|
|
8
|
-
const descendantOf =
|
|
44
|
+
const contentModel = getSpecByTagName(document.specs.specs, el.localName, el.namespaceURI)?.contentModel;
|
|
45
|
+
const descendantOf = contentModel?.descendantOf;
|
|
9
46
|
if (!descendantOf) {
|
|
10
47
|
return;
|
|
11
48
|
}
|
|
12
|
-
let ancestor = el.
|
|
49
|
+
let ancestor = el.treeParentElement;
|
|
13
50
|
let found = false;
|
|
14
51
|
while (ancestor) {
|
|
15
52
|
if (ancestor.matches(descendantOf)) {
|
|
16
53
|
found = true;
|
|
17
54
|
break;
|
|
18
55
|
}
|
|
19
|
-
ancestor
|
|
56
|
+
// What the component renders is not known, so the ancestor may be among it. The climb does not
|
|
57
|
+
// leave the tree of the element (`treeParentElement`), so a component outside a declarative shadow
|
|
58
|
+
// root is not reached
|
|
59
|
+
if (isUnknownOutput(ancestor)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
ancestor = ancestor.treeParentElement;
|
|
63
|
+
}
|
|
64
|
+
if (found || (contentModel.descendantOfWithParentNode && !hasParentNode(el))) {
|
|
65
|
+
return;
|
|
20
66
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
message: t('{0} must appear as a descendant of {1}', t('the "{0}" {1}', el.localName, 'element'), t('the "{0}" {1}', descendantOf, 'element')),
|
|
25
|
-
});
|
|
67
|
+
const root = el.getRootNode();
|
|
68
|
+
if (root.is(root.DOCUMENT_FRAGMENT_NODE) && !isShadowRoot(root)) {
|
|
69
|
+
return;
|
|
26
70
|
}
|
|
71
|
+
report({
|
|
72
|
+
scope: el,
|
|
73
|
+
message: t('{0} must appear as a descendant of {1}', t('the "{0*}" {1}', el.localName, 'element'), t('the "{0*}" {1}', descendantOf, 'element')),
|
|
74
|
+
});
|
|
27
75
|
});
|
|
28
76
|
},
|
|
29
77
|
});
|
|
78
|
+
/**
|
|
79
|
+
* Whether the element has a parent node, as far as the file tells. In a whole document every element
|
|
80
|
+
* has one. An element at the top of a file that is a fragment of a document (a partial, a component)
|
|
81
|
+
* has none: where it is inserted is not known from the markup. What is rendered away between the
|
|
82
|
+
* element and the top of the file is not a parent node: a block of a template language, which
|
|
83
|
+
* `parentNode` (and so `parentElement`) walks through, and a fragment of JSX or a `template` with
|
|
84
|
+
* a directive ({@link isRenderedAway}).
|
|
85
|
+
*
|
|
86
|
+
* @template T - The rule configuration value type
|
|
87
|
+
* @template O - The rule options type
|
|
88
|
+
* @param el - An element
|
|
89
|
+
* @returns `false` if the element is at the top of a fragment document
|
|
90
|
+
*/
|
|
91
|
+
function hasParentNode(
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
93
|
+
el) {
|
|
94
|
+
if (!el.isInFragmentDocument()) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
let parent = el.parentElement;
|
|
98
|
+
while (parent && isRenderedAway(parent)) {
|
|
99
|
+
parent = parent.parentElement;
|
|
100
|
+
}
|
|
101
|
+
return parent !== null;
|
|
102
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
declare const _default: {
|
|
2
2
|
readonly category: "structure";
|
|
3
|
+
readonly specConformance: {
|
|
4
|
+
readonly sources: readonly ["html"];
|
|
5
|
+
readonly level: "must";
|
|
6
|
+
readonly cites: readonly ["https://html.spec.whatwg.org/multipage/image-maps.html#the-area-element"];
|
|
7
|
+
};
|
|
3
8
|
};
|
|
4
9
|
export default _default;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRule, getAttrSpecs } from '@markuplint/ml-core';
|
|
2
2
|
import { attrMatches, match } from '../helpers.js';
|
|
3
|
+
import { noTranslate } from '../no-translate.js';
|
|
3
4
|
import meta from './meta.js';
|
|
4
5
|
export default createRule({
|
|
5
6
|
meta: meta,
|
|
@@ -107,7 +108,9 @@ export default createRule({
|
|
|
107
108
|
if (spec.values.length > 0 && value) {
|
|
108
109
|
const matched = spec.values.some(pattern => match(value, pattern));
|
|
109
110
|
if (!matched) {
|
|
110
|
-
const expects = spec.values.length === 1
|
|
111
|
+
const expects = spec.values.length === 1
|
|
112
|
+
? t(spec.values.map(noTranslate))
|
|
113
|
+
: t('either {0}', t(spec.values.map(noTranslate)));
|
|
111
114
|
const message = t('{0} expects {1}', t('the "{0*}" {1}', spec.name, 'attribute'), expects);
|
|
112
115
|
const attrToken = el.getAttributeToken(spec.name);
|
|
113
116
|
const valueToken = attrToken[0]?.valueNode;
|
|
@@ -8,8 +8,9 @@ export default createRule({
|
|
|
8
8
|
langs: undefined,
|
|
9
9
|
},
|
|
10
10
|
verify({ document, report, t }) {
|
|
11
|
-
for (const time of document.
|
|
12
|
-
|
|
11
|
+
for (const time of document.deepQuerySelectorAll('time:not([datetime])')) {
|
|
12
|
+
// The text of the element is the text of its descendants, which a component may render
|
|
13
|
+
if (time.hasMutableChildren() || [...time.querySelectorAll('*')].some(el => el.isUnresolvedComponent())) {
|
|
13
14
|
continue;
|
|
14
15
|
}
|
|
15
16
|
const content = time.textContent.trim();
|
|
@@ -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
|
* Without `autofocus`, the browser's dialog focusing steps fall back to the
|
|
@@ -14,8 +15,9 @@ export default createRule({
|
|
|
14
15
|
defaultSeverity: 'warning',
|
|
15
16
|
meta: meta,
|
|
16
17
|
verify({ document, report, t }) {
|
|
17
|
-
|
|
18
|
-
const
|
|
18
|
+
// The dialog is looked for in the tree of the trigger, so the triggers of every tree are checked
|
|
19
|
+
const triggers = document.deepQuerySelectorAll('button[command][commandfor]');
|
|
20
|
+
const reportedDialogs = new Set();
|
|
19
21
|
for (const trigger of triggers) {
|
|
20
22
|
const command = trigger.getAttribute('command');
|
|
21
23
|
if (!command || command.toLowerCase() !== 'show-modal') {
|
|
@@ -25,11 +27,11 @@ export default createRule({
|
|
|
25
27
|
if (!targetId) {
|
|
26
28
|
continue;
|
|
27
29
|
}
|
|
28
|
-
|
|
30
|
+
const target = getElementByIdInTree(trigger, targetId, trigger.getAttributeNode('commandfor')?.isExpressionValue);
|
|
31
|
+
if (!target) {
|
|
29
32
|
continue;
|
|
30
33
|
}
|
|
31
|
-
|
|
32
|
-
if (!target) {
|
|
34
|
+
if (reportedDialogs.has(target)) {
|
|
33
35
|
continue;
|
|
34
36
|
}
|
|
35
37
|
if (target.localName !== 'dialog') {
|
|
@@ -42,7 +44,7 @@ export default createRule({
|
|
|
42
44
|
if (autofocusDescendants.length > 0) {
|
|
43
45
|
continue;
|
|
44
46
|
}
|
|
45
|
-
|
|
47
|
+
reportedDialogs.add(target);
|
|
46
48
|
report({
|
|
47
49
|
scope: target,
|
|
48
50
|
message: t('The "{0*}" element referenced by a "{1*}" command requires an element with the "{2*}" attribute', 'dialog', 'show-modal', 'autofocus'),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
+
import { mayRenderUnknownChildren } from '../helpers.js';
|
|
2
3
|
import meta from './meta.js';
|
|
3
4
|
export default createRule({
|
|
4
5
|
meta: meta,
|
|
@@ -8,7 +9,10 @@ export default createRule({
|
|
|
8
9
|
ignoreOmittedElements: true,
|
|
9
10
|
},
|
|
10
11
|
async verify({ document, report, t }) {
|
|
11
|
-
|
|
12
|
+
// The element that a global rule asks for may be anywhere, so what any component, block or
|
|
13
|
+
// descendant renders may be it
|
|
14
|
+
const hasMutableContent = document.rule.options.ignoreHasMutableContents &&
|
|
15
|
+
document.nodeList.some(n => n.is(n.ELEMENT_NODE) && (n.hasMutableChildren() || n.isUnresolvedComponent()));
|
|
12
16
|
if (!hasMutableContent) {
|
|
13
17
|
for (const query of document.rule.value) {
|
|
14
18
|
const matched = document.querySelectorAll(query);
|
|
@@ -30,7 +34,9 @@ export default createRule({
|
|
|
30
34
|
if (el.rule.value === document.rule.value) {
|
|
31
35
|
return;
|
|
32
36
|
}
|
|
33
|
-
|
|
37
|
+
// What a descendant renders is not a child of the element that has to have the required one
|
|
38
|
+
if (el.rule.options.ignoreHasMutableContents &&
|
|
39
|
+
[...el.getPureChildNodes()].some(child => mayRenderUnknownChildren(child))) {
|
|
34
40
|
return;
|
|
35
41
|
}
|
|
36
42
|
for (const query of el.rule.value) {
|
package/lib/require-h1/index.js
CHANGED
|
@@ -14,20 +14,16 @@ export default createRule({
|
|
|
14
14
|
defaultOptions: {
|
|
15
15
|
'in-document-fragment': false,
|
|
16
16
|
},
|
|
17
|
-
|
|
18
|
-
const h1Stack = [];
|
|
17
|
+
verify({ document, report, t }) {
|
|
19
18
|
if (document.nodeList.length === 0) {
|
|
20
19
|
return;
|
|
21
20
|
}
|
|
22
21
|
if (!document.rule.options['in-document-fragment'] && document.isFragment) {
|
|
23
22
|
return;
|
|
24
23
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
if (h1Stack.length === 0) {
|
|
24
|
+
// The h1 of the document is required: what is in the content of a `template` element or in a
|
|
25
|
+
// declarative shadow root is a tree of its own, and where it is inserted is not known from the markup
|
|
26
|
+
if (!document.querySelector('h1')) {
|
|
31
27
|
const message = t('Require {0}', t('the "{0*}" {1}', 'h1', 'element'));
|
|
32
28
|
report({
|
|
33
29
|
message,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
-
import {
|
|
2
|
+
import { computeLandmarkRolesOfEachTree } from '../landmark-roles/roles.js';
|
|
3
3
|
import meta from './meta.js';
|
|
4
4
|
/**
|
|
5
5
|
* Split from the former `landmark-roles` rule (#3989): when a document has
|
|
@@ -17,22 +17,24 @@ export default createRule({
|
|
|
17
17
|
if (document.isFragment) {
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
for (const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
for (const el of roleEls) {
|
|
28
|
-
if (el.rule.disabled) {
|
|
20
|
+
// Landmarks are counted by tree: the content of a `template` element is a tree of its own
|
|
21
|
+
for (const roles of computeLandmarkRolesOfEachTree(document)) {
|
|
22
|
+
for (const role of Object.keys(roles)) {
|
|
23
|
+
const roleEls = roles[role];
|
|
24
|
+
const duplicated = 1 < roleEls.length;
|
|
25
|
+
if (!duplicated) {
|
|
29
26
|
continue;
|
|
30
27
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
for (const el of roleEls) {
|
|
29
|
+
if (el.rule.disabled) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (!hasLabel(el)) {
|
|
33
|
+
report({
|
|
34
|
+
scope: el,
|
|
35
|
+
message: t('Require {0}', t('unique {0}', 'accessible name')),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
}
|
|
@@ -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 { checkingRoleSupportsAriaProp } from '../wai-aria/checkings/role-supports-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-disallowed-props` rule (#3989): the
|
|
@@ -21,7 +21,7 @@ export default createRule({
|
|
|
21
21
|
return;
|
|
22
22
|
if (elSpec && !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;
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
* `auto` (`allowsAutoSizes`), not `loading="lazy"` alone.
|
|
10
10
|
*
|
|
11
11
|
* Gated on `srcset` being present, matching the source rule this was split
|
|
12
|
-
* from
|
|
13
|
-
*
|
|
12
|
+
* from. An `<img>` without `srcset` is left to the `@markuplint/html-spec`
|
|
13
|
+
* data of its `sizes`: the condition allows it only on a lazily loaded image
|
|
14
|
+
* (`no-disallowed-attr`) and the type only as the value `auto`
|
|
15
|
+
* (`no-invalid-attr-value`).
|
|
14
16
|
*/
|
|
15
17
|
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, undefined>>;
|
|
16
18
|
export default _default;
|
|
@@ -13,8 +13,10 @@ import meta from './meta.js';
|
|
|
13
13
|
* `auto` (`allowsAutoSizes`), not `loading="lazy"` alone.
|
|
14
14
|
*
|
|
15
15
|
* Gated on `srcset` being present, matching the source rule this was split
|
|
16
|
-
* from
|
|
17
|
-
*
|
|
16
|
+
* from. An `<img>` without `srcset` is left to the `@markuplint/html-spec`
|
|
17
|
+
* data of its `sizes`: the condition allows it only on a lazily loaded image
|
|
18
|
+
* (`no-disallowed-attr`) and the type only as the value `auto`
|
|
19
|
+
* (`no-invalid-attr-value`).
|
|
18
20
|
*/
|
|
19
21
|
export default createRule({
|
|
20
22
|
meta,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
+
import { getDOMValue, querySelectorAllInTree } from '../helpers.js';
|
|
2
3
|
import meta from './meta.js';
|
|
3
4
|
/**
|
|
4
5
|
* Require the `usemap` attribute on an `<img>` element to be a valid
|
|
@@ -27,18 +28,26 @@ export default createRule({
|
|
|
27
28
|
return;
|
|
28
29
|
if (usemapAttr.isDynamicValue)
|
|
29
30
|
return;
|
|
30
|
-
|
|
31
|
+
// The character references are resolved on both sides, as the DOM has the strings
|
|
32
|
+
const value = getDOMValue(usemapAttr) ?? usemapAttr.value;
|
|
31
33
|
if (!value.startsWith('#') || value.length <= 1)
|
|
32
34
|
return;
|
|
33
35
|
const targetName = value.slice(1);
|
|
34
|
-
const target =
|
|
36
|
+
const target = querySelectorAllInTree(el, 'map').find(candidate => {
|
|
37
|
+
const name = candidate.getAttributeNode('name');
|
|
38
|
+
if (!name) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
// A name that is not known until it is rendered may be the one that the reference means
|
|
42
|
+
return name.isDynamicValue || getDOMValue(name) === targetName;
|
|
43
|
+
});
|
|
35
44
|
if (!target) {
|
|
36
45
|
report({
|
|
37
46
|
scope: usemapAttr,
|
|
38
47
|
line: usemapAttr.valueNode?.startLine,
|
|
39
48
|
col: usemapAttr.valueNode?.startCol,
|
|
40
49
|
raw: usemapAttr.valueNode?.raw,
|
|
41
|
-
message: t('The "{0}" attribute of the "{1}" element must be a valid hash-name reference to a "{2}" element', 'usemap', 'img', 'map'),
|
|
50
|
+
message: t('The "{0*}" attribute of the "{1*}" element must be a valid hash-name reference to a "{2*}" element', 'usemap', 'img', 'map'),
|
|
42
51
|
});
|
|
43
52
|
}
|
|
44
53
|
});
|
|
@@ -216,7 +216,7 @@ function validateUrls(urls, label, reportAt, t) {
|
|
|
216
216
|
* Validate a document-rule `where` predicate. A predicate object must contain
|
|
217
217
|
* exactly one of `and` / `or` / `not` / `href_matches` / `selector_matches`.
|
|
218
218
|
*
|
|
219
|
-
* @see https://
|
|
219
|
+
* @see https://html.spec.whatwg.org/multipage/speculative-loading.html#parse-a-document-rule-predicate
|
|
220
220
|
*/
|
|
221
221
|
function validatePredicate(predicate, label, reportAt, t) {
|
|
222
222
|
if (!isPlainObject(predicate)) {
|
|
@@ -50,7 +50,7 @@ export const checkingContradictoryAriaProp = ({ attr, propSpecs, attrSpecs }) =>
|
|
|
50
50
|
}
|
|
51
51
|
return {
|
|
52
52
|
scope: attr,
|
|
53
|
-
message: t('{0} contradicts {1}', t('the "{0*}" {1}', attr.name, `ARIA ${propSpec.type}`), t('the current "{0}" {1}', equivalentHtmlAttr.htmlAttrName, 'attribute')),
|
|
53
|
+
message: t('{0} contradicts {1}', t('the "{0*}" {1}', attr.name, `ARIA ${propSpec.type}`), t('the current "{0*}" {1}', equivalentHtmlAttr.htmlAttrName, 'attribute')),
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
else if (value === 'true' &&
|
|
@@ -58,7 +58,7 @@ export const checkingContradictoryAriaProp = ({ attr, propSpecs, attrSpecs }) =>
|
|
|
58
58
|
htmlAttrSpec?.type === 'Boolean') {
|
|
59
59
|
return {
|
|
60
60
|
scope: attr,
|
|
61
|
-
message: t('{0} contradicts {1}', t('the "{0*}" {1}', attr.name, `ARIA ${propSpec.type}`), t('the implicit "{0}" {1}', equivalentHtmlAttr.htmlAttrName, 'attribute')),
|
|
61
|
+
message: t('{0} contradicts {1}', t('the "{0*}" {1}', attr.name, `ARIA ${propSpec.type}`), t('the implicit "{0*}" {1}', equivalentHtmlAttr.htmlAttrName, 'attribute')),
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mayBeFocusable } from '@markuplint/ml-spec';
|
|
1
|
+
import { getComposedParentElement, mayBeFocusable } from '@markuplint/ml-spec';
|
|
2
2
|
/**
|
|
3
3
|
* Checks whether a focusable interactive element is inside an `aria-hidden=true` subtree.
|
|
4
4
|
*
|
|
@@ -38,6 +38,10 @@ export const checkingInteractionInHidden = ({ el }) => t => {
|
|
|
38
38
|
/**
|
|
39
39
|
* Traverses the element's ancestor chain to find the closest element with `aria-hidden="true"`.
|
|
40
40
|
*
|
|
41
|
+
* The chain is the one the accessibility tree is built from ({@link getComposedParentElement}): it ends at the
|
|
42
|
+
* top of the content of an ordinary `template` element, which is not rendered and whose placement is not known,
|
|
43
|
+
* and it goes on from a declarative shadow root to its host.
|
|
44
|
+
*
|
|
41
45
|
* @param el - The element to start searching from (inclusive).
|
|
42
46
|
* @returns The `aria-hidden` attribute node if found, or `null` if no ancestor is hidden.
|
|
43
47
|
*/
|
|
@@ -50,7 +54,7 @@ el) {
|
|
|
50
54
|
if (ariaHidden?.value === 'true') {
|
|
51
55
|
return ariaHidden;
|
|
52
56
|
}
|
|
53
|
-
current = current
|
|
57
|
+
current = getComposedParentElement(current);
|
|
54
58
|
}
|
|
55
59
|
return null;
|
|
56
60
|
}
|