@markuplint/rules 5.1.1 → 5.3.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 +47 -0
- package/CLAUDE.md +19 -19
- package/lib/aria-prop-requires-role/index.js +2 -1
- package/lib/attr-check.js +6 -12
- package/lib/attr-eligibility.d.ts +15 -8
- package/lib/attr-eligibility.js +19 -10
- package/lib/attr-order/index.js +24 -18
- package/lib/case-sensitive-attr-name/index.js +24 -5
- package/lib/class-naming/index.js +6 -0
- package/lib/debug.d.ts +8 -1
- package/lib/debug.js +8 -1
- package/lib/element-supports-aria-prop/index.js +2 -1
- package/lib/form-attr-references-form/index.d.ts +6 -3
- package/lib/form-attr-references-form/index.js +6 -3
- package/lib/helpers.d.ts +111 -1
- package/lib/helpers.js +212 -2
- package/lib/input-list-references-datalist/index.d.ts +8 -6
- package/lib/input-list-references-datalist/index.js +8 -6
- package/lib/label-for-references-labelable/index.d.ts +8 -5
- package/lib/label-for-references-labelable/index.js +8 -5
- package/lib/landmark-roles/roles.js +14 -2
- package/lib/map-id-name-match/index.d.ts +3 -3
- package/lib/map-id-name-match/index.js +3 -3
- package/lib/no-ambiguous-navigable-target-names/index.js +2 -1
- package/lib/no-aria-hidden-on-hidden-until-found/index.d.ts +9 -6
- package/lib/no-aria-hidden-on-hidden-until-found/index.js +9 -6
- package/lib/no-aria-hidden-on-hidden-until-found/meta.d.ts +1 -1
- package/lib/no-aria-hidden-on-hidden-until-found/meta.js +1 -1
- package/lib/no-aria-on-unsupported-element/index.js +2 -2
- package/lib/no-aria-on-unsupported-element/meta.d.ts +1 -1
- package/lib/no-aria-on-unsupported-element/meta.js +1 -1
- package/lib/no-boolean-attr-value/index.js +2 -2
- package/lib/no-content-after-body/index.d.ts +2 -2
- package/lib/no-content-after-body/index.js +2 -2
- package/lib/no-contradictory-aria-prop/index.js +2 -1
- package/lib/no-default-aria-value/index.js +2 -1
- package/lib/no-default-value/index.js +2 -2
- package/lib/no-deprecated-aria-prop/index.js +2 -1
- package/lib/no-deprecated-attr/index.js +2 -1
- package/lib/no-disallowed-attr/index.d.ts +7 -1
- package/lib/no-disallowed-attr/index.js +13 -20
- package/lib/no-duplicate-attr/index.js +1 -2
- package/lib/no-duplicate-autofocus/index.js +2 -2
- package/lib/no-duplicate-h1/index.d.ts +1 -1
- package/lib/no-duplicate-h1/index.js +1 -1
- package/lib/no-empty-table-track/index.d.ts +6 -2
- package/lib/no-empty-table-track/index.js +6 -2
- package/lib/no-experimental-features/index.d.ts +5 -1
- package/lib/no-experimental-features/index.js +6 -8
- package/lib/no-hardcoded-id/index.js +1 -1
- package/lib/no-ineffective-attr/index.js +2 -2
- package/lib/no-input-file-value/index.d.ts +10 -4
- package/lib/no-input-file-value/index.js +10 -4
- package/lib/no-invalid-aria-prop-value/index.js +2 -1
- package/lib/no-invalid-attr-value/index.d.ts +3 -0
- package/lib/no-invalid-attr-value/index.js +9 -24
- package/lib/no-nonstandard-features/index.d.ts +5 -1
- package/lib/no-nonstandard-features/index.js +6 -8
- package/lib/no-obsolete-attr/index.js +3 -3
- package/lib/no-prohibited-naming/index.js +2 -1
- package/lib/no-redundant-accessible-name/index.js +17 -1
- package/lib/no-redundant-aria-prop/index.js +2 -1
- package/lib/no-refer-to-non-existent-id/index.js +4 -4
- package/lib/no-restricted-attr/index.d.ts +4 -0
- package/lib/no-restricted-attr/index.js +5 -22
- package/lib/no-stray-head-or-body-tag/index.d.ts +6 -4
- package/lib/no-stray-head-or-body-tag/index.js +11 -6
- package/lib/no-unknown-attr/index.d.ts +7 -1
- package/lib/no-unknown-attr/index.js +11 -25
- package/lib/no-unpaired-srcset-sizes/index.d.ts +3 -2
- package/lib/no-unpaired-srcset-sizes/index.js +23 -17
- package/lib/no-unsupported-browser-features/compat-data.d.ts +15 -2
- package/lib/no-unsupported-browser-features/compat-data.js +48 -3
- package/lib/no-unsupported-browser-features/index.d.ts +5 -1
- package/lib/no-unsupported-browser-features/index.js +6 -7
- package/lib/permitted-contents/choice.js +19 -8
- package/lib/permitted-contents/content-model.d.ts +24 -1
- package/lib/permitted-contents/content-model.js +56 -9
- package/lib/permitted-contents/count-pattern.js +36 -13
- package/lib/permitted-contents/debug.browser.d.ts +18 -0
- package/lib/permitted-contents/debug.browser.js +27 -0
- package/lib/permitted-contents/debug.d.ts +18 -0
- package/lib/permitted-contents/debug.js +27 -0
- package/lib/permitted-contents/index.js +2 -2
- package/lib/permitted-contents/matches-selector.js +40 -18
- package/lib/permitted-contents/order.js +23 -11
- package/lib/permitted-contents/represent-transparent-nodes.js +3 -1
- package/lib/permitted-contents/transparent.js +6 -2
- package/lib/placeholder-label-option/index.d.ts +3 -3
- package/lib/placeholder-label-option/index.js +163 -37
- package/lib/progress-value-bounds/index.d.ts +1 -1
- package/lib/progress-value-bounds/index.js +1 -1
- package/lib/require-attr/index.d.ts +6 -1
- package/lib/require-attr/index.js +13 -5
- package/lib/require-h1/index.d.ts +1 -1
- package/lib/require-h1/index.js +1 -1
- package/lib/role-supports-aria-prop/index.js +2 -1
- package/lib/sizes-auto-requires-lazy-loading/index.d.ts +3 -1
- package/lib/sizes-auto-requires-lazy-loading/index.js +30 -16
- package/lib/srcset-sizes-constraint/parse-srcset.d.ts +6 -5
- package/lib/srcset-sizes-constraint/parse-srcset.js +8 -7
- package/lib/srcset-sizes-constraint/sibling-helpers.d.ts +31 -0
- package/lib/srcset-sizes-constraint/sibling-helpers.js +58 -0
- package/lib/valid-speculation-rules/index.js +1 -1
- package/lib/wai-aria/checkings/contradictory-aria-prop.js +3 -5
- package/lib/wai-aria/checkings/default-value.js +1 -1
- package/lib/wai-aria/checkings/deprecated-props.js +2 -2
- package/lib/wai-aria/checkings/element-supports-aria-prop.js +5 -4
- package/lib/wai-aria/checkings/interaction-in-hidden.d.ts +1 -1
- package/lib/wai-aria/checkings/interaction-in-hidden.js +1 -1
- package/lib/wai-aria/checkings/no-global-prop.js +1 -1
- package/lib/wai-aria/checkings/presentational-children.d.ts +4 -4
- package/lib/wai-aria/checkings/presentational-children.js +6 -5
- package/lib/wai-aria/checkings/prohibited-naming.js +5 -4
- package/lib/wai-aria/checkings/redundant-aria-prop.js +3 -5
- package/lib/wai-aria/checkings/required-prop.js +1 -4
- package/lib/wai-aria/checkings/role-supports-aria-prop.js +4 -3
- package/lib/wai-aria/checkings/tab-requires-tabpanel.d.ts +3 -2
- package/lib/wai-aria/checkings/tab-requires-tabpanel.js +3 -2
- package/lib/wai-aria/checkings/value.d.ts +1 -1
- package/lib/wai-aria/checkings/value.js +2 -2
- package/package.json +19 -19
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
+
import { decodeEntities } from '@markuplint/shared';
|
|
2
3
|
import meta from './meta.js';
|
|
3
4
|
/**
|
|
4
5
|
* HTML LS §4.10.7 The select element:
|
|
@@ -7,9 +8,9 @@ import meta from './meta.js';
|
|
|
7
8
|
* > multiple attribute specified, and has a display size of 1, then the
|
|
8
9
|
* > select element must have a placeholder label option.
|
|
9
10
|
*
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* `needPlaceholderLabelOption` gates whether the requirement applies to this
|
|
12
|
+
* `<select>`; `hasPlaceholderLabelOption` then decides whether an existing
|
|
13
|
+
* option already satisfies it.
|
|
13
14
|
*
|
|
14
15
|
* @see https://html.spec.whatwg.org/multipage/form-elements.html#the-select-element
|
|
15
16
|
*/
|
|
@@ -17,10 +18,10 @@ export default createRule({
|
|
|
17
18
|
meta: meta,
|
|
18
19
|
verify({ document, report, t }) {
|
|
19
20
|
for (const select of document.querySelectorAll('select')) {
|
|
20
|
-
if (
|
|
21
|
+
if (!needPlaceholderLabelOption(select)) {
|
|
21
22
|
continue;
|
|
22
23
|
}
|
|
23
|
-
if (
|
|
24
|
+
if (hasPlaceholderLabelOption(select)) {
|
|
24
25
|
continue;
|
|
25
26
|
}
|
|
26
27
|
report({
|
|
@@ -50,54 +51,179 @@ select) {
|
|
|
50
51
|
if (hasMultiple) {
|
|
51
52
|
return false;
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
-
if (size !== '1') {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
return true;
|
|
54
|
+
return displaySize(select) === 1;
|
|
58
55
|
}
|
|
59
56
|
/**
|
|
60
57
|
* HTML LS §4.10.7 The select element — placeholder label option definition:
|
|
61
58
|
*
|
|
62
|
-
* > If a select element has a required attribute specified, and has a
|
|
63
|
-
* >
|
|
64
|
-
* >
|
|
65
|
-
* >
|
|
66
|
-
* > element), then that option is the select element's placeholder label
|
|
67
|
-
* > option.
|
|
59
|
+
* > If a select element has a required attribute specified, and has a display size of 1; and if the value of the first
|
|
60
|
+
* > option element in the select element's list of options (if any) is the empty string, and that option element does
|
|
61
|
+
* > not have an ancestor optgroup in between itself and the select element, then that option is the select element's
|
|
62
|
+
* > placeholder label option.
|
|
68
63
|
*
|
|
69
|
-
* The
|
|
70
|
-
*
|
|
71
|
-
*
|
|
64
|
+
* The conditions on the `<select>` itself (required, display size) are
|
|
65
|
+
* `needPlaceholderLabelOption`'s, which `verify` asks first; this decides the
|
|
66
|
+
* rest. The "(if any)" clause requires a first option to exist for the
|
|
67
|
+
* placeholder label option to exist at all; an empty `<select>` therefore has
|
|
68
|
+
* no placeholder label option and must be reported by `verify`.
|
|
72
69
|
*
|
|
73
70
|
* @see https://html.spec.whatwg.org/multipage/form-elements.html#placeholder-label-option
|
|
74
71
|
*/
|
|
75
72
|
function hasPlaceholderLabelOption(
|
|
76
73
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
77
74
|
select) {
|
|
78
|
-
// >
|
|
79
|
-
|
|
75
|
+
// > the first option element in the select element's list of options (if any)
|
|
76
|
+
// No first option → no placeholder label option exists.
|
|
77
|
+
const first = firstOption(select, false);
|
|
78
|
+
if (!first) {
|
|
80
79
|
return false;
|
|
81
80
|
}
|
|
82
|
-
// > does not have
|
|
83
|
-
if (
|
|
81
|
+
// > does not have an ancestor optgroup in between itself and the select element
|
|
82
|
+
if (first.insideOptgroup) {
|
|
84
83
|
return false;
|
|
85
84
|
}
|
|
86
|
-
// >
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
// > the value of [it] is the empty string
|
|
86
|
+
// An option whose value cannot be known (a template interpolation as its text) is not reported.
|
|
87
|
+
const value = optionValue(first.option);
|
|
88
|
+
return value === '' || value === null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
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.
|
|
97
|
+
*
|
|
98
|
+
* `null` when the size is given by an expression (`:size="n"`, `size={n}`): it
|
|
99
|
+
* 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
|
+
*/
|
|
103
|
+
function displaySize(
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
105
|
+
select) {
|
|
106
|
+
const size = select.getAttributeNode('size');
|
|
107
|
+
if (size?.isDynamicValue) {
|
|
108
|
+
return null;
|
|
90
109
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
110
|
+
return parseNonNegativeInteger(size?.value ?? null) ?? (select.hasAttribute('multiple') ? 4 : 1);
|
|
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;
|
|
96
124
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
125
|
+
const value = Number(match[2]);
|
|
126
|
+
return match[1] === '-' && value !== 0 ? null : value;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The first option of a select's list of options, and whether an optgroup lies
|
|
130
|
+
* between it and the select.
|
|
131
|
+
*
|
|
132
|
+
* Following the algorithm literally, an `option` found while descending is
|
|
133
|
+
* appended and its subtree is skipped; so are the subtrees of a nested `select`,
|
|
134
|
+
* `hr` or `datalist`, and of an `optgroup` that has an `optgroup` ancestor below
|
|
135
|
+
* the select. Any other element, such as the wrapper that a customizable select
|
|
136
|
+
* allows, is looked through.
|
|
137
|
+
*
|
|
138
|
+
* @see https://html.spec.whatwg.org/multipage/form-elements.html#concept-select-option-list
|
|
139
|
+
*/
|
|
140
|
+
function firstOption(
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
142
|
+
parent, insideOptgroup) {
|
|
143
|
+
for (const child of parent.childNodes) {
|
|
144
|
+
if (!child.is(child.ELEMENT_NODE)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
switch (child.localName) {
|
|
148
|
+
case 'option': {
|
|
149
|
+
return { option: child, insideOptgroup };
|
|
150
|
+
}
|
|
151
|
+
case 'select':
|
|
152
|
+
case 'hr':
|
|
153
|
+
case 'datalist': {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
case 'optgroup': {
|
|
157
|
+
if (insideOptgroup) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const found = firstOption(child, true);
|
|
161
|
+
if (found) {
|
|
162
|
+
return found;
|
|
163
|
+
}
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
default: {
|
|
167
|
+
const found = firstOption(child, insideOptgroup);
|
|
168
|
+
if (found) {
|
|
169
|
+
return found;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
100
173
|
}
|
|
101
|
-
|
|
102
|
-
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The value of an `<option>`, or `null` when it cannot be known statically.
|
|
178
|
+
*
|
|
179
|
+
* > The value of an option element is the value of the value content attribute, if there is one, or, if there is not,
|
|
180
|
+
* > the result of get HTML-aware text content given this and false.
|
|
181
|
+
*
|
|
182
|
+
* @see https://html.spec.whatwg.org/multipage/form-elements.html#attr-option-value
|
|
183
|
+
*/
|
|
184
|
+
function optionValue(
|
|
185
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
186
|
+
option) {
|
|
187
|
+
const valueAttr = option.getAttributeNode('value');
|
|
188
|
+
if (valueAttr) {
|
|
189
|
+
// A value given by an expression (`:value="v"`, `value={v}`) is not known until the template runs.
|
|
190
|
+
return valueAttr.isDynamicValue ? null : valueAttr.value;
|
|
191
|
+
}
|
|
192
|
+
const text = htmlAwareTextContent(option);
|
|
193
|
+
return text === null ? null : stripAndCollapseAsciiWhitespace(text);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Get HTML-aware text content with `includeAltText` false: the data of the
|
|
197
|
+
* Text descendants. The algorithm also skips `script` and `img` subtrees; that
|
|
198
|
+
* changes nothing here, because the HTML parser's tree does not expose a
|
|
199
|
+
* script's source as a Text node and an `img` has no text descendants.
|
|
200
|
+
* `null` when a template construct stands where text would be.
|
|
201
|
+
*
|
|
202
|
+
* @see https://html.spec.whatwg.org/multipage/form-elements.html#get-html-aware-text-content
|
|
203
|
+
*/
|
|
204
|
+
function htmlAwareTextContent(
|
|
205
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
206
|
+
element) {
|
|
207
|
+
let text = '';
|
|
208
|
+
for (const child of element.childNodes) {
|
|
209
|
+
if (child.is(child.TEXT_NODE)) {
|
|
210
|
+
// The node holds the source text; a character reference such as ` ` has to be decoded first.
|
|
211
|
+
text += decodeEntities(child.textContent ?? '');
|
|
212
|
+
}
|
|
213
|
+
else if (child.is(child.ELEMENT_NODE)) {
|
|
214
|
+
const inner = htmlAwareTextContent(child);
|
|
215
|
+
if (inner === null) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
text += inner;
|
|
219
|
+
}
|
|
220
|
+
else if (child.is(child.MARKUPLINT_PREPROCESSOR_BLOCK)) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return text;
|
|
225
|
+
}
|
|
226
|
+
/** ASCII whitespace only: `trim()` would also strip a no-break space, which is part of the text. */
|
|
227
|
+
function stripAndCollapseAsciiWhitespace(text) {
|
|
228
|
+
return text.replaceAll(/[\t\n\f\r ]+/g, ' ').replaceAll(/^ | $/g, '');
|
|
103
229
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Enforce HTML LS §4.10.
|
|
2
|
+
* Enforce HTML LS §4.10.13 inequalities for `<progress>`:
|
|
3
3
|
* `value ≤ max` when both attributes are present, and `value ≤ 1` when
|
|
4
4
|
* `max` is absent. Authored-but-unparsable values, out-of-range `max`
|
|
5
5
|
* (≤ 0), and negative `value` are deferred to `no-invalid-attr-value`.
|
|
@@ -11,7 +11,7 @@ function parseFloatAttr(raw) {
|
|
|
11
11
|
return Number.isFinite(n) ? n : null;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
* Enforce HTML LS §4.10.
|
|
14
|
+
* Enforce HTML LS §4.10.13 inequalities for `<progress>`:
|
|
15
15
|
* `value ≤ max` when both attributes are present, and `value ≤ 1` when
|
|
16
16
|
* `max` is absent. Authored-but-unparsable values, out-of-range `max`
|
|
17
17
|
* (≤ 0), and negative `value` are deferred to `no-invalid-attr-value`.
|
|
@@ -13,7 +13,12 @@ type Attr = {
|
|
|
13
13
|
* Configuration options for the `require-attr` rule.
|
|
14
14
|
*/
|
|
15
15
|
type Options = {
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Attribute names to exclude from require-attribute checks, compared as the DOM compares attribute
|
|
18
|
+
* names.
|
|
19
|
+
*
|
|
20
|
+
* @see https://markuplint.dev/docs/rules/no-unknown-attr#attribute-name-case
|
|
21
|
+
*/
|
|
17
22
|
readonly ignoreAttrs?: readonly string[];
|
|
18
23
|
};
|
|
19
24
|
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<RequiredAttributes, Options>>;
|
|
@@ -12,7 +12,8 @@ export default createRule({
|
|
|
12
12
|
}
|
|
13
13
|
const customRequiredAttrs = typeof el.rule.value === 'string' ? [el.rule.value] : el.rule.value;
|
|
14
14
|
const attrSpec = getAttrSpecs(el, document.specs);
|
|
15
|
-
const ignoreAttrs =
|
|
15
|
+
const ignoreAttrs = el.rule.options.ignoreAttrs ?? [];
|
|
16
|
+
const isIgnored = (name) => ignoreAttrs.some(ignored => el.isSameAttrName(ignored, name));
|
|
16
17
|
const attributeSpecs = {};
|
|
17
18
|
if (attrSpec && el.elementType === 'html') {
|
|
18
19
|
for (const spec of attrSpec) {
|
|
@@ -39,6 +40,13 @@ export default createRule({
|
|
|
39
40
|
values.push(req.value);
|
|
40
41
|
}
|
|
41
42
|
}
|
|
43
|
+
// A name of the rule value that the spec table names too is one requirement, and the
|
|
44
|
+
// rule value's is the one that counts
|
|
45
|
+
for (const specName of Object.keys(attributeSpecs)) {
|
|
46
|
+
if (el.isSameAttrName(specName, name)) {
|
|
47
|
+
delete attributeSpecs[specName];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
42
50
|
attributeSpecs[name] = {
|
|
43
51
|
name,
|
|
44
52
|
values,
|
|
@@ -46,9 +54,9 @@ export default createRule({
|
|
|
46
54
|
};
|
|
47
55
|
}
|
|
48
56
|
for (const spec of Object.values(attributeSpecs)) {
|
|
49
|
-
if (
|
|
57
|
+
if (isIgnored(spec.name)) {
|
|
50
58
|
if (spec.requiredEither) {
|
|
51
|
-
const activeCandidates = spec.requiredEither.filter(n => !
|
|
59
|
+
const activeCandidates = spec.requiredEither.filter(n => !isIgnored(n));
|
|
52
60
|
if (activeCandidates.length > 0) {
|
|
53
61
|
const invalid = !activeCandidates.some(attrName => el.hasAttribute(attrName));
|
|
54
62
|
if (invalid) {
|
|
@@ -71,7 +79,7 @@ export default createRule({
|
|
|
71
79
|
let invalid = false;
|
|
72
80
|
if (spec.requiredEither) {
|
|
73
81
|
candidate.push(...spec.requiredEither);
|
|
74
|
-
const activeCandidates = candidate.filter(n => !
|
|
82
|
+
const activeCandidates = candidate.filter(n => !isIgnored(n));
|
|
75
83
|
invalid = !activeCandidates.some(attrName => el.hasAttribute(attrName));
|
|
76
84
|
}
|
|
77
85
|
else if (spec.required === true) {
|
|
@@ -81,7 +89,7 @@ export default createRule({
|
|
|
81
89
|
const selector = typeof spec.required === 'string' ? spec.required : spec.required.join(',');
|
|
82
90
|
invalid = el.matches(selector) && didntHave;
|
|
83
91
|
}
|
|
84
|
-
const sortedCandidate = candidate.filter(n => !
|
|
92
|
+
const sortedCandidate = candidate.filter(n => !isIgnored(n)).toSorted();
|
|
85
93
|
if (invalid) {
|
|
86
94
|
const expects = sortedCandidate.length === 1
|
|
87
95
|
? t('the "{0*}" {1}', spec.name, 'attribute')
|
|
@@ -11,7 +11,7 @@ export interface Options {
|
|
|
11
11
|
* technique, not the criterion itself, so this rule's default severity is
|
|
12
12
|
* `warning`.
|
|
13
13
|
*
|
|
14
|
-
* @see https://www.w3.org/WAI/
|
|
14
|
+
* @see https://www.w3.org/WAI/WCAG22/Techniques/html/H42
|
|
15
15
|
*/
|
|
16
16
|
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, Options>>;
|
|
17
17
|
export default _default;
|
package/lib/require-h1/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import meta from './meta.js';
|
|
|
6
6
|
* technique, not the criterion itself, so this rule's default severity is
|
|
7
7
|
* `warning`.
|
|
8
8
|
*
|
|
9
|
-
* @see https://www.w3.org/WAI/
|
|
9
|
+
* @see https://www.w3.org/WAI/WCAG22/Techniques/html/H42
|
|
10
10
|
*/
|
|
11
11
|
export default createRule({
|
|
12
12
|
meta: meta,
|
|
@@ -2,6 +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 { isAriaAttr } from '../helpers.js';
|
|
5
6
|
import meta from './meta.js';
|
|
6
7
|
/**
|
|
7
8
|
* Split from the former `wai-aria-disallowed-props` rule (#3989): the
|
|
@@ -20,7 +21,7 @@ export default createRule({
|
|
|
20
21
|
return;
|
|
21
22
|
if (elSpec && !elSpec.globalAttrs['#ARIAAttrs'])
|
|
22
23
|
return;
|
|
23
|
-
const propAttrs = el.attributes.filter(attr =>
|
|
24
|
+
const propAttrs = el.attributes.filter(attr => isAriaAttr(attr));
|
|
24
25
|
if (propAttrs.length === 0)
|
|
25
26
|
return;
|
|
26
27
|
const ariaVersion = el.rule.options?.version ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* image starts loading — so it only makes sense together with
|
|
5
5
|
* `loading="lazy"`, which the spec requires. Split out of the former
|
|
6
6
|
* `srcset-sizes-constraint` rule's Checks 3 (`<img>`) and 4 (`<source>`, which
|
|
7
|
-
* checks the following sibling `<img>` instead of itself).
|
|
7
|
+
* checks the following sibling `<img>` instead of itself). For the `<source>`,
|
|
8
|
+
* the sibling `<img>` must allow auto-sizes: `loading="lazy"` and a `sizes` of
|
|
9
|
+
* `auto` (`allowsAutoSizes`), not `loading="lazy"` alone.
|
|
8
10
|
*
|
|
9
11
|
* Gated on `srcset` being present, matching the source rule this was split
|
|
10
12
|
* from: `sizes="auto"` has nothing to auto-size without candidates to choose
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
2
|
import { hasSizesAuto } from '../srcset-sizes-constraint/parse-srcset.js';
|
|
3
|
-
import { findFollowingImg } from '../srcset-sizes-constraint/sibling-helpers.js';
|
|
3
|
+
import { findFollowingImg, isAutoSizes, isLazyLoading } from '../srcset-sizes-constraint/sibling-helpers.js';
|
|
4
4
|
import meta from './meta.js';
|
|
5
5
|
/**
|
|
6
6
|
* HTML LS § the img element / the source element: `sizes="auto"` defers
|
|
@@ -8,7 +8,9 @@ import meta from './meta.js';
|
|
|
8
8
|
* image starts loading — so it only makes sense together with
|
|
9
9
|
* `loading="lazy"`, which the spec requires. Split out of the former
|
|
10
10
|
* `srcset-sizes-constraint` rule's Checks 3 (`<img>`) and 4 (`<source>`, which
|
|
11
|
-
* checks the following sibling `<img>` instead of itself).
|
|
11
|
+
* checks the following sibling `<img>` instead of itself). For the `<source>`,
|
|
12
|
+
* the sibling `<img>` must allow auto-sizes: `loading="lazy"` and a `sizes` of
|
|
13
|
+
* `auto` (`allowsAutoSizes`), not `loading="lazy"` alone.
|
|
12
14
|
*
|
|
13
15
|
* Gated on `srcset` being present, matching the source rule this was split
|
|
14
16
|
* from: `sizes="auto"` has nothing to auto-size without candidates to choose
|
|
@@ -38,28 +40,40 @@ export default createRule({
|
|
|
38
40
|
const sizesValue = sizesAttr?.value ?? null;
|
|
39
41
|
const sizesIsDynamic = sizesAttr?.isDynamicValue;
|
|
40
42
|
// img[sizes=auto] → loading=lazy required
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
// The keyword is matched ASCII case-insensitively; a dynamic
|
|
44
|
+
// `loading` is not known and is not reported.
|
|
45
|
+
if (localName === 'img' &&
|
|
46
|
+
sizesValue != null &&
|
|
47
|
+
!sizesIsDynamic &&
|
|
48
|
+
hasSizesAuto(sizesValue) &&
|
|
49
|
+
isLazyLoading(el) === false) {
|
|
50
|
+
report({
|
|
51
|
+
scope: el,
|
|
52
|
+
line: sizesAttr.valueNode?.startLine,
|
|
53
|
+
col: sizesAttr.valueNode?.startCol,
|
|
54
|
+
raw: sizesAttr.valueNode?.raw,
|
|
55
|
+
message: 'The "sizes" attribute with "auto" requires the "loading" attribute to be "lazy"',
|
|
56
|
+
});
|
|
52
57
|
}
|
|
53
|
-
// source[sizes=auto] → following sibling img must
|
|
58
|
+
// source[sizes=auto] → the following sibling img must allow
|
|
59
|
+
// auto-sizes: loading=lazy and sizes=auto (HTML LS §
|
|
60
|
+
// allows-auto-sizes, see `allowsAutoSizes`). A dynamic loading / sizes on the img is not known.
|
|
54
61
|
if (localName === 'source' && sizesValue != null && !sizesIsDynamic && hasSizesAuto(sizesValue)) {
|
|
55
62
|
const img = findFollowingImg(el);
|
|
56
|
-
|
|
63
|
+
const lazy = img ? isLazyLoading(img) : false;
|
|
64
|
+
const auto = img ? isAutoSizes(img) : false;
|
|
65
|
+
if (lazy === false || auto === false) {
|
|
66
|
+
// Say everything that is missing at once, so that fixing
|
|
67
|
+
// one does not reveal the other.
|
|
68
|
+
const missing = [lazy === false && 'loading="lazy"', auto === false && 'sizes="auto"'].filter(Boolean);
|
|
57
69
|
report({
|
|
58
70
|
scope: el,
|
|
59
71
|
line: sizesAttr.valueNode?.startLine,
|
|
60
72
|
col: sizesAttr.valueNode?.startCol,
|
|
61
73
|
raw: sizesAttr.valueNode?.raw,
|
|
62
|
-
message:
|
|
74
|
+
message: !img || (missing.length === 1 && lazy === false)
|
|
75
|
+
? 'The "source" element with sizes="auto" requires the following sibling "img" element to have loading="lazy"'
|
|
76
|
+
: `The "source" element with sizes="auto" requires the following sibling "img" element to have ${missing.join(' and ')}`,
|
|
63
77
|
});
|
|
64
78
|
}
|
|
65
79
|
}
|
|
@@ -33,14 +33,15 @@ type ParseResult = {
|
|
|
33
33
|
*/
|
|
34
34
|
export declare function parseSrcset(value: string): ParseResult;
|
|
35
35
|
/**
|
|
36
|
-
* Check whether a `sizes` attribute value starts with `auto
|
|
36
|
+
* Check whether a `sizes` attribute value is `auto` or starts with `auto,`.
|
|
37
37
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
38
|
+
* HTML LS § allows-auto-sizes: the value is "auto" (ASCII case-insensitive),
|
|
39
|
+
* or starts with "auto," (ASCII case-insensitive). The value is not trimmed.
|
|
40
|
+
* Examples: `"auto"`, `"auto, 100vw"`, `"AUTO,100vw"`. Not: `"auto 100vw"`.
|
|
40
41
|
*
|
|
41
42
|
* @param value - The raw `sizes` attribute value string
|
|
42
|
-
* @returns `true` if the value starts with `auto
|
|
43
|
-
* @see https://html.spec.whatwg.org/multipage/embedded-content.html#
|
|
43
|
+
* @returns `true` if the value is `auto` or starts with `auto,`
|
|
44
|
+
* @see https://html.spec.whatwg.org/multipage/embedded-content.html#allows-auto-sizes
|
|
44
45
|
*/
|
|
45
46
|
export declare function hasSizesAuto(value: string): boolean;
|
|
46
47
|
export {};
|
|
@@ -47,16 +47,17 @@ export function parseSrcset(value) {
|
|
|
47
47
|
return { candidates, hasWidth, hasDensity, hasImplied };
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
|
-
* Check whether a `sizes` attribute value starts with `auto
|
|
50
|
+
* Check whether a `sizes` attribute value is `auto` or starts with `auto,`.
|
|
51
51
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* HTML LS § allows-auto-sizes: the value is "auto" (ASCII case-insensitive),
|
|
53
|
+
* or starts with "auto," (ASCII case-insensitive). The value is not trimmed.
|
|
54
|
+
* Examples: `"auto"`, `"auto, 100vw"`, `"AUTO,100vw"`. Not: `"auto 100vw"`.
|
|
54
55
|
*
|
|
55
56
|
* @param value - The raw `sizes` attribute value string
|
|
56
|
-
* @returns `true` if the value starts with `auto
|
|
57
|
-
* @see https://html.spec.whatwg.org/multipage/embedded-content.html#
|
|
57
|
+
* @returns `true` if the value is `auto` or starts with `auto,`
|
|
58
|
+
* @see https://html.spec.whatwg.org/multipage/embedded-content.html#allows-auto-sizes
|
|
58
59
|
*/
|
|
59
60
|
export function hasSizesAuto(value) {
|
|
60
|
-
const v = value.
|
|
61
|
-
return v === 'auto' ||
|
|
61
|
+
const v = value.toLowerCase();
|
|
62
|
+
return v === 'auto' || v.startsWith('auto,');
|
|
62
63
|
}
|
|
@@ -19,3 +19,34 @@ export declare function followingElementSiblings(el: Element<boolean>): Generato
|
|
|
19
19
|
* @returns The first following sibling `<img>` element, or `null` if none found
|
|
20
20
|
*/
|
|
21
21
|
export declare function findFollowingImg(el: Element<boolean>): Element<boolean> | null;
|
|
22
|
+
/**
|
|
23
|
+
* Whether an `<img>`'s `loading` attribute is in the Lazy state.
|
|
24
|
+
*
|
|
25
|
+
* @param img - The `<img>` element
|
|
26
|
+
* @returns `true` / `false`, or `null` when it cannot be known statically (a
|
|
27
|
+
* dynamic `loading` value, or spread attributes that may supply it)
|
|
28
|
+
*/
|
|
29
|
+
export declare function isLazyLoading(img: Element<boolean>): boolean | null;
|
|
30
|
+
/**
|
|
31
|
+
* Whether an `<img>` "allows auto-sizes".
|
|
32
|
+
*
|
|
33
|
+
* HTML LS § allows-auto-sizes: its `loading` attribute is in the Lazy state,
|
|
34
|
+
* and its `sizes` attribute's value is `auto` (ASCII case-insensitive) or
|
|
35
|
+
* starts with `auto,`. `loading="lazy"` alone is not enough: that is what
|
|
36
|
+
* the `<source>` direction of `no-unpaired-srcset-sizes` and
|
|
37
|
+
* `sizes-auto-requires-lazy-loading` both depend on.
|
|
38
|
+
*
|
|
39
|
+
* @see https://html.spec.whatwg.org/multipage/embedded-content.html#allows-auto-sizes
|
|
40
|
+
* @param img - The `<img>` element
|
|
41
|
+
* @returns `true` / `false`, or `null` when it cannot be known statically
|
|
42
|
+
*/
|
|
43
|
+
export declare function allowsAutoSizes(img: Element<boolean>): boolean | null;
|
|
44
|
+
/**
|
|
45
|
+
* Whether an `<img>`'s `sizes` attribute is `auto` or starts with `auto,`
|
|
46
|
+
* (the `sizes` half of "allows auto-sizes"; see `hasSizesAuto`).
|
|
47
|
+
*
|
|
48
|
+
* @param img - The `<img>` element
|
|
49
|
+
* @returns `true` / `false`, or `null` when it cannot be known statically (a
|
|
50
|
+
* dynamic `sizes` value, or spread attributes that may supply it)
|
|
51
|
+
*/
|
|
52
|
+
export declare function isAutoSizes(img: Element<boolean>): boolean | null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasSizesAuto } from './parse-srcset.js';
|
|
1
2
|
/**
|
|
2
3
|
* Iterate the element's following element siblings in document order.
|
|
3
4
|
*
|
|
@@ -35,3 +36,60 @@ el) {
|
|
|
35
36
|
}
|
|
36
37
|
return null;
|
|
37
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Whether an `<img>`'s `loading` attribute is in the Lazy state.
|
|
41
|
+
*
|
|
42
|
+
* @param img - The `<img>` element
|
|
43
|
+
* @returns `true` / `false`, or `null` when it cannot be known statically (a
|
|
44
|
+
* dynamic `loading` value, or spread attributes that may supply it)
|
|
45
|
+
*/
|
|
46
|
+
export function isLazyLoading(
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
48
|
+
img) {
|
|
49
|
+
const loading = img.getAttributeNode('loading');
|
|
50
|
+
if (loading?.isDynamicValue || (!loading && img.hasSpreadAttr)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
// The keyword is matched ASCII case-insensitively (HTML LS § lazy-loading attributes).
|
|
54
|
+
return loading?.value.toLowerCase() === 'lazy';
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Whether an `<img>` "allows auto-sizes".
|
|
58
|
+
*
|
|
59
|
+
* HTML LS § allows-auto-sizes: its `loading` attribute is in the Lazy state,
|
|
60
|
+
* and its `sizes` attribute's value is `auto` (ASCII case-insensitive) or
|
|
61
|
+
* starts with `auto,`. `loading="lazy"` alone is not enough: that is what
|
|
62
|
+
* the `<source>` direction of `no-unpaired-srcset-sizes` and
|
|
63
|
+
* `sizes-auto-requires-lazy-loading` both depend on.
|
|
64
|
+
*
|
|
65
|
+
* @see https://html.spec.whatwg.org/multipage/embedded-content.html#allows-auto-sizes
|
|
66
|
+
* @param img - The `<img>` element
|
|
67
|
+
* @returns `true` / `false`, or `null` when it cannot be known statically
|
|
68
|
+
*/
|
|
69
|
+
export function allowsAutoSizes(
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
71
|
+
img) {
|
|
72
|
+
const lazy = isLazyLoading(img);
|
|
73
|
+
const auto = isAutoSizes(img);
|
|
74
|
+
if (lazy === false || auto === false) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return lazy === null || auto === null ? null : true;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Whether an `<img>`'s `sizes` attribute is `auto` or starts with `auto,`
|
|
81
|
+
* (the `sizes` half of "allows auto-sizes"; see `hasSizesAuto`).
|
|
82
|
+
*
|
|
83
|
+
* @param img - The `<img>` element
|
|
84
|
+
* @returns `true` / `false`, or `null` when it cannot be known statically (a
|
|
85
|
+
* dynamic `sizes` value, or spread attributes that may supply it)
|
|
86
|
+
*/
|
|
87
|
+
export function isAutoSizes(
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
89
|
+
img) {
|
|
90
|
+
const sizes = img.getAttributeNode('sizes');
|
|
91
|
+
if (sizes?.isDynamicValue || (!sizes && img.hasSpreadAttr)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return sizes != null && hasSizesAuto(sizes.value);
|
|
95
|
+
}
|
|
@@ -16,7 +16,7 @@ function isPlainObject(value) {
|
|
|
16
16
|
* corpus. When the spec changes, update this set/the SR_* sets below, the error
|
|
17
17
|
* message strings, and the README tables.
|
|
18
18
|
*
|
|
19
|
-
* @see https://html.spec.whatwg.org/multipage/speculative-loading.html
|
|
19
|
+
* @see https://html.spec.whatwg.org/multipage/speculative-loading.html#speculation-rules
|
|
20
20
|
*/
|
|
21
21
|
// Valid keys per HTML LS § 7.6.1.2 "parse a speculation rule" / the rule-set
|
|
22
22
|
// object grammar. Unknown keys are rejected by the spec (the rule returns null),
|
|
@@ -22,7 +22,7 @@ export const checkingContradictoryAriaProp = ({ attr, propSpecs, attrSpecs }) =>
|
|
|
22
22
|
if (attr.isDynamicValue) {
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
|
-
const propSpec = propSpecs.find(p => p.name === attr.
|
|
25
|
+
const propSpec = propSpecs.find(p => p.name === attr.nameKey);
|
|
26
26
|
if (!propSpec?.equivalentHtmlAttrs) {
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
@@ -30,13 +30,11 @@ export const checkingContradictoryAriaProp = ({ attr, propSpecs, attrSpecs }) =>
|
|
|
30
30
|
const htmlAttrSpec = attrSpecs.find(a => a.name === equivalentHtmlAttr.htmlAttrName);
|
|
31
31
|
const eligibility = resolveAttrEligibility(equivalentHtmlAttr.htmlAttrName, attr.ownerElement, attrSpecs);
|
|
32
32
|
// Skip only when the native attribute doesn't apply to this element at all
|
|
33
|
-
// (unknown name
|
|
33
|
+
// (unknown name or an unmet condition) — mirrors the old
|
|
34
34
|
// `invalidType === 'non-existent'` check. `noUse` still proceeds: an
|
|
35
35
|
// explicitly-forbidden native attribute can still be "implicit" for
|
|
36
36
|
// equivalence purposes.
|
|
37
|
-
if (eligibility.status === 'unknown' ||
|
|
38
|
-
eligibility.status === 'case-mismatch' ||
|
|
39
|
-
eligibility.status === 'condition-not-met') {
|
|
37
|
+
if (eligibility.status === 'unknown' || eligibility.status === 'condition-not-met') {
|
|
40
38
|
continue;
|
|
41
39
|
}
|
|
42
40
|
const value = attr.value.trim().toLowerCase();
|