@markuplint/rules 5.2.0 → 5.4.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 +48 -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-presentational-children/index.js +3 -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 +21 -10
- package/lib/wai-aria/checkings/presentational-children.js +240 -26
- 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 +15 -15
package/lib/helpers.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FixToken, IRuleFixer, PlainData, TextEdit } from '@markuplint/ml-config';
|
|
2
|
-
import type { Element, RuleConfigValue, Document } from '@markuplint/ml-core';
|
|
2
|
+
import type { Attr, Element, RuleConfigValue, Document } from '@markuplint/ml-core';
|
|
3
3
|
import type { Attribute } from '@markuplint/ml-spec';
|
|
4
4
|
import type { WritableDeep } from 'type-fest';
|
|
5
5
|
/** A `null`/`undefined` condition means the attribute applies unconditionally. */
|
|
@@ -10,6 +10,94 @@ export declare function attrMatches<T extends RuleConfigValue, O extends PlainDa
|
|
|
10
10
|
* indeterminate and must not be used to report a violation.
|
|
11
11
|
*/
|
|
12
12
|
export declare function conditionDependsOnDynamicAttr<T extends RuleConfigValue, O extends PlainData>(node: Element<T, O>, condition: NonNullable<Attribute['condition']>): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Memoizes `fn` by the identity of its argument. Use it for an object that is one object for every
|
|
15
|
+
* caller, such as the array `getAttrSpecs` hands out for an element name. The options of the
|
|
16
|
+
* elements a `nodeRule` selects are not: each element gets a copy, so use {@link memoizeByContent} for them.
|
|
17
|
+
*
|
|
18
|
+
* @param fn - The function to memoize
|
|
19
|
+
* @returns The function that computes `fn` once per object
|
|
20
|
+
*/
|
|
21
|
+
export declare function memoizeByObject<K extends object, V>(fn: (key: K) => V): (key: K) => V;
|
|
22
|
+
/**
|
|
23
|
+
* Memoizes `fn` by the content of its argument, which must be JSON data (a rule option is). The
|
|
24
|
+
* options of the elements a `nodeRule` selects arrive as a copy for each element, so keying by
|
|
25
|
+
* identity alone would rebuild the table for every element; identity stays as the first, cheap
|
|
26
|
+
* lookup, and the content is serialised only for an object not seen before. The cache lives as long
|
|
27
|
+
* as the memoized function, which a rule creates once per `verify` call, so it is bounded by the
|
|
28
|
+
* options of one document. Two contents that differ only in key order are two entries, which is
|
|
29
|
+
* harmless for a table read by name. JSON keeps `['a', 'b']` and `['a\0b']` apart, which a joined
|
|
30
|
+
* string would not.
|
|
31
|
+
*
|
|
32
|
+
* @param fn - The function to memoize
|
|
33
|
+
* @returns The function that computes `fn` once per content
|
|
34
|
+
*/
|
|
35
|
+
export declare function memoizeByContent<K extends object, V>(fn: (key: K) => V): (key: K) => V;
|
|
36
|
+
/**
|
|
37
|
+
* Finds the spec of an attribute: the spec named as the attribute is written, and otherwise the
|
|
38
|
+
* first one that the attribute says it is (`MLAttr#isNamedBy`). The case of a name is decided by
|
|
39
|
+
* the attribute, so the rules that look an attribute up share one answer.
|
|
40
|
+
*
|
|
41
|
+
* `getAttrSpecs` hands out the same array for every element of a name, so the exact names are
|
|
42
|
+
* indexed once per array and the scan runs only for a name that is not written as the spec spells it.
|
|
43
|
+
*
|
|
44
|
+
* @param attrSpecs - The specs of the attributes of the element
|
|
45
|
+
* @param attr - The attribute
|
|
46
|
+
* @returns The spec, or `undefined` if the attribute has none
|
|
47
|
+
*/
|
|
48
|
+
export declare function findAttrSpec<T extends RuleConfigValue, O extends PlainData>(attrSpecs: readonly Attribute[], attr: Attr<T, O>): Attribute | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* The name that a namespace of names (`data-*`, `aria-*`, the predefined groups of `attr-order`)
|
|
51
|
+
* is told by: the name that the DOM gives the attribute (`nameKey`). For the prop of a component it
|
|
52
|
+
* is the name as written in ASCII lower case instead, because
|
|
53
|
+
* `nameKey` camelizes it in Vue (`data-x` is `dataX`) and a prefix cannot be told then.
|
|
54
|
+
*
|
|
55
|
+
* @param attr - The attribute, with the element type of its owner
|
|
56
|
+
* @returns The name to test against a namespace
|
|
57
|
+
*/
|
|
58
|
+
export declare function namespaceKeyOf(attr: Pick<Attr<RuleConfigValue, PlainData>, 'name' | 'nameKey'> & {
|
|
59
|
+
readonly ownerElement: {
|
|
60
|
+
readonly elementType: string;
|
|
61
|
+
};
|
|
62
|
+
}): string;
|
|
63
|
+
/**
|
|
64
|
+
* Whether the attribute is an `aria-*` one, told by {@link namespaceKeyOf}.
|
|
65
|
+
*
|
|
66
|
+
* @param attr - The attribute
|
|
67
|
+
* @returns `true` if the attribute is an `aria-*` one
|
|
68
|
+
*/
|
|
69
|
+
export declare function isAriaAttr<T extends RuleConfigValue, O extends PlainData>(attr: Attr<T, O>): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* The shape of an option that lists attributes (`allowAttrs`, `disallowAttrs`): bare names, which
|
|
72
|
+
* carry no value of their own, `{ name, value }` pairs, or a record of name to value.
|
|
73
|
+
*/
|
|
74
|
+
type NameValueOption<V> = (string | {
|
|
75
|
+
readonly name: string;
|
|
76
|
+
readonly value: V;
|
|
77
|
+
})[] | Record<string, V>;
|
|
78
|
+
/**
|
|
79
|
+
* Creates a lookup of an attribute in a `NameValueOption`.
|
|
80
|
+
*
|
|
81
|
+
* A name of an option is compared as the DOM compares attribute names (`MLAttr#isNamedBy`): ASCII
|
|
82
|
+
* case-insensitive in the HTML syntax and for an HTML element that code sets through the DOM API
|
|
83
|
+
* (JSX, MDX, Vue), exact for an SVG or MathML element there, and the props of a component are not
|
|
84
|
+
* folded. The README of `no-unknown-attr` states it for users.
|
|
85
|
+
*
|
|
86
|
+
* Among the entries, one spelled as the attribute is written wins over any other, and the last of
|
|
87
|
+
* the same spelling wins. With no such entry, the last entry that the attribute accepts wins.
|
|
88
|
+
* The table of an option is built once per content (`memoizeByContent`).
|
|
89
|
+
*
|
|
90
|
+
* @param stringValue - The value of a bare name
|
|
91
|
+
* @returns The lookup, which gives the value for the attribute, or `undefined` if no entry names it
|
|
92
|
+
*/
|
|
93
|
+
export declare function createNameOptionLookup<V>(stringValue: V): <T extends RuleConfigValue, O extends PlainData>(option: NameValueOption<V> | undefined, attr: Attr<T, O>) => V | undefined;
|
|
94
|
+
/**
|
|
95
|
+
* Creates a test of whether an attribute has one of the prefixes of an option
|
|
96
|
+
* (`ignoreAttrNamePrefix`), which `MLAttr#isPrefixedBy` decides.
|
|
97
|
+
*
|
|
98
|
+
* @returns The test, which takes the prefixes of the option and the attribute
|
|
99
|
+
*/
|
|
100
|
+
export declare function createPrefixMatcher(): <T extends RuleConfigValue, O extends PlainData>(prefixes: string | readonly string[] | undefined, attr: Attr<T, O>) => boolean;
|
|
13
101
|
/** `pattern` is a regex literal of the form `/pattern/flags` if it matches that shape, otherwise an exact string. */
|
|
14
102
|
export declare function match(needle: string, pattern: string): boolean;
|
|
15
103
|
/**
|
|
@@ -72,3 +160,25 @@ export declare function removeAttrValue(fixer: IRuleFixer, attr: {
|
|
|
72
160
|
readonly valueNode: FixToken | null;
|
|
73
161
|
readonly endQuote: FixToken | null;
|
|
74
162
|
}): TextEdit | TextEdit[];
|
|
163
|
+
/**
|
|
164
|
+
* Creates the lookup of `ignoreFeatures`, whose keys are an element (`dialog`) or an attribute of
|
|
165
|
+
* an element (`img[loading]`). The element part of a key is compared as the document compares tag
|
|
166
|
+
* names (`tagNameCaseSensitive`), so `DIALOG` is not `<dialog>` in Svelte. The attribute part is
|
|
167
|
+
* compared by the attribute that is checked (`MLAttr#isNamedBy`). An element key does not ignore
|
|
168
|
+
* the attributes of the element.
|
|
169
|
+
*
|
|
170
|
+
* The option of an element replaces the option of the rule for that element. The options of the
|
|
171
|
+
* elements a `nodeRule` selects arrive as a copy for each element, so the table of an option is built
|
|
172
|
+
* once per content (`memoizeByContent`).
|
|
173
|
+
*
|
|
174
|
+
* @param tagNameCaseSensitive - `MLDocument#tagNameCaseSensitive`
|
|
175
|
+
* @param ruleOption - `ignoreFeatures` of the rule
|
|
176
|
+
* @returns The lookup for an element and for an attribute of an element
|
|
177
|
+
*/
|
|
178
|
+
export declare function createIgnoreFeaturesLookup(tagNameCaseSensitive: boolean, ruleOption: readonly string[] | undefined): {
|
|
179
|
+
/** Whether the element is ignored. */
|
|
180
|
+
isElementIgnored(elementOption: readonly string[] | undefined, elementName: string): boolean;
|
|
181
|
+
/** Whether the attribute of the element is ignored. */
|
|
182
|
+
isAttrIgnored<T extends RuleConfigValue, O extends PlainData>(elementOption: readonly string[] | undefined, elementName: string, attr: Attr<T, O>): boolean;
|
|
183
|
+
};
|
|
184
|
+
export {};
|
package/lib/helpers.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { escapeCSS } from '@markuplint/ml-spec';
|
|
2
|
+
import { asciiLowerCase } from '@markuplint/shared';
|
|
2
3
|
/** A `null`/`undefined` condition means the attribute applies unconditionally. */
|
|
3
4
|
export function attrMatches(
|
|
4
5
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -25,11 +26,163 @@ node, condition) {
|
|
|
25
26
|
for (const matched of selector.matchAll(/\[\s*([^\s\]=~|^$*]+)/g)) {
|
|
26
27
|
const name = matched[1];
|
|
27
28
|
if (name) {
|
|
28
|
-
referencedNames.add(name
|
|
29
|
+
referencedNames.add(name);
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
// Whether the attribute is the one that the selector names is up to the attribute (`isNamedBy`)
|
|
34
|
+
const names = [...referencedNames];
|
|
35
|
+
return node.attributes.some(attr => attr.isDynamicValue && names.some(name => attr.isNamedBy(name)));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Memoizes `fn` by the identity of its argument. Use it for an object that is one object for every
|
|
39
|
+
* caller, such as the array `getAttrSpecs` hands out for an element name. The options of the
|
|
40
|
+
* elements a `nodeRule` selects are not: each element gets a copy, so use {@link memoizeByContent} for them.
|
|
41
|
+
*
|
|
42
|
+
* @param fn - The function to memoize
|
|
43
|
+
* @returns The function that computes `fn` once per object
|
|
44
|
+
*/
|
|
45
|
+
export function memoizeByObject(fn) {
|
|
46
|
+
const cache = new WeakMap();
|
|
47
|
+
return key => {
|
|
48
|
+
if (cache.has(key)) {
|
|
49
|
+
return cache.get(key);
|
|
50
|
+
}
|
|
51
|
+
const value = fn(key);
|
|
52
|
+
cache.set(key, value);
|
|
53
|
+
return value;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Memoizes `fn` by the content of its argument, which must be JSON data (a rule option is). The
|
|
58
|
+
* options of the elements a `nodeRule` selects arrive as a copy for each element, so keying by
|
|
59
|
+
* identity alone would rebuild the table for every element; identity stays as the first, cheap
|
|
60
|
+
* lookup, and the content is serialised only for an object not seen before. The cache lives as long
|
|
61
|
+
* as the memoized function, which a rule creates once per `verify` call, so it is bounded by the
|
|
62
|
+
* options of one document. Two contents that differ only in key order are two entries, which is
|
|
63
|
+
* harmless for a table read by name. JSON keeps `['a', 'b']` and `['a\0b']` apart, which a joined
|
|
64
|
+
* string would not.
|
|
65
|
+
*
|
|
66
|
+
* @param fn - The function to memoize
|
|
67
|
+
* @returns The function that computes `fn` once per content
|
|
68
|
+
*/
|
|
69
|
+
export function memoizeByContent(fn) {
|
|
70
|
+
const byContent = new Map();
|
|
71
|
+
return memoizeByObject((key) => {
|
|
72
|
+
const content = JSON.stringify(key);
|
|
73
|
+
if (byContent.has(content)) {
|
|
74
|
+
return byContent.get(content);
|
|
75
|
+
}
|
|
76
|
+
const value = fn(key);
|
|
77
|
+
byContent.set(content, value);
|
|
78
|
+
return value;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const exactSpecsOf = memoizeByObject((attrSpecs) => {
|
|
82
|
+
const exact = new Map();
|
|
83
|
+
for (const spec of attrSpecs) {
|
|
84
|
+
if (!exact.has(spec.name)) {
|
|
85
|
+
exact.set(spec.name, spec);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return exact;
|
|
89
|
+
});
|
|
90
|
+
/**
|
|
91
|
+
* Finds the spec of an attribute: the spec named as the attribute is written, and otherwise the
|
|
92
|
+
* first one that the attribute says it is (`MLAttr#isNamedBy`). The case of a name is decided by
|
|
93
|
+
* the attribute, so the rules that look an attribute up share one answer.
|
|
94
|
+
*
|
|
95
|
+
* `getAttrSpecs` hands out the same array for every element of a name, so the exact names are
|
|
96
|
+
* indexed once per array and the scan runs only for a name that is not written as the spec spells it.
|
|
97
|
+
*
|
|
98
|
+
* @param attrSpecs - The specs of the attributes of the element
|
|
99
|
+
* @param attr - The attribute
|
|
100
|
+
* @returns The spec, or `undefined` if the attribute has none
|
|
101
|
+
*/
|
|
102
|
+
export function findAttrSpec(
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
104
|
+
attrSpecs,
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
106
|
+
attr) {
|
|
107
|
+
return exactSpecsOf(attrSpecs).get(attr.name) ?? attrSpecs.find(spec => attr.isNamedBy(spec.name));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The name that a namespace of names (`data-*`, `aria-*`, the predefined groups of `attr-order`)
|
|
111
|
+
* is told by: the name that the DOM gives the attribute (`nameKey`). For the prop of a component it
|
|
112
|
+
* is the name as written in ASCII lower case instead, because
|
|
113
|
+
* `nameKey` camelizes it in Vue (`data-x` is `dataX`) and a prefix cannot be told then.
|
|
114
|
+
*
|
|
115
|
+
* @param attr - The attribute, with the element type of its owner
|
|
116
|
+
* @returns The name to test against a namespace
|
|
117
|
+
*/
|
|
118
|
+
export function namespaceKeyOf(attr) {
|
|
119
|
+
return attr.ownerElement.elementType === 'authored' ? asciiLowerCase(attr.name) : attr.nameKey;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Whether the attribute is an `aria-*` one, told by {@link namespaceKeyOf}.
|
|
123
|
+
*
|
|
124
|
+
* @param attr - The attribute
|
|
125
|
+
* @returns `true` if the attribute is an `aria-*` one
|
|
126
|
+
*/
|
|
127
|
+
export function isAriaAttr(
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
129
|
+
attr) {
|
|
130
|
+
return namespaceKeyOf(attr).startsWith('aria-');
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Creates a lookup of an attribute in a `NameValueOption`.
|
|
134
|
+
*
|
|
135
|
+
* A name of an option is compared as the DOM compares attribute names (`MLAttr#isNamedBy`): ASCII
|
|
136
|
+
* case-insensitive in the HTML syntax and for an HTML element that code sets through the DOM API
|
|
137
|
+
* (JSX, MDX, Vue), exact for an SVG or MathML element there, and the props of a component are not
|
|
138
|
+
* folded. The README of `no-unknown-attr` states it for users.
|
|
139
|
+
*
|
|
140
|
+
* Among the entries, one spelled as the attribute is written wins over any other, and the last of
|
|
141
|
+
* the same spelling wins. With no such entry, the last entry that the attribute accepts wins.
|
|
142
|
+
* The table of an option is built once per content (`memoizeByContent`).
|
|
143
|
+
*
|
|
144
|
+
* @param stringValue - The value of a bare name
|
|
145
|
+
* @returns The lookup, which gives the value for the attribute, or `undefined` if no entry names it
|
|
146
|
+
*/
|
|
147
|
+
export function createNameOptionLookup(stringValue) {
|
|
148
|
+
const tableOf = memoizeByContent((option) => {
|
|
149
|
+
const entries = Array.isArray(option)
|
|
150
|
+
? option.map(entry => (typeof entry === 'string' ? [entry, stringValue] : [entry.name, entry.value]))
|
|
151
|
+
: Object.entries(option);
|
|
152
|
+
// `new Map` keeps the later of two entries of one spelling. Across spellings, the exact
|
|
153
|
+
// spelling is looked up first and the scan below keeps the last that the attribute accepts.
|
|
154
|
+
return { entries, exact: new Map(entries) };
|
|
155
|
+
});
|
|
156
|
+
return (option, attr) => {
|
|
157
|
+
if (!option) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const { entries, exact } = tableOf(option);
|
|
161
|
+
if (exact.has(attr.name)) {
|
|
162
|
+
return exact.get(attr.name);
|
|
163
|
+
}
|
|
164
|
+
let found;
|
|
165
|
+
for (const [name, value] of entries) {
|
|
166
|
+
if (attr.isNamedBy(name)) {
|
|
167
|
+
found = value;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return found;
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Creates a test of whether an attribute has one of the prefixes of an option
|
|
175
|
+
* (`ignoreAttrNamePrefix`), which `MLAttr#isPrefixedBy` decides.
|
|
176
|
+
*
|
|
177
|
+
* @returns The test, which takes the prefixes of the option and the attribute
|
|
178
|
+
*/
|
|
179
|
+
export function createPrefixMatcher() {
|
|
180
|
+
return (prefixes, attr) => {
|
|
181
|
+
if (prefixes == null) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
return (typeof prefixes === 'string' ? [prefixes] : prefixes).some(prefix => attr.isPrefixedBy(prefix));
|
|
185
|
+
};
|
|
33
186
|
}
|
|
34
187
|
/** `pattern` is a regex literal of the form `/pattern/flags` if it matches that shape, otherwise an exact string. */
|
|
35
188
|
export function match(needle, pattern) {
|
|
@@ -259,3 +412,60 @@ fixer, attr) {
|
|
|
259
412
|
}
|
|
260
413
|
return fixer.removeRange(range);
|
|
261
414
|
}
|
|
415
|
+
function parseIgnoreFeatures(features) {
|
|
416
|
+
const elements = { exact: new Set(), folded: new Set() };
|
|
417
|
+
const attrs = { exact: new Map(), folded: new Map() };
|
|
418
|
+
const push = (map, element, attrName) => {
|
|
419
|
+
const names = map.get(element);
|
|
420
|
+
if (names) {
|
|
421
|
+
names.push(attrName);
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
map.set(element, [attrName]);
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
for (const feature of features) {
|
|
428
|
+
const open = feature.indexOf('[');
|
|
429
|
+
if (open === -1 || !feature.endsWith(']')) {
|
|
430
|
+
elements.exact.add(feature);
|
|
431
|
+
elements.folded.add(asciiLowerCase(feature));
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const element = feature.slice(0, open);
|
|
435
|
+
const attrName = feature.slice(open + 1, -1);
|
|
436
|
+
push(attrs.exact, element, attrName);
|
|
437
|
+
push(attrs.folded, asciiLowerCase(element), attrName);
|
|
438
|
+
}
|
|
439
|
+
return { elements, attrs };
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Creates the lookup of `ignoreFeatures`, whose keys are an element (`dialog`) or an attribute of
|
|
443
|
+
* an element (`img[loading]`). The element part of a key is compared as the document compares tag
|
|
444
|
+
* names (`tagNameCaseSensitive`), so `DIALOG` is not `<dialog>` in Svelte. The attribute part is
|
|
445
|
+
* compared by the attribute that is checked (`MLAttr#isNamedBy`). An element key does not ignore
|
|
446
|
+
* the attributes of the element.
|
|
447
|
+
*
|
|
448
|
+
* The option of an element replaces the option of the rule for that element. The options of the
|
|
449
|
+
* elements a `nodeRule` selects arrive as a copy for each element, so the table of an option is built
|
|
450
|
+
* once per content (`memoizeByContent`).
|
|
451
|
+
*
|
|
452
|
+
* @param tagNameCaseSensitive - `MLDocument#tagNameCaseSensitive`
|
|
453
|
+
* @param ruleOption - `ignoreFeatures` of the rule
|
|
454
|
+
* @returns The lookup for an element and for an attribute of an element
|
|
455
|
+
*/
|
|
456
|
+
export function createIgnoreFeaturesLookup(tagNameCaseSensitive, ruleOption) {
|
|
457
|
+
const tableOf = memoizeByContent(parseIgnoreFeatures);
|
|
458
|
+
const ruleTable = tableOf(ruleOption ?? []);
|
|
459
|
+
const pick = (elementOption) => (elementOption ? tableOf(elementOption) : ruleTable);
|
|
460
|
+
return {
|
|
461
|
+
isElementIgnored(elementOption, elementName) {
|
|
462
|
+
const { exact, folded } = pick(elementOption).elements;
|
|
463
|
+
return tagNameCaseSensitive ? exact.has(elementName) : folded.has(asciiLowerCase(elementName));
|
|
464
|
+
},
|
|
465
|
+
isAttrIgnored(elementOption, elementName, attr) {
|
|
466
|
+
const { exact, folded } = pick(elementOption).attrs;
|
|
467
|
+
const names = tagNameCaseSensitive ? exact.get(elementName) : folded.get(asciiLowerCase(elementName));
|
|
468
|
+
return names?.some(name => attr.isNamedBy(name)) ?? false;
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Require the `list` attribute on an `<input>` element to reference an
|
|
3
|
-
* actual `<datalist>` element by ID. Per HTML LS §4.10.5.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* a
|
|
7
|
-
*
|
|
8
|
-
*
|
|
3
|
+
* actual `<datalist>` element by ID. Per HTML LS §4.10.5.3.9:
|
|
4
|
+
*
|
|
5
|
+
* > The list attribute is used to identify an element that lists predefined options suggested to the user. If present,
|
|
6
|
+
* > its value must be the ID of a datalist element in the same tree.
|
|
7
|
+
*
|
|
8
|
+
* Existence of the ID itself is the responsibility of
|
|
9
|
+
* `no-refer-to-non-existent-id`; this rule only checks the element type
|
|
10
|
+
* when the ID resolves.
|
|
9
11
|
*
|
|
10
12
|
* @see https://html.spec.whatwg.org/multipage/input.html#the-list-attribute
|
|
11
13
|
*/
|
|
@@ -2,12 +2,14 @@ import { createRule } from '@markuplint/ml-core';
|
|
|
2
2
|
import meta from './meta.js';
|
|
3
3
|
/**
|
|
4
4
|
* Require the `list` attribute on an `<input>` element to reference an
|
|
5
|
-
* actual `<datalist>` element by ID. Per HTML LS §4.10.5.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* a
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* actual `<datalist>` element by ID. Per HTML LS §4.10.5.3.9:
|
|
6
|
+
*
|
|
7
|
+
* > The list attribute is used to identify an element that lists predefined options suggested to the user. If present,
|
|
8
|
+
* > its value must be the ID of a datalist element in the same tree.
|
|
9
|
+
*
|
|
10
|
+
* Existence of the ID itself is the responsibility of
|
|
11
|
+
* `no-refer-to-non-existent-id`; this rule only checks the element type
|
|
12
|
+
* when the ID resolves.
|
|
11
13
|
*
|
|
12
14
|
* @see https://html.spec.whatwg.org/multipage/input.html#the-list-attribute
|
|
13
15
|
*/
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Require the `for` attribute on a `<label>` element to reference an
|
|
3
|
-
* actual labelable element by ID. Per HTML LS §4.10.4:
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* actual labelable element by ID. Per HTML LS §4.10.4:
|
|
4
|
+
*
|
|
5
|
+
* > If the attribute is specified, the attribute's value must be the ID of a labelable element in the same tree as the
|
|
6
|
+
* > label element.
|
|
7
|
+
*
|
|
8
|
+
* Existence of the ID itself is the responsibility of
|
|
9
|
+
* `no-refer-to-non-existent-id`; this rule only checks the element type
|
|
10
|
+
* when the ID resolves.
|
|
8
11
|
*
|
|
9
12
|
* @see https://html.spec.whatwg.org/multipage/forms.html#attr-label-for
|
|
10
13
|
*/
|
|
@@ -25,11 +25,14 @@ const LABELABLE_SELECTOR = [
|
|
|
25
25
|
].join(',');
|
|
26
26
|
/**
|
|
27
27
|
* Require the `for` attribute on a `<label>` element to reference an
|
|
28
|
-
* actual labelable element by ID. Per HTML LS §4.10.4:
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
28
|
+
* actual labelable element by ID. Per HTML LS §4.10.4:
|
|
29
|
+
*
|
|
30
|
+
* > If the attribute is specified, the attribute's value must be the ID of a labelable element in the same tree as the
|
|
31
|
+
* > label element.
|
|
32
|
+
*
|
|
33
|
+
* Existence of the ID itself is the responsibility of
|
|
34
|
+
* `no-refer-to-non-existent-id`; this rule only checks the element type
|
|
35
|
+
* when the ID resolves.
|
|
33
36
|
*
|
|
34
37
|
* @see https://html.spec.whatwg.org/multipage/forms.html#attr-label-for
|
|
35
38
|
*/
|
|
@@ -39,7 +39,7 @@ document) {
|
|
|
39
39
|
* > - section
|
|
40
40
|
*/
|
|
41
41
|
const headers = [...document.querySelectorAll('header')].filter(header => {
|
|
42
|
-
return !header.closest('article, aside, main, nav, section');
|
|
42
|
+
return !header.closest('article, aside, main, nav, section') && !hasElement(roles.banner, header);
|
|
43
43
|
});
|
|
44
44
|
roles.banner.push(...headers);
|
|
45
45
|
/**
|
|
@@ -55,11 +55,23 @@ document) {
|
|
|
55
55
|
* > - section
|
|
56
56
|
*/
|
|
57
57
|
const footers = [...document.querySelectorAll('footer')].filter(footer => {
|
|
58
|
-
return !footer.closest('article, aside, main, nav, section');
|
|
58
|
+
return !footer.closest('article, aside, main, nav, section') && !hasElement(roles.contentinfo, footer);
|
|
59
59
|
});
|
|
60
60
|
roles.contentinfo.push(...footers);
|
|
61
61
|
return roles;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* An element already collected through its explicit `role` attribute (e.g.
|
|
65
|
+
* `<header role="banner">`) must not be added a second time by the implicit
|
|
66
|
+
* `<header>` / `<footer>` lookup, or a lone landmark would count as a duplicate (#4180).
|
|
67
|
+
*/
|
|
68
|
+
function hasElement(
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
70
|
+
elements,
|
|
71
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
72
|
+
target) {
|
|
73
|
+
return elements.some(element => element.uuid === target.uuid);
|
|
74
|
+
}
|
|
63
75
|
/**
|
|
64
76
|
* Collects all UUIDs of elements that have landmark roles.
|
|
65
77
|
*
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Require that a `<map>` element's `id` attribute, when present, has
|
|
3
|
-
* the same value as its `name` attribute. Per HTML LS §4.8.
|
|
4
|
-
*
|
|
5
|
-
* the same value.
|
|
3
|
+
* the same value as its `name` attribute. Per HTML LS §4.8.12:
|
|
4
|
+
*
|
|
5
|
+
* > If the id attribute is also specified, both attributes must have the same value.
|
|
6
6
|
*
|
|
7
7
|
* @see https://html.spec.whatwg.org/multipage/image-maps.html#the-map-element
|
|
8
8
|
*/
|
|
@@ -2,9 +2,9 @@ import { createRule } from '@markuplint/ml-core';
|
|
|
2
2
|
import meta from './meta.js';
|
|
3
3
|
/**
|
|
4
4
|
* Require that a `<map>` element's `id` attribute, when present, has
|
|
5
|
-
* the same value as its `name` attribute. Per HTML LS §4.8.
|
|
6
|
-
*
|
|
7
|
-
* the same value.
|
|
5
|
+
* the same value as its `name` attribute. Per HTML LS §4.8.12:
|
|
6
|
+
*
|
|
7
|
+
* > If the id attribute is also specified, both attributes must have the same value.
|
|
8
8
|
*
|
|
9
9
|
* @see https://html.spec.whatwg.org/multipage/image-maps.html#the-map-element
|
|
10
10
|
*/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRule, getAttrSpecs } from '@markuplint/ml-core';
|
|
2
2
|
import { valueCheck } from '../attr-check.js';
|
|
3
|
+
import { findAttrSpec } from '../helpers.js';
|
|
3
4
|
import meta from './meta.js';
|
|
4
5
|
export default createRule({
|
|
5
6
|
meta: meta,
|
|
@@ -13,7 +14,7 @@ export default createRule({
|
|
|
13
14
|
if (!attrSpecs) {
|
|
14
15
|
return;
|
|
15
16
|
}
|
|
16
|
-
const attrSpec = attrSpecs
|
|
17
|
+
const attrSpec = findAttrSpec(attrSpecs, attr);
|
|
17
18
|
if (attrSpec?.type !== 'NavigableTargetNameOrKeyword' && attrSpec?.type !== 'NavigableTargetName') {
|
|
18
19
|
return;
|
|
19
20
|
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { Options } from '../wai-aria/types.js';
|
|
2
2
|
/**
|
|
3
|
-
* ARIA in HTML
|
|
4
|
-
* "authors MUST NOT use `aria-hidden="true"` on any element which also has the
|
|
5
|
-
* `hidden` attribute specified in the Hidden Until Found state." Content revealed
|
|
6
|
-
* by the browser's find-in-page feature would otherwise stay excluded from the
|
|
7
|
-
* accessibility tree, silently breaking that reveal for assistive technology users.
|
|
3
|
+
* ARIA in HTML, section Rules of ARIA attribute usage by HTML feature, the `hidden` row:
|
|
8
4
|
*
|
|
9
|
-
*
|
|
5
|
+
* > Authors MUST NOT use `aria-hidden="true"` on any element which also has the `hidden` attribute specified in the
|
|
6
|
+
* > Hidden Until Found state.
|
|
7
|
+
*
|
|
8
|
+
* Content revealed by the browser's find-in-page feature would otherwise stay
|
|
9
|
+
* excluded from the accessibility tree, silently breaking that reveal for
|
|
10
|
+
* assistive technology users.
|
|
11
|
+
*
|
|
12
|
+
* @see https://www.w3.org/TR/html-aria/#att-hidden
|
|
10
13
|
*/
|
|
11
14
|
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, Options>>;
|
|
12
15
|
export default _default;
|
|
@@ -2,13 +2,16 @@ import { createRule } from '@markuplint/ml-core';
|
|
|
2
2
|
import { defaultOptions } from '../wai-aria/default-options.js';
|
|
3
3
|
import meta from './meta.js';
|
|
4
4
|
/**
|
|
5
|
-
* ARIA in HTML
|
|
6
|
-
* "authors MUST NOT use `aria-hidden="true"` on any element which also has the
|
|
7
|
-
* `hidden` attribute specified in the Hidden Until Found state." Content revealed
|
|
8
|
-
* by the browser's find-in-page feature would otherwise stay excluded from the
|
|
9
|
-
* accessibility tree, silently breaking that reveal for assistive technology users.
|
|
5
|
+
* ARIA in HTML, section Rules of ARIA attribute usage by HTML feature, the `hidden` row:
|
|
10
6
|
*
|
|
11
|
-
*
|
|
7
|
+
* > Authors MUST NOT use `aria-hidden="true"` on any element which also has the `hidden` attribute specified in the
|
|
8
|
+
* > Hidden Until Found state.
|
|
9
|
+
*
|
|
10
|
+
* Content revealed by the browser's find-in-page feature would otherwise stay
|
|
11
|
+
* excluded from the accessibility tree, silently breaking that reveal for
|
|
12
|
+
* assistive technology users.
|
|
13
|
+
*
|
|
14
|
+
* @see https://www.w3.org/TR/html-aria/#att-hidden
|
|
12
15
|
*/
|
|
13
16
|
export default createRule({
|
|
14
17
|
meta,
|
|
@@ -3,7 +3,7 @@ declare const _default: {
|
|
|
3
3
|
readonly specConformance: {
|
|
4
4
|
readonly sources: readonly ["aria-in-html"];
|
|
5
5
|
readonly level: "must";
|
|
6
|
-
readonly cites: readonly ["https://
|
|
6
|
+
readonly cites: readonly ["https://www.w3.org/TR/html-aria/#att-hidden"];
|
|
7
7
|
};
|
|
8
8
|
};
|
|
9
9
|
export default _default;
|
|
@@ -13,7 +13,9 @@ export default createRule({
|
|
|
13
13
|
return;
|
|
14
14
|
if (!elSpec.globalAttrs['#ARIAAttrs'])
|
|
15
15
|
return;
|
|
16
|
-
|
|
16
|
+
for (const attr of el.attributes) {
|
|
17
|
+
report(checkingPresentationalChildren({ attr, el }));
|
|
18
|
+
}
|
|
17
19
|
});
|
|
18
20
|
},
|
|
19
21
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRule, getSpec } from '@markuplint/ml-core';
|
|
2
|
-
import { Collection } from '../helpers.js';
|
|
2
|
+
import { Collection, isAriaAttr } from '../helpers.js';
|
|
3
3
|
import meta from './meta.js';
|
|
4
4
|
/**
|
|
5
5
|
* Extracted from the former `wai-aria` umbrella rule (#3989), which always
|
|
@@ -22,7 +22,7 @@ export default createRule({
|
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
const roleAttr = el.getAttributeNode('role');
|
|
25
|
-
const propAttrs = el.attributes.filter(attr =>
|
|
25
|
+
const propAttrs = el.attributes.filter(attr => isAriaAttr(attr));
|
|
26
26
|
const ariaAttrs = new Collection(roleAttr, ...propAttrs);
|
|
27
27
|
for (const ariaAttr of ariaAttrs) {
|
|
28
28
|
report({
|
|
@@ -3,7 +3,7 @@ declare const _default: {
|
|
|
3
3
|
readonly specConformance: {
|
|
4
4
|
readonly sources: readonly ["aria-in-html"];
|
|
5
5
|
readonly level: "must";
|
|
6
|
-
readonly cites: readonly ["https://
|
|
6
|
+
readonly cites: readonly ["https://www.w3.org/TR/html-aria/#docconformance"];
|
|
7
7
|
};
|
|
8
8
|
};
|
|
9
9
|
export default _default;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRule, getAttrSpecs } from '@markuplint/ml-core';
|
|
2
|
-
import { removeAttrValue } from '../helpers.js';
|
|
2
|
+
import { findAttrSpec, removeAttrValue } from '../helpers.js';
|
|
3
3
|
import meta from './meta.js';
|
|
4
4
|
export default createRule({
|
|
5
5
|
meta: meta,
|
|
@@ -14,7 +14,7 @@ export default createRule({
|
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
16
|
const name = attr.name;
|
|
17
|
-
const spec = attrSpec
|
|
17
|
+
const spec = findAttrSpec(attrSpec, attr);
|
|
18
18
|
if (!spec) {
|
|
19
19
|
return;
|
|
20
20
|
}
|