@markuplint/rules 5.3.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 +28 -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 +7 -1
- 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.d.ts +17 -6
- package/lib/wai-aria/checkings/presentational-children.js +259 -25
- 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
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { isInterElementWhitespace } from '@markuplint/selector';
|
|
1
2
|
import { resolveContentModel } from './content-model.js';
|
|
2
3
|
import { cmLog } from './debug.js';
|
|
3
4
|
import { order } from './order.js';
|
|
4
5
|
import { expandChildren, getContentOwner, getSlotContent } from './slot-content.js';
|
|
5
|
-
import { Collection, isTransparent, matches } from './utils.js';
|
|
6
|
+
import { Collection, isComment, isTransparent, matches } from './utils.js';
|
|
6
7
|
/**
|
|
7
8
|
* A map tracking child nodes that are currently being evaluated through
|
|
8
9
|
* a transparent content model. Used to prevent infinite recursion and
|
|
@@ -97,78 +98,13 @@ childNodes, rules, specs, options, mode) {
|
|
|
97
98
|
}
|
|
98
99
|
continue;
|
|
99
100
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// belong to the wrapper, not to the outermost element that is being matched.
|
|
103
|
-
const childSlot = getSlotContent(childNode, mode);
|
|
104
|
-
if (childSlot && (!childSlot.wrapper || getContentOwner(childNode, mode) !== childNode)) {
|
|
101
|
+
const branchGroups = transparentBranchGroups(childNode, rules, specs, options, mode);
|
|
102
|
+
if (branchGroups == null) {
|
|
105
103
|
for (const p of patterns) {
|
|
106
104
|
p.push(childNode);
|
|
107
105
|
}
|
|
108
106
|
continue;
|
|
109
107
|
}
|
|
110
|
-
const models = resolveContentModel(childNode, rules, specs, mode);
|
|
111
|
-
if (models == null || typeof models === 'boolean') {
|
|
112
|
-
for (const p of patterns) {
|
|
113
|
-
p.push(childNode);
|
|
114
|
-
}
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
const noTransparentModels = models.filter(m => !isTransparent(m));
|
|
118
|
-
if (noTransparentModels.length === models.length) {
|
|
119
|
-
for (const p of patterns) {
|
|
120
|
-
p.push(childNode);
|
|
121
|
-
}
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
const childNodesPatterns = options.evaluateConditionalChildNodes
|
|
125
|
-
? childNode.conditionalChildNodes().map(childNodes => [...expandChildren(childNode, [...childNodes], mode)])
|
|
126
|
-
: [
|
|
127
|
-
[...expandChildren(childNode, [...childNode.childNodes], mode)].filter(child => !(child.is(child.TEXT_NODE) && child.isWhitespace())),
|
|
128
|
-
];
|
|
129
|
-
const branchGroups = [];
|
|
130
|
-
for (const branchChildNodes of childNodesPatterns) {
|
|
131
|
-
const collection = new Collection([...branchChildNodes]);
|
|
132
|
-
let unmatched;
|
|
133
|
-
if (noTransparentModels.length > 0) {
|
|
134
|
-
const result = order(noTransparentModels, collection.unmatched, rules, specs, options, Number.POSITIVE_INFINITY, mode);
|
|
135
|
-
unmatched = result.unmatched;
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
unmatched = collection.unmatched;
|
|
139
|
-
}
|
|
140
|
-
const transparent = models.find(m => isTransparent(m));
|
|
141
|
-
if (!transparent || !isTransparent(transparent)) {
|
|
142
|
-
throw new Error('Unreachable code');
|
|
143
|
-
}
|
|
144
|
-
const branchChildren = [];
|
|
145
|
-
for (const _child of unmatched) {
|
|
146
|
-
const child = _child;
|
|
147
|
-
if (transparentMode.has(child)) {
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
transparentMode.set(child, true);
|
|
151
|
-
if (child.is(child.ELEMENT_NODE)) {
|
|
152
|
-
const transparentCondMatched = matches(transparent.transparent, child, specs, mode);
|
|
153
|
-
if (!transparentCondMatched.matched) {
|
|
154
|
-
branchChildren.push({
|
|
155
|
-
type: 'TRANSPARENT_MODEL_DISALLOWS',
|
|
156
|
-
matched: [],
|
|
157
|
-
unmatched: [childNode],
|
|
158
|
-
zeroMatch: false,
|
|
159
|
-
query: transparent.transparent,
|
|
160
|
-
hint: {
|
|
161
|
-
not: transparentCondMatched.not,
|
|
162
|
-
transparent: childNode,
|
|
163
|
-
},
|
|
164
|
-
});
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
branchChildren.push(child);
|
|
169
|
-
}
|
|
170
|
-
branchGroups.push(branchChildren);
|
|
171
|
-
}
|
|
172
108
|
// The transparent element itself is still a child of the current parent
|
|
173
109
|
// and must be evaluated against the parent's own content model (e.g., a
|
|
174
110
|
// `<button>` must reject an `<a href>` descendant even though `<a>`'s
|
|
@@ -223,3 +159,99 @@ childNodes, rules, specs, options, mode) {
|
|
|
223
159
|
});
|
|
224
160
|
return result;
|
|
225
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* The children that a transparent element stands for in its parent, one group per
|
|
164
|
+
* conditional branch, or `null` when the element is not looked through (its content
|
|
165
|
+
* model has no transparent part, or it is a component whose children belong to an
|
|
166
|
+
* inner wrapper).
|
|
167
|
+
*
|
|
168
|
+
* A child that is itself a transparent element is followed by the children it
|
|
169
|
+
* stands for, so that `<span><ins><ins><div>` puts the `<div>` in the `<span>`: the
|
|
170
|
+
* HTML Standard derives the content model of a transparent element from its parent
|
|
171
|
+
* again for each transparent element in which the content finds itself.
|
|
172
|
+
*
|
|
173
|
+
* > In some cases, where transparent elements are nested in each other, the process
|
|
174
|
+
* > has to be applied iteratively.
|
|
175
|
+
*
|
|
176
|
+
* @see https://html.spec.whatwg.org/multipage/dom.html#transparent-content-models
|
|
177
|
+
*
|
|
178
|
+
* The branches of a nested transparent element are merged into each branch of the
|
|
179
|
+
* outer one, the same over-approximation as the fallback documented on
|
|
180
|
+
* {@link MAX_PATTERNS}.
|
|
181
|
+
*/
|
|
182
|
+
function transparentBranchGroups(
|
|
183
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
184
|
+
childNode, rules, specs, options, mode) {
|
|
185
|
+
// A component whose children sit inside an inner wrapper element (or one of
|
|
186
|
+
// several) cannot be looked through as transparent content: the children
|
|
187
|
+
// belong to the wrapper, not to the outermost element that is being matched.
|
|
188
|
+
const childSlot = getSlotContent(childNode, mode);
|
|
189
|
+
if (childSlot && (!childSlot.wrapper || getContentOwner(childNode, mode) !== childNode)) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const models = resolveContentModel(childNode, rules, specs, mode);
|
|
193
|
+
if (models == null || typeof models === 'boolean') {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const noTransparentModels = models.filter(m => !isTransparent(m));
|
|
197
|
+
if (noTransparentModels.length === models.length) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const childNodesPatterns = options.evaluateConditionalChildNodes
|
|
201
|
+
? childNode
|
|
202
|
+
.conditionalChildNodes()
|
|
203
|
+
.map(childNodes => [...expandChildren(childNode, [...childNodes], mode)].filter(child => !isComment(child)))
|
|
204
|
+
: [
|
|
205
|
+
[...expandChildren(childNode, [...childNode.childNodes], mode)].filter(child => !isComment(child) && !(child.is(child.TEXT_NODE) && isInterElementWhitespace(child.raw))),
|
|
206
|
+
];
|
|
207
|
+
const branchGroups = [];
|
|
208
|
+
for (const branchChildNodes of childNodesPatterns) {
|
|
209
|
+
const collection = new Collection([...branchChildNodes]);
|
|
210
|
+
let unmatched;
|
|
211
|
+
if (noTransparentModels.length > 0) {
|
|
212
|
+
const result = order(noTransparentModels, collection.unmatched, rules, specs, options, Number.POSITIVE_INFINITY, mode);
|
|
213
|
+
unmatched = result.unmatched;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
unmatched = collection.unmatched;
|
|
217
|
+
}
|
|
218
|
+
const transparent = models.find(m => isTransparent(m));
|
|
219
|
+
if (!transparent || !isTransparent(transparent)) {
|
|
220
|
+
throw new Error('Unreachable code');
|
|
221
|
+
}
|
|
222
|
+
const branchChildren = [];
|
|
223
|
+
for (const _child of unmatched) {
|
|
224
|
+
const child = _child;
|
|
225
|
+
if (transparentMode.has(child)) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
transparentMode.set(child, true);
|
|
229
|
+
if (child.is(child.ELEMENT_NODE)) {
|
|
230
|
+
const transparentCondMatched = matches(transparent.transparent, child, specs, mode);
|
|
231
|
+
if (!transparentCondMatched.matched) {
|
|
232
|
+
branchChildren.push({
|
|
233
|
+
type: 'TRANSPARENT_MODEL_DISALLOWS',
|
|
234
|
+
matched: [],
|
|
235
|
+
unmatched: [childNode],
|
|
236
|
+
zeroMatch: false,
|
|
237
|
+
query: transparent.transparent,
|
|
238
|
+
hint: {
|
|
239
|
+
not: transparentCondMatched.not,
|
|
240
|
+
transparent: childNode,
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
branchChildren.push(child);
|
|
246
|
+
const nested = transparentBranchGroups(child, rules, specs, options, mode);
|
|
247
|
+
if (nested != null) {
|
|
248
|
+
branchChildren.push(...nested.flat());
|
|
249
|
+
}
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
branchChildren.push(child);
|
|
253
|
+
}
|
|
254
|
+
branchGroups.push(branchChildren);
|
|
255
|
+
}
|
|
256
|
+
return branchGroups;
|
|
257
|
+
}
|
|
@@ -24,7 +24,8 @@ export declare function getContentOwner<T extends RuleConfigValue, O extends Pla
|
|
|
24
24
|
* The children of `el` to validate: the nodes given at the usage site, passed
|
|
25
25
|
* through the pretender's `contents` when it declares any, with the children of
|
|
26
26
|
* fragment pretenders (`element: '#fragment'`) expanded in place because such a
|
|
27
|
-
* component contributes its contents directly to its parent.
|
|
27
|
+
* component contributes its contents directly to its parent. What the framework
|
|
28
|
+
* moves to the `<head>` (`movedToHead` of the spec) is not a child of `el` in the DOM.
|
|
28
29
|
*/
|
|
29
30
|
export declare function expandChildren<T extends RuleConfigValue, O extends PlainData>(el: Element<T, O>, given: readonly ChildNode<T, O>[], mode: Mode): readonly ChildNode<T, O>[];
|
|
30
31
|
/**
|
|
@@ -34,14 +34,40 @@ el, mode) {
|
|
|
34
34
|
}
|
|
35
35
|
return el;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether the framework moves `child` out of `parent`: the spec of the framework lists the elements
|
|
39
|
+
* it places in the `<head>` wherever they are written (`movedToHead`), so what `parent` contains in
|
|
40
|
+
* the DOM does not include them. The `<head>` itself keeps them: that is where they are placed.
|
|
41
|
+
*
|
|
42
|
+
* @see MLMLSpec.movedToHead in `@markuplint/ml-spec`
|
|
43
|
+
*/
|
|
44
|
+
function isMovedToHead(
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
46
|
+
parent,
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
48
|
+
child) {
|
|
49
|
+
const selectors = parent.ownerMLDocument.specs.movedToHead;
|
|
50
|
+
if (!selectors || !child.is(child.ELEMENT_NODE) || parent.localName === 'head') {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return selectors.some(selector => child.matches(selector));
|
|
54
|
+
}
|
|
37
55
|
/**
|
|
38
56
|
* The children of `el` to validate: the nodes given at the usage site, passed
|
|
39
57
|
* through the pretender's `contents` when it declares any, with the children of
|
|
40
58
|
* fragment pretenders (`element: '#fragment'`) expanded in place because such a
|
|
41
|
-
* component contributes its contents directly to its parent.
|
|
59
|
+
* component contributes its contents directly to its parent. What the framework
|
|
60
|
+
* moves to the `<head>` (`movedToHead` of the spec) is not a child of `el` in the DOM.
|
|
42
61
|
*/
|
|
43
62
|
export function expandChildren(
|
|
44
63
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
64
|
+
el, given, mode) {
|
|
65
|
+
// Only here, not in the expansion of a fragment: what a fragment contributes belongs to `el`, so
|
|
66
|
+
// it is `el` that decides whether it is moved (the `<head>` keeps it), not the fragment.
|
|
67
|
+
return fillAndExpand(el, given, mode).filter(child => !isMovedToHead(el, child));
|
|
68
|
+
}
|
|
69
|
+
function fillAndExpand(
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
45
71
|
el, given, mode) {
|
|
46
72
|
const slotContent = getSlotContent(el, mode);
|
|
47
73
|
const children = slotContent ? slotContent.fill(given) : given;
|
|
@@ -71,7 +97,7 @@ node, mode) {
|
|
|
71
97
|
function expandFragment(
|
|
72
98
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
73
99
|
node, mode) {
|
|
74
|
-
return isFragmentPretender(node, mode) ?
|
|
100
|
+
return isFragmentPretender(node, mode) ? fillAndExpand(node, [...node.childNodes], mode) : [node];
|
|
75
101
|
}
|
|
76
102
|
/**
|
|
77
103
|
* A violation on an element that only the pretender renders (`contents`) is reported
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { isInterElementWhitespace } from '@markuplint/selector';
|
|
1
2
|
import { order } from './order.js';
|
|
2
3
|
import { representTransparentNodes } from './represent-transparent-nodes.js';
|
|
3
4
|
import { expandChildren } from './slot-content.js';
|
|
5
|
+
import { isComment } from './utils.js';
|
|
4
6
|
/**
|
|
5
7
|
* Entry point for validating an element's child nodes against a content model definition.
|
|
6
8
|
* Handles the three forms of content model: `false` (no content allowed), `true` (any content
|
|
@@ -23,10 +25,10 @@ el, rules, specs, options, mode) {
|
|
|
23
25
|
? el.conditionalChildNodes().map(childNodes => [...childNodes])
|
|
24
26
|
: [[...el.childNodes]];
|
|
25
27
|
const childNodesPatterns = givenPatterns.map(given => {
|
|
26
|
-
const children = expandChildren(el, given, mode);
|
|
28
|
+
const children = expandChildren(el, given, mode).filter(child => !isComment(child));
|
|
27
29
|
return options.evaluateConditionalChildNodes
|
|
28
30
|
? [...children]
|
|
29
|
-
: children.filter(child => !(child.is(child.TEXT_NODE) && child.
|
|
31
|
+
: children.filter(child => !(child.is(child.TEXT_NODE) && isInterElementWhitespace(child.raw)));
|
|
30
32
|
});
|
|
31
33
|
return childNodesPatterns.flatMap(childNodes => {
|
|
32
34
|
if (contents === false) {
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { ChildNode, Hints, MissingNodeReason, Mode, RepeatSign, Specs } from './types.js';
|
|
2
2
|
import type { PermittedContentPattern, PermittedContentChoice, PermittedContentOneOrMore, PermittedContentOptional, PermittedContentRequire, PermittedContentTransparent, PermittedContentZeroOrMore, Model } from '@markuplint/ml-spec';
|
|
3
3
|
import type { ReadonlyDeep } from 'type-fest';
|
|
4
|
+
/**
|
|
5
|
+
* Whether a child node is a comment, which a content model does not see.
|
|
6
|
+
* Without this, a comment is matched like an element and takes the place of
|
|
7
|
+
* one the model requires (`<html><!-- c --><head>` leaves `head` unmatched).
|
|
8
|
+
*
|
|
9
|
+
* @param node - The child node to test.
|
|
10
|
+
* @returns True if the node is a comment node.
|
|
11
|
+
* @see https://html.spec.whatwg.org/multipage/dom.html#content-models
|
|
12
|
+
* > Inter-element whitespace, comment nodes, and processing instruction nodes must be ignored when establishing whether an element's contents match the element's content model or not
|
|
13
|
+
*/
|
|
14
|
+
export declare function isComment(node: ChildNode): boolean;
|
|
4
15
|
/**
|
|
5
16
|
* Determines whether a given value is a terminal model (a selector string or an array
|
|
6
17
|
* of selector strings) rather than a nested array of content model patterns.
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { createSelector } from '@markuplint/selector';
|
|
2
2
|
import { bgGreen, green, bgRed, bgBlue, blue, bgMagenta, cyan } from './debug.js';
|
|
3
3
|
import { transparentMode } from './represent-transparent-nodes.js';
|
|
4
|
+
/**
|
|
5
|
+
* Whether a child node is a comment, which a content model does not see.
|
|
6
|
+
* Without this, a comment is matched like an element and takes the place of
|
|
7
|
+
* one the model requires (`<html><!-- c --><head>` leaves `head` unmatched).
|
|
8
|
+
*
|
|
9
|
+
* @param node - The child node to test.
|
|
10
|
+
* @returns True if the node is a comment node.
|
|
11
|
+
* @see https://html.spec.whatwg.org/multipage/dom.html#content-models
|
|
12
|
+
* > Inter-element whitespace, comment nodes, and processing instruction nodes must be ignored when establishing whether an element's contents match the element's content model or not
|
|
13
|
+
*/
|
|
14
|
+
export function isComment(
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
16
|
+
node) {
|
|
17
|
+
return node.is(node.COMMENT_NODE);
|
|
18
|
+
}
|
|
4
19
|
/**
|
|
5
20
|
* Determines whether a given value is a terminal model (a selector string or an array
|
|
6
21
|
* of selector strings) rather than a nested array of content model patterns.
|
|
@@ -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
|
|
@@ -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,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Looks for the ancestor that the content model asks for (`descendantOf`) in the tree of the
|
|
3
|
+
* element, as the DOM does: the climb stops at the top of the content of a `template` element, and
|
|
4
|
+
* does not go on to the tree that holds the `template`.
|
|
5
|
+
*
|
|
6
|
+
* What the rule decides for an element whose climb ends there depends on where the content is placed:
|
|
7
|
+
*
|
|
8
|
+
* - The content of an ordinary `template` element is placed where the markup does not tell
|
|
9
|
+
* (a script clones it into the document), so the rule does not decide and reports nothing,
|
|
10
|
+
* whether or not a matching ancestor stands outside of the `template`.
|
|
11
|
+
* - The content of a declarative shadow root is placed in its host, and it is a tree of its own:
|
|
12
|
+
* an ancestor outside of the shadow root is not an ancestor of the element, so it is reported.
|
|
13
|
+
*
|
|
14
|
+
* A `template` of a template engine that renders its children in place of it (Vue's `<template v-if>`,
|
|
15
|
+
* or the `template` of a framework whose spec puts the children in the element itself) is not an HTML
|
|
16
|
+
* `template` element, and the climb goes through it as through any other element.
|
|
17
|
+
*
|
|
18
|
+
* @see https://dom.spec.whatwg.org/#parent-element
|
|
19
|
+
* @see https://html.spec.whatwg.org/multipage/scripting.html#the-template-element
|
|
20
|
+
*/
|
|
1
21
|
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<import("@markuplint/ml-core").RuleConfigValue, undefined>>;
|
|
2
22
|
export default _default;
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
+
import { isShadowRoot } from '@markuplint/ml-spec';
|
|
2
3
|
import meta from './meta.js';
|
|
4
|
+
/**
|
|
5
|
+
* Looks for the ancestor that the content model asks for (`descendantOf`) in the tree of the
|
|
6
|
+
* element, as the DOM does: the climb stops at the top of the content of a `template` element, and
|
|
7
|
+
* does not go on to the tree that holds the `template`.
|
|
8
|
+
*
|
|
9
|
+
* What the rule decides for an element whose climb ends there depends on where the content is placed:
|
|
10
|
+
*
|
|
11
|
+
* - The content of an ordinary `template` element is placed where the markup does not tell
|
|
12
|
+
* (a script clones it into the document), so the rule does not decide and reports nothing,
|
|
13
|
+
* whether or not a matching ancestor stands outside of the `template`.
|
|
14
|
+
* - The content of a declarative shadow root is placed in its host, and it is a tree of its own:
|
|
15
|
+
* an ancestor outside of the shadow root is not an ancestor of the element, so it is reported.
|
|
16
|
+
*
|
|
17
|
+
* A `template` of a template engine that renders its children in place of it (Vue's `<template v-if>`,
|
|
18
|
+
* or the `template` of a framework whose spec puts the children in the element itself) is not an HTML
|
|
19
|
+
* `template` element, and the climb goes through it as through any other element.
|
|
20
|
+
*
|
|
21
|
+
* @see https://dom.spec.whatwg.org/#parent-element
|
|
22
|
+
* @see https://html.spec.whatwg.org/multipage/scripting.html#the-template-element
|
|
23
|
+
*/
|
|
3
24
|
export default createRule({
|
|
4
25
|
meta: meta,
|
|
5
26
|
async verify({ document, report, t }) {
|
|
@@ -9,21 +30,26 @@ export default createRule({
|
|
|
9
30
|
if (!descendantOf) {
|
|
10
31
|
return;
|
|
11
32
|
}
|
|
12
|
-
let ancestor = el.
|
|
33
|
+
let ancestor = el.treeParentElement;
|
|
13
34
|
let found = false;
|
|
14
35
|
while (ancestor) {
|
|
15
36
|
if (ancestor.matches(descendantOf)) {
|
|
16
37
|
found = true;
|
|
17
38
|
break;
|
|
18
39
|
}
|
|
19
|
-
ancestor = ancestor.
|
|
40
|
+
ancestor = ancestor.treeParentElement;
|
|
20
41
|
}
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
42
|
+
if (found) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const root = el.getRootNode();
|
|
46
|
+
if (root.is(root.DOCUMENT_FRAGMENT_NODE) && !isShadowRoot(root)) {
|
|
47
|
+
return;
|
|
26
48
|
}
|
|
49
|
+
report({
|
|
50
|
+
scope: el,
|
|
51
|
+
message: t('{0} must appear as a descendant of {1}', t('the "{0}" {1}', el.localName, 'element'), t('the "{0}" {1}', descendantOf, 'element')),
|
|
52
|
+
});
|
|
27
53
|
});
|
|
28
54
|
},
|
|
29
55
|
});
|
|
@@ -8,7 +8,7 @@ export default createRule({
|
|
|
8
8
|
langs: undefined,
|
|
9
9
|
},
|
|
10
10
|
verify({ document, report, t }) {
|
|
11
|
-
for (const time of document.
|
|
11
|
+
for (const time of document.deepQuerySelectorAll('time:not([datetime])')) {
|
|
12
12
|
if (time.hasMutableChildren()) {
|
|
13
13
|
continue;
|
|
14
14
|
}
|
|
@@ -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'),
|
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;
|