@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.
Files changed (123) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/CLAUDE.md +19 -19
  3. package/lib/aria-prop-requires-role/index.js +2 -1
  4. package/lib/attr-check.js +6 -12
  5. package/lib/attr-eligibility.d.ts +15 -8
  6. package/lib/attr-eligibility.js +19 -10
  7. package/lib/attr-order/index.js +24 -18
  8. package/lib/case-sensitive-attr-name/index.js +24 -5
  9. package/lib/class-naming/index.js +6 -0
  10. package/lib/debug.d.ts +8 -1
  11. package/lib/debug.js +8 -1
  12. package/lib/element-supports-aria-prop/index.js +2 -1
  13. package/lib/form-attr-references-form/index.d.ts +6 -3
  14. package/lib/form-attr-references-form/index.js +6 -3
  15. package/lib/helpers.d.ts +111 -1
  16. package/lib/helpers.js +212 -2
  17. package/lib/input-list-references-datalist/index.d.ts +8 -6
  18. package/lib/input-list-references-datalist/index.js +8 -6
  19. package/lib/label-for-references-labelable/index.d.ts +8 -5
  20. package/lib/label-for-references-labelable/index.js +8 -5
  21. package/lib/landmark-roles/roles.js +14 -2
  22. package/lib/map-id-name-match/index.d.ts +3 -3
  23. package/lib/map-id-name-match/index.js +3 -3
  24. package/lib/no-ambiguous-navigable-target-names/index.js +2 -1
  25. package/lib/no-aria-hidden-on-hidden-until-found/index.d.ts +9 -6
  26. package/lib/no-aria-hidden-on-hidden-until-found/index.js +9 -6
  27. package/lib/no-aria-hidden-on-hidden-until-found/meta.d.ts +1 -1
  28. package/lib/no-aria-hidden-on-hidden-until-found/meta.js +1 -1
  29. package/lib/no-aria-on-presentational-children/index.js +3 -1
  30. package/lib/no-aria-on-unsupported-element/index.js +2 -2
  31. package/lib/no-aria-on-unsupported-element/meta.d.ts +1 -1
  32. package/lib/no-aria-on-unsupported-element/meta.js +1 -1
  33. package/lib/no-boolean-attr-value/index.js +2 -2
  34. package/lib/no-content-after-body/index.d.ts +2 -2
  35. package/lib/no-content-after-body/index.js +2 -2
  36. package/lib/no-contradictory-aria-prop/index.js +2 -1
  37. package/lib/no-default-aria-value/index.js +2 -1
  38. package/lib/no-default-value/index.js +2 -2
  39. package/lib/no-deprecated-aria-prop/index.js +2 -1
  40. package/lib/no-deprecated-attr/index.js +2 -1
  41. package/lib/no-disallowed-attr/index.d.ts +7 -1
  42. package/lib/no-disallowed-attr/index.js +13 -20
  43. package/lib/no-duplicate-attr/index.js +1 -2
  44. package/lib/no-duplicate-autofocus/index.js +2 -2
  45. package/lib/no-duplicate-h1/index.d.ts +1 -1
  46. package/lib/no-duplicate-h1/index.js +1 -1
  47. package/lib/no-empty-table-track/index.d.ts +6 -2
  48. package/lib/no-empty-table-track/index.js +6 -2
  49. package/lib/no-experimental-features/index.d.ts +5 -1
  50. package/lib/no-experimental-features/index.js +6 -8
  51. package/lib/no-hardcoded-id/index.js +1 -1
  52. package/lib/no-ineffective-attr/index.js +2 -2
  53. package/lib/no-input-file-value/index.d.ts +10 -4
  54. package/lib/no-input-file-value/index.js +10 -4
  55. package/lib/no-invalid-aria-prop-value/index.js +2 -1
  56. package/lib/no-invalid-attr-value/index.d.ts +3 -0
  57. package/lib/no-invalid-attr-value/index.js +9 -24
  58. package/lib/no-nonstandard-features/index.d.ts +5 -1
  59. package/lib/no-nonstandard-features/index.js +6 -8
  60. package/lib/no-obsolete-attr/index.js +3 -3
  61. package/lib/no-prohibited-naming/index.js +2 -1
  62. package/lib/no-redundant-accessible-name/index.js +17 -1
  63. package/lib/no-redundant-aria-prop/index.js +2 -1
  64. package/lib/no-refer-to-non-existent-id/index.js +4 -4
  65. package/lib/no-restricted-attr/index.d.ts +4 -0
  66. package/lib/no-restricted-attr/index.js +5 -22
  67. package/lib/no-stray-head-or-body-tag/index.d.ts +6 -4
  68. package/lib/no-stray-head-or-body-tag/index.js +11 -6
  69. package/lib/no-unknown-attr/index.d.ts +7 -1
  70. package/lib/no-unknown-attr/index.js +11 -25
  71. package/lib/no-unpaired-srcset-sizes/index.d.ts +3 -2
  72. package/lib/no-unpaired-srcset-sizes/index.js +23 -17
  73. package/lib/no-unsupported-browser-features/compat-data.d.ts +15 -2
  74. package/lib/no-unsupported-browser-features/compat-data.js +48 -3
  75. package/lib/no-unsupported-browser-features/index.d.ts +5 -1
  76. package/lib/no-unsupported-browser-features/index.js +6 -7
  77. package/lib/permitted-contents/choice.js +19 -8
  78. package/lib/permitted-contents/content-model.d.ts +24 -1
  79. package/lib/permitted-contents/content-model.js +56 -9
  80. package/lib/permitted-contents/count-pattern.js +36 -13
  81. package/lib/permitted-contents/debug.browser.d.ts +18 -0
  82. package/lib/permitted-contents/debug.browser.js +27 -0
  83. package/lib/permitted-contents/debug.d.ts +18 -0
  84. package/lib/permitted-contents/debug.js +27 -0
  85. package/lib/permitted-contents/index.js +2 -2
  86. package/lib/permitted-contents/matches-selector.js +40 -18
  87. package/lib/permitted-contents/order.js +23 -11
  88. package/lib/permitted-contents/represent-transparent-nodes.js +3 -1
  89. package/lib/permitted-contents/transparent.js +6 -2
  90. package/lib/placeholder-label-option/index.d.ts +3 -3
  91. package/lib/placeholder-label-option/index.js +163 -37
  92. package/lib/progress-value-bounds/index.d.ts +1 -1
  93. package/lib/progress-value-bounds/index.js +1 -1
  94. package/lib/require-attr/index.d.ts +6 -1
  95. package/lib/require-attr/index.js +13 -5
  96. package/lib/require-h1/index.d.ts +1 -1
  97. package/lib/require-h1/index.js +1 -1
  98. package/lib/role-supports-aria-prop/index.js +2 -1
  99. package/lib/sizes-auto-requires-lazy-loading/index.d.ts +3 -1
  100. package/lib/sizes-auto-requires-lazy-loading/index.js +30 -16
  101. package/lib/srcset-sizes-constraint/parse-srcset.d.ts +6 -5
  102. package/lib/srcset-sizes-constraint/parse-srcset.js +8 -7
  103. package/lib/srcset-sizes-constraint/sibling-helpers.d.ts +31 -0
  104. package/lib/srcset-sizes-constraint/sibling-helpers.js +58 -0
  105. package/lib/valid-speculation-rules/index.js +1 -1
  106. package/lib/wai-aria/checkings/contradictory-aria-prop.js +3 -5
  107. package/lib/wai-aria/checkings/default-value.js +1 -1
  108. package/lib/wai-aria/checkings/deprecated-props.js +2 -2
  109. package/lib/wai-aria/checkings/element-supports-aria-prop.js +5 -4
  110. package/lib/wai-aria/checkings/interaction-in-hidden.d.ts +1 -1
  111. package/lib/wai-aria/checkings/interaction-in-hidden.js +1 -1
  112. package/lib/wai-aria/checkings/no-global-prop.js +1 -1
  113. package/lib/wai-aria/checkings/presentational-children.d.ts +21 -10
  114. package/lib/wai-aria/checkings/presentational-children.js +240 -26
  115. package/lib/wai-aria/checkings/prohibited-naming.js +5 -4
  116. package/lib/wai-aria/checkings/redundant-aria-prop.js +3 -5
  117. package/lib/wai-aria/checkings/required-prop.js +1 -4
  118. package/lib/wai-aria/checkings/role-supports-aria-prop.js +4 -3
  119. package/lib/wai-aria/checkings/tab-requires-tabpanel.d.ts +3 -2
  120. package/lib/wai-aria/checkings/tab-requires-tabpanel.js +3 -2
  121. package/lib/wai-aria/checkings/value.d.ts +1 -1
  122. package/lib/wai-aria/checkings/value.js +2 -2
  123. 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.toLowerCase());
