@markuplint/rules 5.0.0-rc.0 → 5.0.0-rc.2
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 +26 -0
- package/CLAUDE.md +27 -0
- package/SKILL.md +1 -0
- package/lib/attr-check.d.ts +3 -1
- package/lib/attr-check.js +10 -1
- package/lib/helpers.js +16 -1
- package/lib/index.d.ts +25 -7
- package/lib/index.js +36 -0
- package/lib/invalid-attr/index.js +12 -0
- package/lib/no-duplicate-autofocus/index.d.ts +7 -0
- package/lib/no-duplicate-autofocus/index.js +27 -0
- package/lib/no-duplicate-autofocus/meta.d.ts +4 -0
- package/lib/no-duplicate-autofocus/meta.js +3 -0
- package/lib/no-duplicate-visible-main/index.d.ts +8 -0
- package/lib/no-duplicate-visible-main/index.js +28 -0
- package/lib/no-duplicate-visible-main/meta.d.ts +4 -0
- package/lib/no-duplicate-visible-main/meta.js +3 -0
- package/lib/permitted-contents/choice.d.ts +5 -2
- package/lib/permitted-contents/choice.js +5 -2
- package/lib/permitted-contents/complex-branch.d.ts +5 -2
- package/lib/permitted-contents/complex-branch.js +6 -3
- package/lib/permitted-contents/content-model.d.ts +21 -2
- package/lib/permitted-contents/content-model.js +32 -6
- package/lib/permitted-contents/count-pattern.d.ts +5 -2
- package/lib/permitted-contents/count-pattern.js +13 -5
- package/lib/permitted-contents/index.d.ts +7 -1
- package/lib/permitted-contents/index.js +160 -65
- package/lib/permitted-contents/matches-selector.d.ts +2 -2
- package/lib/permitted-contents/matches-selector.js +43 -5
- package/lib/permitted-contents/order.d.ts +9 -2
- package/lib/permitted-contents/order.js +9 -2
- package/lib/permitted-contents/recursive-branch.d.ts +5 -2
- package/lib/permitted-contents/recursive-branch.js +7 -4
- package/lib/permitted-contents/represent-transparent-nodes.d.ts +2 -2
- package/lib/permitted-contents/represent-transparent-nodes.js +6 -6
- package/lib/permitted-contents/start.d.ts +2 -2
- package/lib/permitted-contents/start.js +3 -3
- package/lib/permitted-contents/types.d.ts +14 -0
- package/lib/permitted-contents/utils.d.ts +11 -2
- package/lib/permitted-contents/utils.js +62 -13
- package/lib/wai-aria/checkings/disallowed-prop.d.ts +2 -1
- package/lib/wai-aria/checkings/disallowed-prop.js +27 -5
- package/lib/wai-aria/checkings/permitted-roles.js +16 -3
- package/lib/wai-aria/checkings/required-accessibility-parent-role.js +1 -1
- package/lib/wai-aria/default-options.d.ts +5 -0
- package/lib/wai-aria/default-options.js +18 -0
- package/lib/wai-aria/index.js +8 -19
- package/lib/wai-aria-abstract-role/index.d.ts +4 -0
- package/lib/wai-aria-abstract-role/index.js +22 -0
- package/lib/wai-aria-abstract-role/meta.d.ts +4 -0
- package/lib/wai-aria-abstract-role/meta.js +3 -0
- package/lib/wai-aria-default-value/index.d.ts +4 -0
- package/lib/wai-aria-default-value/index.js +28 -0
- package/lib/wai-aria-default-value/meta.d.ts +5 -0
- package/lib/wai-aria-default-value/meta.js +4 -0
- package/lib/wai-aria-deprecated-props/index.d.ts +4 -0
- package/lib/wai-aria-deprecated-props/index.js +29 -0
- package/lib/wai-aria-deprecated-props/meta.d.ts +5 -0
- package/lib/wai-aria-deprecated-props/meta.js +4 -0
- package/lib/wai-aria-deprecated-role/index.d.ts +4 -0
- package/lib/wai-aria-deprecated-role/index.js +26 -0
- package/lib/wai-aria-deprecated-role/meta.d.ts +4 -0
- package/lib/wai-aria-deprecated-role/meta.js +3 -0
- package/lib/wai-aria-disallowed-props/index.d.ts +4 -0
- package/lib/wai-aria-disallowed-props/index.js +33 -0
- package/lib/wai-aria-disallowed-props/meta.d.ts +5 -0
- package/lib/wai-aria-disallowed-props/meta.js +4 -0
- package/lib/wai-aria-implicit-props/index.d.ts +4 -0
- package/lib/wai-aria-implicit-props/index.js +29 -0
- package/lib/wai-aria-implicit-props/meta.d.ts +5 -0
- package/lib/wai-aria-implicit-props/meta.js +4 -0
- package/lib/wai-aria-implicit-role/index.d.ts +4 -0
- package/lib/wai-aria-implicit-role/index.js +23 -0
- package/lib/wai-aria-implicit-role/meta.d.ts +4 -0
- package/lib/wai-aria-implicit-role/meta.js +3 -0
- package/lib/wai-aria-interaction-in-hidden/index.d.ts +4 -0
- package/lib/wai-aria-interaction-in-hidden/index.js +20 -0
- package/lib/wai-aria-interaction-in-hidden/meta.d.ts +5 -0
- package/lib/wai-aria-interaction-in-hidden/meta.js +4 -0
- package/lib/wai-aria-no-global-prop/index.d.ts +4 -0
- package/lib/wai-aria-no-global-prop/index.js +30 -0
- package/lib/wai-aria-no-global-prop/meta.d.ts +5 -0
- package/lib/wai-aria-no-global-prop/meta.js +4 -0
- package/lib/wai-aria-non-existent-role/index.d.ts +4 -0
- package/lib/wai-aria-non-existent-role/index.js +22 -0
- package/lib/wai-aria-non-existent-role/meta.d.ts +4 -0
- package/lib/wai-aria-non-existent-role/meta.js +3 -0
- package/lib/wai-aria-permitted-roles/index.d.ts +4 -0
- package/lib/wai-aria-permitted-roles/index.js +22 -0
- package/lib/wai-aria-permitted-roles/meta.d.ts +4 -0
- package/lib/wai-aria-permitted-roles/meta.js +3 -0
- package/lib/wai-aria-presentational-children/index.d.ts +4 -0
- package/lib/wai-aria-presentational-children/index.js +20 -0
- package/lib/wai-aria-presentational-children/meta.d.ts +5 -0
- package/lib/wai-aria-presentational-children/meta.js +4 -0
- package/lib/wai-aria-required-owned-elements/index.d.ts +4 -0
- package/lib/wai-aria-required-owned-elements/index.js +23 -0
- package/lib/wai-aria-required-owned-elements/meta.d.ts +4 -0
- package/lib/wai-aria-required-owned-elements/meta.js +3 -0
- package/lib/wai-aria-required-parent-role/index.d.ts +4 -0
- package/lib/wai-aria-required-parent-role/index.js +25 -0
- package/lib/wai-aria-required-parent-role/meta.d.ts +4 -0
- package/lib/wai-aria-required-parent-role/meta.js +3 -0
- package/lib/wai-aria-required-props/index.d.ts +4 -0
- package/lib/wai-aria-required-props/index.js +26 -0
- package/lib/wai-aria-required-props/meta.d.ts +4 -0
- package/lib/wai-aria-required-props/meta.js +3 -0
- package/lib/wai-aria-value/index.d.ts +4 -0
- package/lib/wai-aria-value/index.js +28 -0
- package/lib/wai-aria-value/meta.d.ts +5 -0
- package/lib/wai-aria-value/meta.js +4 -0
- package/package.json +15 -11
- package/schema.json +54 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ContentModelResult, Element, Options, Specs } from './types.js';
|
|
1
|
+
import type { ContentModelResult, Element, Mode, Options, Specs, TagRule } from './types.js';
|
|
2
2
|
import type { ContentModel } from '@markuplint/ml-spec';
|
|
3
3
|
import type { ReadonlyDeep } from 'type-fest';
|
|
4
4
|
/**
|
|
@@ -16,4 +16,4 @@ import type { ReadonlyDeep } from 'type-fest';
|
|
|
16
16
|
* @param options - Validation behavior options.
|
|
17
17
|
* @returns An array of content model results describing any violations found.
|
|
18
18
|
*/
|
|
19
|
-
export declare function start(contents: ReadonlyDeep<ContentModel['contents']>, el: Element, specs: Specs, options: Options): ContentModelResult[];
|
|
19
|
+
export declare function start(contents: ReadonlyDeep<ContentModel['contents']>, el: Element, rules: readonly TagRule[], specs: Specs, options: Options, mode: Mode): ContentModelResult[];
|
|
@@ -17,7 +17,7 @@ import { representTransparentNodes } from './represent-transparent-nodes.js';
|
|
|
17
17
|
*/
|
|
18
18
|
export function start(contents,
|
|
19
19
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
20
|
-
el, specs, options) {
|
|
20
|
+
el, rules, specs, options, mode) {
|
|
21
21
|
const childNodesPatterns = options.evaluateConditionalChildNodes
|
|
22
22
|
? el.conditionalChildNodes().map(childNodes => [...childNodes])
|
|
23
23
|
: [[...el.childNodes].filter(child => !(child.is(child.TEXT_NODE) && child.isWhitespace()))];
|
|
@@ -53,9 +53,9 @@ el, specs, options) {
|
|
|
53
53
|
},
|
|
54
54
|
];
|
|
55
55
|
}
|
|
56
|
-
const patterns = representTransparentNodes(childNodes, specs, options);
|
|
56
|
+
const patterns = representTransparentNodes(childNodes, rules, specs, options, mode);
|
|
57
57
|
return patterns.flatMap(({ nodes, errors }) => {
|
|
58
|
-
const result = order(contents, nodes, specs, options, 0);
|
|
58
|
+
const result = order(contents, nodes, rules, specs, options, 0, mode);
|
|
59
59
|
return [
|
|
60
60
|
{
|
|
61
61
|
type: result.type,
|
|
@@ -103,6 +103,20 @@ export type TransparentModel = {
|
|
|
103
103
|
export type TagRule = {
|
|
104
104
|
readonly tag: string;
|
|
105
105
|
} & ReadonlyDeep<ContentModel>;
|
|
106
|
+
/**
|
|
107
|
+
* Which identity of a pretendered element is being evaluated during a
|
|
108
|
+
* content-model check. `'pretended'` is the default view: the element is
|
|
109
|
+
* validated against the HTML spec for the pretender target name (e.g. a
|
|
110
|
+
* `<Breadcrumbs>` pretending to `<nav>` is validated as `<nav>`).
|
|
111
|
+
* `'origin'` re-runs the validation with the pretender context suppressed,
|
|
112
|
+
* so the element is validated against any user-defined tag rule keyed on
|
|
113
|
+
* the original AST name (e.g. `Breadcrumbs`).
|
|
114
|
+
*
|
|
115
|
+
* The mode is threaded through every helper so that selector matching,
|
|
116
|
+
* transparent-model resolution, and content-model lookups all agree on the
|
|
117
|
+
* same view of the element identity.
|
|
118
|
+
*/
|
|
119
|
+
export type Mode = 'origin' | 'pretended';
|
|
106
120
|
/**
|
|
107
121
|
* Options for the permitted-contents rule that control validation behavior.
|
|
108
122
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChildNode, Hints, MissingNodeReason, RepeatSign, Specs } from './types.js';
|
|
1
|
+
import type { ChildNode, Hints, MissingNodeReason, Mode, RepeatSign, Specs } from './types.js';
|
|
2
2
|
import type { PermittedContentPattern, PermittedContentChoice, PermittedContentOneOrMore, PermittedContentOptional, PermittedContentRequire, PermittedContentTransparent, PermittedContentZeroOrMore, Model } from '@markuplint/ml-spec';
|
|
3
3
|
import type { ReadonlyDeep } from 'type-fest';
|
|
4
4
|
/**
|
|
@@ -16,12 +16,21 @@ export declare function isModel(model: ReadonlyDeep<Model | PermittedContentPatt
|
|
|
16
16
|
* selector engine. Returns whether the node matched and, if not, the deepest
|
|
17
17
|
* unmatched descendant node for diagnostic purposes.
|
|
18
18
|
*
|
|
19
|
+
* When `mode === 'origin'` and the node has an active pretender context, the
|
|
20
|
+
* context is temporarily suppressed so that the selector engine reads the
|
|
21
|
+
* element's original AST identity (`rawName` / original attrs) instead of the
|
|
22
|
+
* pretender target. The mutation mirrors the pattern already used by
|
|
23
|
+
* `MLElement.matchMLSelector` and is safe because the selector engine runs
|
|
24
|
+
* synchronously — the original context is restored in `finally` before the
|
|
25
|
+
* call returns.
|
|
26
|
+
*
|
|
19
27
|
* @param selector - The CSS selector string to test against.
|
|
20
28
|
* @param node - The child node to test.
|
|
21
29
|
* @param specs - The spec data passed to the selector engine for attribute resolution.
|
|
30
|
+
* @param mode - Which identity to match against (`'pretended'` is the default view).
|
|
22
31
|
* @returns An object with `matched: true` if the node matches, or `matched: false` with an optional `not` node.
|
|
23
32
|
*/
|
|
24
|
-
export declare function matches(selector: string, node: ChildNode, specs: Specs): {
|
|
33
|
+
export declare function matches(selector: string, node: ChildNode, specs: Specs, mode: Mode): {
|
|
25
34
|
matched: boolean;
|
|
26
35
|
not?: undefined;
|
|
27
36
|
} | {
|
|
@@ -31,29 +31,78 @@ export function isModel(model) {
|
|
|
31
31
|
* selector engine. Returns whether the node matched and, if not, the deepest
|
|
32
32
|
* unmatched descendant node for diagnostic purposes.
|
|
33
33
|
*
|
|
34
|
+
* When `mode === 'origin'` and the node has an active pretender context, the
|
|
35
|
+
* context is temporarily suppressed so that the selector engine reads the
|
|
36
|
+
* element's original AST identity (`rawName` / original attrs) instead of the
|
|
37
|
+
* pretender target. The mutation mirrors the pattern already used by
|
|
38
|
+
* `MLElement.matchMLSelector` and is safe because the selector engine runs
|
|
39
|
+
* synchronously — the original context is restored in `finally` before the
|
|
40
|
+
* call returns.
|
|
41
|
+
*
|
|
34
42
|
* @param selector - The CSS selector string to test against.
|
|
35
43
|
* @param node - The child node to test.
|
|
36
44
|
* @param specs - The spec data passed to the selector engine for attribute resolution.
|
|
45
|
+
* @param mode - Which identity to match against (`'pretended'` is the default view).
|
|
37
46
|
* @returns An object with `matched: true` if the node matches, or `matched: false` with an optional `not` node.
|
|
38
47
|
*/
|
|
39
48
|
export function matches(selector,
|
|
40
49
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
41
|
-
node, specs) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
node, specs, mode) {
|
|
51
|
+
let savedPretenderContext = null;
|
|
52
|
+
if (mode === 'origin') {
|
|
53
|
+
savedPretenderContext = suppressPretender(node);
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const selectorResult = createSelector(selector, specs).search(node);
|
|
57
|
+
const matched = selectorResult.filter((r) => r.matched);
|
|
58
|
+
if (matched.length > 0) {
|
|
59
|
+
return {
|
|
60
|
+
matched: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const not = selectorResult
|
|
64
|
+
.flatMap(r => (r.matched ? [] : (r.not ?? [])))
|
|
65
|
+
.flatMap(descendants)
|
|
66
|
+
.shift();
|
|
45
67
|
return {
|
|
46
|
-
matched:
|
|
68
|
+
matched: false,
|
|
69
|
+
not,
|
|
47
70
|
};
|
|
48
71
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
72
|
+
finally {
|
|
73
|
+
if (savedPretenderContext) {
|
|
74
|
+
restorePretender(node, savedPretenderContext);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Temporarily hides the pretender context on an element so that selector
|
|
80
|
+
* matching sees its original AST identity. Returns the saved context for
|
|
81
|
+
* later restoration, or `null` if the node is not an element with an active
|
|
82
|
+
* pretender.
|
|
83
|
+
*/
|
|
84
|
+
function suppressPretender(
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
86
|
+
node) {
|
|
87
|
+
if (!node.is(node.ELEMENT_NODE)) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const pretenderContext = node.pretenderContext;
|
|
91
|
+
if (pretenderContext?.type !== 'pretender') {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
node.pretenderContext = null;
|
|
95
|
+
return pretenderContext;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Restores a pretender context previously saved by {@link suppressPretender}.
|
|
99
|
+
*/
|
|
100
|
+
function restorePretender(
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
102
|
+
node, saved) {
|
|
103
|
+
if (node.is(node.ELEMENT_NODE)) {
|
|
104
|
+
node.pretenderContext = saved;
|
|
105
|
+
}
|
|
57
106
|
}
|
|
58
107
|
/**
|
|
59
108
|
* Traverses a chain of nested `:has()` selector results to find the deepest
|
|
@@ -7,7 +7,8 @@ import type { ARIAProperty, ARIARole } from '@markuplint/ml-spec';
|
|
|
7
7
|
* Each ARIA role defines a set of supported states and properties. This checker reports
|
|
8
8
|
* usage of ARIA attributes that are not in that set. It also considers element-specific
|
|
9
9
|
* restrictions from the ARIA in HTML specification (e.g., properties that should not
|
|
10
|
-
* be used on certain native HTML elements
|
|
10
|
+
* be used on certain native HTML elements, and naming prohibition for elements with
|
|
11
|
+
* no implicit role such as `<cite>` or `<abbr>`).
|
|
11
12
|
*
|
|
12
13
|
* @param attr - The ARIA attribute node to inspect.
|
|
13
14
|
* @param role - The computed ARIA role of the element.
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { ARIA_RECOMMENDED_VERSION, getARIA } from '@markuplint/ml-spec';
|
|
2
|
+
/**
|
|
3
|
+
* ARIA naming attributes subject to the "naming prohibition" constraint
|
|
4
|
+
* defined by ARIA in HTML.
|
|
5
|
+
*
|
|
6
|
+
* @see https://w3c.github.io/html-aria/#dfn-naming-prohibited
|
|
7
|
+
*/
|
|
8
|
+
const NAMING_ATTRS = new Set(['aria-label', 'aria-labelledby', 'aria-braillelabel']);
|
|
2
9
|
/**
|
|
3
10
|
* Checks whether an ARIA property or state is disallowed on the element's computed role.
|
|
4
11
|
*
|
|
5
12
|
* Each ARIA role defines a set of supported states and properties. This checker reports
|
|
6
13
|
* usage of ARIA attributes that are not in that set. It also considers element-specific
|
|
7
14
|
* restrictions from the ARIA in HTML specification (e.g., properties that should not
|
|
8
|
-
* be used on certain native HTML elements
|
|
15
|
+
* be used on certain native HTML elements, and naming prohibition for elements with
|
|
16
|
+
* no implicit role such as `<cite>` or `<abbr>`).
|
|
9
17
|
*
|
|
10
18
|
* @param attr - The ARIA attribute node to inspect.
|
|
11
19
|
* @param role - The computed ARIA role of the element.
|
|
@@ -14,18 +22,32 @@ import { ARIA_RECOMMENDED_VERSION, getARIA } from '@markuplint/ml-spec';
|
|
|
14
22
|
* @returns A violation if the property is not allowed on the given role or element.
|
|
15
23
|
*/
|
|
16
24
|
export const checkingDisallowedProp = ({ attr, role, propSpecs, disallowSetImplicitProps }) => t => {
|
|
17
|
-
if (!role) {
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
25
|
if (!/^aria-/i.test(attr.name)) {
|
|
21
26
|
return;
|
|
22
27
|
}
|
|
23
28
|
const ariaVersion = attr.rule.options?.version ??
|
|
24
29
|
attr.ownerMLDocument.ruleCommonSettings?.ariaVersion ??
|
|
25
30
|
ARIA_RECOMMENDED_VERSION;
|
|
26
|
-
const statesAndProp = role.ownedProperties.find(p => p.name === attr.name);
|
|
27
31
|
const propSpec = propSpecs.find(p => p.name === attr.name);
|
|
28
32
|
const elAriaSpec = getARIA(attr.ownerMLDocument.specs, attr.ownerElement.localName, attr.ownerElement.namespaceURI, ariaVersion, attr.ownerElement.matches.bind(attr.ownerElement));
|
|
33
|
+
// Naming prohibition (ARIA in HTML): elements with namingProhibited=true
|
|
34
|
+
// must not use aria-label / aria-labelledby / aria-braillelabel unless
|
|
35
|
+
// an explicit role that supports naming is set. When `role` is null and
|
|
36
|
+
// the element is namingProhibited, the naming attrs are prohibited.
|
|
37
|
+
// At time of writing, the affected elements (implicitRole=false +
|
|
38
|
+
// namingProhibited=true in html-spec) are:
|
|
39
|
+
// abbr, cite, figcaption, kbd, label, legend, mark, rt, var
|
|
40
|
+
// This list is not hard-coded here; it is derived from html-spec data.
|
|
41
|
+
if (!role && NAMING_ATTRS.has(attr.name) && elAriaSpec?.namingProhibited === true) {
|
|
42
|
+
return {
|
|
43
|
+
scope: attr,
|
|
44
|
+
message: t('{0:c} on {1}', t('{0} is {1:c}', t('the "{0*}" {1}', attr.name, `ARIA ${propSpec?.type ?? 'property'}`), 'prohibited'), t('the "{0*}" {1}', attr.ownerElement.localName, 'element')),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (!role) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const statesAndProp = role.ownedProperties.find(p => p.name === attr.name);
|
|
29
51
|
if (disallowSetImplicitProps && elAriaSpec?.properties !== false && elAriaSpec?.properties?.without) {
|
|
30
52
|
for (const ignore of elAriaSpec.properties.without) {
|
|
31
53
|
if (ignore.name === attr.name) {
|
|
@@ -15,13 +15,26 @@ export const checkingPermittedRoles = ({ attr }) => t => {
|
|
|
15
15
|
ARIA_RECOMMENDED_VERSION;
|
|
16
16
|
const el = attr.ownerElement;
|
|
17
17
|
const permittedRoles = getPermittedRoles(el, ariaVersion, attr.ownerMLDocument.specs);
|
|
18
|
+
const tokens = attr.tokenList?.allTokens();
|
|
19
|
+
// When no explicit role is permitted on this element, report against each token
|
|
20
|
+
// so the offending role name is preserved in the message (same scope as the
|
|
21
|
+
// per-token branch below). If there are no tokens, fall back to the whole attr.
|
|
18
22
|
if (permittedRoles.length === 0) {
|
|
23
|
+
if (!tokens || tokens.length === 0) {
|
|
24
|
+
return {
|
|
25
|
+
scope: attr,
|
|
26
|
+
message: t('{0} according to {1}', t('Cannot overwrite {0}', t('{0} of {1}', t('the {0}', 'role'), t('the "{0*}" {1}', el.localName, 'element'))), 'ARIA in HTML specification'),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const firstToken = tokens[0];
|
|
30
|
+
if (!firstToken) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
19
33
|
return {
|
|
20
|
-
scope:
|
|
21
|
-
message: t('{0} according to {1}', t('Cannot overwrite {0}
|
|
34
|
+
scope: firstToken,
|
|
35
|
+
message: t('{0} according to {1}', t('Cannot overwrite {0} to {1}', t('the "{0*}" {1}', firstToken.raw, 'role'), t('the "{0*}" {1}', el.localName, 'element')), 'ARIA in HTML specification'),
|
|
22
36
|
};
|
|
23
37
|
}
|
|
24
|
-
const tokens = attr.tokenList?.allTokens();
|
|
25
38
|
if (!tokens) {
|
|
26
39
|
return;
|
|
27
40
|
}
|
|
@@ -41,7 +41,7 @@ export const checkingRequiredAccessibilityParentRole = ({ el, computed }) => t =
|
|
|
41
41
|
// `ariaSpecs().roles` returns raw spec data where the field is always
|
|
42
42
|
// `requiredContextRole` for all versions. The fallback to
|
|
43
43
|
// `requiredAccessibilityParentRole` is a defensive guard in case
|
|
44
|
-
// the
|
|
44
|
+
// the generator changes the field name in the future.
|
|
45
45
|
const parentRoles = roleSpec.requiredContextRole ?? roleSpec.requiredAccessibilityParentRole;
|
|
46
46
|
if (parentRoles && parentRoles.length > 0) {
|
|
47
47
|
return {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default options shared by the `wai-aria` rule and all `wai-aria-*` sub-rules.
|
|
3
|
+
*/
|
|
4
|
+
export const defaultOptions = {
|
|
5
|
+
checkingValue: true,
|
|
6
|
+
checkingDeprecatedRole: true,
|
|
7
|
+
checkingDeprecatedProps: true,
|
|
8
|
+
permittedAriaRoles: true,
|
|
9
|
+
checkingAllowedAccessibilityChildRoles: true,
|
|
10
|
+
checkingRequiredOwnedElements: true,
|
|
11
|
+
checkingRequiredAccessibilityParentRole: true,
|
|
12
|
+
checkingPresentationalChildren: false,
|
|
13
|
+
checkingInteractionInHidden: false,
|
|
14
|
+
disallowSetImplicitRole: true,
|
|
15
|
+
disallowSetImplicitProps: true,
|
|
16
|
+
disallowDefaultValue: false,
|
|
17
|
+
version: undefined,
|
|
18
|
+
};
|
package/lib/wai-aria/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRule, getAttrSpecs, getComputedRole, ariaSpecs, getSpec } from '@markuplint/ml-core';
|
|
2
2
|
import { ARIA_RECOMMENDED_VERSION } from '@markuplint/ml-spec';
|
|
3
3
|
import { Collection } from '../helpers.js';
|
|
4
|
+
import { defaultOptions } from './default-options.js';
|
|
4
5
|
import { checkingAbstractRole } from './checkings/abstract-role.js';
|
|
5
6
|
import { checkingDefaultValue } from './checkings/default-value.js';
|
|
6
7
|
import { checkingDeprecatedProps } from './checkings/deprecated-props.js';
|
|
@@ -30,21 +31,7 @@ import meta from './meta.js';
|
|
|
30
31
|
*/
|
|
31
32
|
export default createRule({
|
|
32
33
|
meta: meta,
|
|
33
|
-
defaultOptions
|
|
34
|
-
checkingValue: true,
|
|
35
|
-
checkingDeprecatedRole: true,
|
|
36
|
-
checkingDeprecatedProps: true,
|
|
37
|
-
permittedAriaRoles: true,
|
|
38
|
-
checkingAllowedAccessibilityChildRoles: true,
|
|
39
|
-
checkingRequiredOwnedElements: true,
|
|
40
|
-
checkingRequiredAccessibilityParentRole: true,
|
|
41
|
-
checkingPresentationalChildren: false,
|
|
42
|
-
checkingInteractionInHidden: false,
|
|
43
|
-
disallowSetImplicitRole: true,
|
|
44
|
-
disallowSetImplicitProps: true,
|
|
45
|
-
disallowDefaultValue: false,
|
|
46
|
-
version: undefined,
|
|
47
|
-
},
|
|
34
|
+
defaultOptions,
|
|
48
35
|
async verify({ document, report, t }) {
|
|
49
36
|
await document.walkOn('Element', el => {
|
|
50
37
|
const roleAttr = el.getAttributeNode('role');
|
|
@@ -80,10 +67,12 @@ export default createRule({
|
|
|
80
67
|
if (el.rule.options.checkingDeprecatedRole) {
|
|
81
68
|
report(checkingDeprecatedRole({ attr: roleAttr, role: computed.role }));
|
|
82
69
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
70
|
+
// If the explicit role duplicates the implicit role, surface only that
|
|
71
|
+
// (more specific) message. Otherwise fall through to the general
|
|
72
|
+
// permitted-roles check to avoid emitting two violations for the same
|
|
73
|
+
// offending token.
|
|
74
|
+
const implicitRoleViolated = el.rule.options.disallowSetImplicitRole && report(checkingImplicitRole({ attr: roleAttr }));
|
|
75
|
+
if (el.rule.options.permittedAriaRoles && !implicitRoleViolated) {
|
|
87
76
|
report(checkingPermittedRoles({ attr: roleAttr }));
|
|
88
77
|
}
|
|
89
78
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createRule, getSpec } from '@markuplint/ml-core';
|
|
2
|
+
import { checkingAbstractRole } from '../wai-aria/checkings/abstract-role.js';
|
|
3
|
+
import { defaultOptions } from '../wai-aria/default-options.js';
|
|
4
|
+
import meta from './meta.js';
|
|
5
|
+
/** Warns when an abstract WAI-ARIA role is used directly in content. */
|
|
6
|
+
export default createRule({
|
|
7
|
+
meta,
|
|
8
|
+
defaultOptions,
|
|
9
|
+
async verify({ document, report }) {
|
|
10
|
+
await document.walkOn('Element', el => {
|
|
11
|
+
const roleAttr = el.getAttributeNode('role');
|
|
12
|
+
if (!roleAttr)
|
|
13
|
+
return;
|
|
14
|
+
const elSpec = getSpec(el, document.specs.specs);
|
|
15
|
+
if (!elSpec)
|
|
16
|
+
return;
|
|
17
|
+
if (!elSpec.globalAttrs['#ARIAAttrs'])
|
|
18
|
+
return;
|
|
19
|
+
report(checkingAbstractRole({ attr: roleAttr }));
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createRule, ariaSpecs, getSpec } from '@markuplint/ml-core';
|
|
2
|
+
import { ARIA_RECOMMENDED_VERSION } from '@markuplint/ml-spec';
|
|
3
|
+
import { checkingDefaultValue } from '../wai-aria/checkings/default-value.js';
|
|
4
|
+
import { defaultOptions } from '../wai-aria/default-options.js';
|
|
5
|
+
import meta from './meta.js';
|
|
6
|
+
/** Warns when an ARIA property is explicitly set to its spec-defined default value. */
|
|
7
|
+
export default createRule({
|
|
8
|
+
meta,
|
|
9
|
+
defaultSeverity: 'warning',
|
|
10
|
+
defaultOptions,
|
|
11
|
+
async verify({ document, report }) {
|
|
12
|
+
await document.walkOn('Element', el => {
|
|
13
|
+
const elSpec = getSpec(el, document.specs.specs);
|
|
14
|
+
if (!elSpec)
|
|
15
|
+
return;
|
|
16
|
+
if (!elSpec.globalAttrs['#ARIAAttrs'])
|
|
17
|
+
return;
|
|
18
|
+
const propAttrs = el.attributes.filter(attr => /^aria-/i.test(attr.name));
|
|
19
|
+
if (propAttrs.length === 0)
|
|
20
|
+
return;
|
|
21
|
+
const ariaVersion = el.rule.options?.version ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
22
|
+
const { props: propSpecs } = ariaSpecs(document.specs, ariaVersion);
|
|
23
|
+
for (const attr of propAttrs) {
|
|
24
|
+
report(checkingDefaultValue({ attr, propSpecs }));
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createRule, getComputedRole, ariaSpecs, getSpec } from '@markuplint/ml-core';
|
|
2
|
+
import { ARIA_RECOMMENDED_VERSION } from '@markuplint/ml-spec';
|
|
3
|
+
import { checkingDeprecatedProps } from '../wai-aria/checkings/deprecated-props.js';
|
|
4
|
+
import { defaultOptions } from '../wai-aria/default-options.js';
|
|
5
|
+
import meta from './meta.js';
|
|
6
|
+
/** Warns when a deprecated ARIA property or state is used on a role. */
|
|
7
|
+
export default createRule({
|
|
8
|
+
meta,
|
|
9
|
+
defaultSeverity: 'warning',
|
|
10
|
+
defaultOptions,
|
|
11
|
+
async verify({ document, report }) {
|
|
12
|
+
await document.walkOn('Element', el => {
|
|
13
|
+
const elSpec = getSpec(el, document.specs.specs);
|
|
14
|
+
if (!elSpec)
|
|
15
|
+
return;
|
|
16
|
+
if (!elSpec.globalAttrs['#ARIAAttrs'])
|
|
17
|
+
return;
|
|
18
|
+
const propAttrs = el.attributes.filter(attr => /^aria-/i.test(attr.name));
|
|
19
|
+
if (propAttrs.length === 0)
|
|
20
|
+
return;
|
|
21
|
+
const ariaVersion = el.rule.options?.version ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
22
|
+
const computed = getComputedRole(document.specs, el, ariaVersion);
|
|
23
|
+
const { props: propSpecs } = ariaSpecs(document.specs, ariaVersion);
|
|
24
|
+
for (const attr of propAttrs) {
|
|
25
|
+
report(checkingDeprecatedProps({ attr, role: computed.role, propSpecs }));
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createRule, getComputedRole, getSpec } from '@markuplint/ml-core';
|
|
2
|
+
import { ARIA_RECOMMENDED_VERSION } from '@markuplint/ml-spec';
|
|
3
|
+
import { checkingDeprecatedRole } from '../wai-aria/checkings/deprecated-role.js';
|
|
4
|
+
import { defaultOptions } from '../wai-aria/default-options.js';
|
|
5
|
+
import meta from './meta.js';
|
|
6
|
+
/** Warns when a deprecated WAI-ARIA role is used. */
|
|
7
|
+
export default createRule({
|
|
8
|
+
meta,
|
|
9
|
+
defaultSeverity: 'warning',
|
|
10
|
+
defaultOptions,
|
|
11
|
+
async verify({ document, report }) {
|
|
12
|
+
await document.walkOn('Element', el => {
|
|
13
|
+
const roleAttr = el.getAttributeNode('role');
|
|
14
|
+
if (!roleAttr)
|
|
15
|
+
return;
|
|
16
|
+
const elSpec = getSpec(el, document.specs.specs);
|
|
17
|
+
if (!elSpec)
|
|
18
|
+
return;
|
|
19
|
+
if (!elSpec.globalAttrs['#ARIAAttrs'])
|
|
20
|
+
return;
|
|
21
|
+
const ariaVersion = el.rule.options?.version ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
22
|
+
const computed = getComputedRole(document.specs, el, ariaVersion);
|
|
23
|
+
report(checkingDeprecatedRole({ attr: roleAttr, role: computed.role }));
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createRule, getComputedRole, ariaSpecs, getSpec } from '@markuplint/ml-core';
|
|
2
|
+
import { ARIA_RECOMMENDED_VERSION } from '@markuplint/ml-spec';
|
|
3
|
+
import { checkingDisallowedProp } from '../wai-aria/checkings/disallowed-prop.js';
|
|
4
|
+
import { defaultOptions } from '../wai-aria/default-options.js';
|
|
5
|
+
import meta from './meta.js';
|
|
6
|
+
/** Warns when an ARIA property or state is not allowed on the element's computed role. */
|
|
7
|
+
export default createRule({
|
|
8
|
+
meta,
|
|
9
|
+
defaultOptions,
|
|
10
|
+
async verify({ document, report }) {
|
|
11
|
+
await document.walkOn('Element', el => {
|
|
12
|
+
const elSpec = getSpec(el, document.specs.specs);
|
|
13
|
+
if (!elSpec)
|
|
14
|
+
return;
|
|
15
|
+
if (!elSpec.globalAttrs['#ARIAAttrs'])
|
|
16
|
+
return;
|
|
17
|
+
const propAttrs = el.attributes.filter(attr => /^aria-/i.test(attr.name));
|
|
18
|
+
if (propAttrs.length === 0)
|
|
19
|
+
return;
|
|
20
|
+
const ariaVersion = el.rule.options?.version ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
21
|
+
const computed = getComputedRole(document.specs, el, ariaVersion);
|
|
22
|
+
const { props: propSpecs } = ariaSpecs(document.specs, ariaVersion);
|
|
23
|
+
for (const attr of propAttrs) {
|
|
24
|
+
report(checkingDisallowedProp({
|
|
25
|
+
attr,
|
|
26
|
+
role: computed.role,
|
|
27
|
+
propSpecs,
|
|
28
|
+
disallowSetImplicitProps: true,
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Options } from '../wai-aria/types.js';
|
|
2
|
+
/** Warns when an ARIA property duplicates or contradicts semantics provided by a native HTML attribute. */
|
|
3
|
+
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, Options>>;
|
|
4
|
+
export default _default;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createRule, ariaSpecs, getAttrSpecs, getSpec } from '@markuplint/ml-core';
|
|
2
|
+
import { ARIA_RECOMMENDED_VERSION } from '@markuplint/ml-spec';
|
|
3
|
+
import { checkingImplicitProps } from '../wai-aria/checkings/implicit-props.js';
|
|
4
|
+
import { defaultOptions } from '../wai-aria/default-options.js';
|
|
5
|
+
import meta from './meta.js';
|
|
6
|
+
/** Warns when an ARIA property duplicates or contradicts semantics provided by a native HTML attribute. */
|
|
7
|
+
export default createRule({
|
|
8
|
+
meta,
|
|
9
|
+
defaultSeverity: 'warning',
|
|
10
|
+
defaultOptions,
|
|
11
|
+
async verify({ document, report }) {
|
|
12
|
+
await document.walkOn('Element', el => {
|
|
13
|
+
const elSpec = getSpec(el, document.specs.specs);
|
|
14
|
+
if (!elSpec)
|
|
15
|
+
return;
|
|
16
|
+
if (!elSpec.globalAttrs['#ARIAAttrs'])
|
|
17
|
+
return;
|
|
18
|
+
const propAttrs = el.attributes.filter(attr => /^aria-/i.test(attr.name));
|
|
19
|
+
if (propAttrs.length === 0)
|
|
20
|
+
return;
|
|
21
|
+
const ariaVersion = el.rule.options?.version ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
22
|
+
const { props: propSpecs } = ariaSpecs(document.specs, ariaVersion);
|
|
23
|
+
const attrSpecs = getAttrSpecs(el, document.specs);
|
|
24
|
+
for (const attr of propAttrs) {
|
|
25
|
+
report(checkingImplicitProps({ attr, propSpecs, attrSpecs }));
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
});
|