29
+ referencedNames.add(name);
29
30
  }
30
31
  }
31
32
  }
32
- return node.attributes.some(attr => attr.isDynamicValue && referencedNames.has(attr.localName.toLowerCase()));
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.2: "The `list`
4
- * content attribute is used to identify an element that lists predefined
5
- * options to suggest to the user. If present, its value must be the ID of
6
- * a `datalist` element in the same tree." Existence of the ID itself is
7
- * the responsibility of `no-refer-to-non-existent-id`; this rule only
8
- * checks the element type when the ID resolves.
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.2: "The `list`
6
- * content attribute is used to identify an element that lists predefined
7
- * options to suggest to the user. If present, its value must be the ID of
8
- * a `datalist` element in the same tree." Existence of the ID itself is
9
- * the responsibility of `no-refer-to-non-existent-id`; this rule only
10
- * checks the element type when the ID resolves.
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: "If the attribute
4
- * is specified, the attribute's value must be the ID of a labelable
5
- * element in the same tree as the label element." Existence of the ID
6
- * itself is the responsibility of `no-refer-to-non-existent-id`; this
7
- * rule only checks the element type when the ID resolves.
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: "If the attribute
29
- * is specified, the attribute's value must be the ID of a labelable
30
- * element in the same tree as the label element." Existence of the ID
31
- * itself is the responsibility of `no-refer-to-non-existent-id`; this
32
- * rule only checks the element type when the ID resolves.
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.13:
4
- * "If the id attribute is also specified, both attributes must have
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.13:
6
- * "If the id attribute is also specified, both attributes must have
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.find(spec => spec.name === attr.name);
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 §"Rules of ARIA attribute usage by HTML feature", the `hidden` row:
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
- * @see https://w3c.github.io/html-aria/#att-hidden
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 §"Rules of ARIA attribute usage by HTML feature", the `hidden` row:
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
- * @see https://w3c.github.io/html-aria/#att-hidden
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://w3c.github.io/html-aria/#att-hidden"];
6
+ readonly cites: readonly ["https://www.w3.org/TR/html-aria/#att-hidden"];
7
7
  };
8
8
  };
9
9
  export default _default;
@@ -3,6 +3,6 @@ export default {
3
3
  specConformance: {
4
4
  sources: ['aria-in-html'],
5
5
  level: 'must',
6
- cites: ['https://w3c.github.io/html-aria/#att-hidden'],
6
+ cites: ['https://www.w3.org/TR/html-aria/#att-hidden'],
7
7
  },
8
8
  };
@@ -13,7 +13,9 @@ export default createRule({
13
13
  return;
14
14
  if (!elSpec.globalAttrs['#ARIAAttrs'])
15
15
  return;
16
- report(checkingPresentationalChildren({ el }));
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 => /^aria-/i.test(attr.name));
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://w3c.github.io/html-aria/#docconformance"];
6
+ readonly cites: readonly ["https://www.w3.org/TR/html-aria/#docconformance"];
7
7
  };
8
8
  };
9
9
  export default _default;
@@ -3,6 +3,6 @@ export default {
3
3
  specConformance: {
4
4
  sources: ['aria-in-html'],
5
5
  level: 'must',
6
- cites: ['https://w3c.github.io/html-aria/#docconformance'],
6
+ cites: ['https://www.w3.org/TR/html-aria/#docconformance'],
7
7
  },
8
8
  };
@@ -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.find(s => s.name === name);
17
+ const spec = findAttrSpec(attrSpec, attr);
18
18
  if (!spec) {
19
19
  return;
20
20
  }