@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.
Files changed (113) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/CLAUDE.md +27 -0
  3. package/SKILL.md +1 -0
  4. package/lib/attr-check.d.ts +3 -1
  5. package/lib/attr-check.js +10 -1
  6. package/lib/helpers.js +16 -1
  7. package/lib/index.d.ts +25 -7
  8. package/lib/index.js +36 -0
  9. package/lib/invalid-attr/index.js +12 -0
  10. package/lib/no-duplicate-autofocus/index.d.ts +7 -0
  11. package/lib/no-duplicate-autofocus/index.js +27 -0
  12. package/lib/no-duplicate-autofocus/meta.d.ts +4 -0
  13. package/lib/no-duplicate-autofocus/meta.js +3 -0
  14. package/lib/no-duplicate-visible-main/index.d.ts +8 -0
  15. package/lib/no-duplicate-visible-main/index.js +28 -0
  16. package/lib/no-duplicate-visible-main/meta.d.ts +4 -0
  17. package/lib/no-duplicate-visible-main/meta.js +3 -0
  18. package/lib/permitted-contents/choice.d.ts +5 -2
  19. package/lib/permitted-contents/choice.js +5 -2
  20. package/lib/permitted-contents/complex-branch.d.ts +5 -2
  21. package/lib/permitted-contents/complex-branch.js +6 -3
  22. package/lib/permitted-contents/content-model.d.ts +21 -2
  23. package/lib/permitted-contents/content-model.js +32 -6
  24. package/lib/permitted-contents/count-pattern.d.ts +5 -2
  25. package/lib/permitted-contents/count-pattern.js +13 -5
  26. package/lib/permitted-contents/index.d.ts +7 -1
  27. package/lib/permitted-contents/index.js +160 -65
  28. package/lib/permitted-contents/matches-selector.d.ts +2 -2
  29. package/lib/permitted-contents/matches-selector.js +43 -5
  30. package/lib/permitted-contents/order.d.ts +9 -2
  31. package/lib/permitted-contents/order.js +9 -2
  32. package/lib/permitted-contents/recursive-branch.d.ts +5 -2
  33. package/lib/permitted-contents/recursive-branch.js +7 -4
  34. package/lib/permitted-contents/represent-transparent-nodes.d.ts +2 -2
  35. package/lib/permitted-contents/represent-transparent-nodes.js +6 -6
  36. package/lib/permitted-contents/start.d.ts +2 -2
  37. package/lib/permitted-contents/start.js +3 -3
  38. package/lib/permitted-contents/types.d.ts +14 -0
  39. package/lib/permitted-contents/utils.d.ts +11 -2
  40. package/lib/permitted-contents/utils.js +62 -13
  41. package/lib/wai-aria/checkings/disallowed-prop.d.ts +2 -1
  42. package/lib/wai-aria/checkings/disallowed-prop.js +27 -5
  43. package/lib/wai-aria/checkings/permitted-roles.js +16 -3
  44. package/lib/wai-aria/checkings/required-accessibility-parent-role.js +1 -1
  45. package/lib/wai-aria/default-options.d.ts +5 -0
  46. package/lib/wai-aria/default-options.js +18 -0
  47. package/lib/wai-aria/index.js +8 -19
  48. package/lib/wai-aria-abstract-role/index.d.ts +4 -0
  49. package/lib/wai-aria-abstract-role/index.js +22 -0
  50. package/lib/wai-aria-abstract-role/meta.d.ts +4 -0
  51. package/lib/wai-aria-abstract-role/meta.js +3 -0
  52. package/lib/wai-aria-default-value/index.d.ts +4 -0
  53. package/lib/wai-aria-default-value/index.js +28 -0
  54. package/lib/wai-aria-default-value/meta.d.ts +5 -0
  55. package/lib/wai-aria-default-value/meta.js +4 -0
  56. package/lib/wai-aria-deprecated-props/index.d.ts +4 -0
  57. package/lib/wai-aria-deprecated-props/index.js +29 -0
  58. package/lib/wai-aria-deprecated-props/meta.d.ts +5 -0
  59. package/lib/wai-aria-deprecated-props/meta.js +4 -0
  60. package/lib/wai-aria-deprecated-role/index.d.ts +4 -0
  61. package/lib/wai-aria-deprecated-role/index.js +26 -0
  62. package/lib/wai-aria-deprecated-role/meta.d.ts +4 -0
  63. package/lib/wai-aria-deprecated-role/meta.js +3 -0
  64. package/lib/wai-aria-disallowed-props/index.d.ts +4 -0
  65. package/lib/wai-aria-disallowed-props/index.js +33 -0
  66. package/lib/wai-aria-disallowed-props/meta.d.ts +5 -0
  67. package/lib/wai-aria-disallowed-props/meta.js +4 -0
  68. package/lib/wai-aria-implicit-props/index.d.ts +4 -0
  69. package/lib/wai-aria-implicit-props/index.js +29 -0
  70. package/lib/wai-aria-implicit-props/meta.d.ts +5 -0
  71. package/lib/wai-aria-implicit-props/meta.js +4 -0
  72. package/lib/wai-aria-implicit-role/index.d.ts +4 -0
  73. package/lib/wai-aria-implicit-role/index.js +23 -0
  74. package/lib/wai-aria-implicit-role/meta.d.ts +4 -0
  75. package/lib/wai-aria-implicit-role/meta.js +3 -0
  76. package/lib/wai-aria-interaction-in-hidden/index.d.ts +4 -0
  77. package/lib/wai-aria-interaction-in-hidden/index.js +20 -0
  78. package/lib/wai-aria-interaction-in-hidden/meta.d.ts +5 -0
  79. package/lib/wai-aria-interaction-in-hidden/meta.js +4 -0
  80. package/lib/wai-aria-no-global-prop/index.d.ts +4 -0
  81. package/lib/wai-aria-no-global-prop/index.js +30 -0
  82. package/lib/wai-aria-no-global-prop/meta.d.ts +5 -0
  83. package/lib/wai-aria-no-global-prop/meta.js +4 -0
  84. package/lib/wai-aria-non-existent-role/index.d.ts +4 -0
  85. package/lib/wai-aria-non-existent-role/index.js +22 -0
  86. package/lib/wai-aria-non-existent-role/meta.d.ts +4 -0
  87. package/lib/wai-aria-non-existent-role/meta.js +3 -0
  88. package/lib/wai-aria-permitted-roles/index.d.ts +4 -0
  89. package/lib/wai-aria-permitted-roles/index.js +22 -0
  90. package/lib/wai-aria-permitted-roles/meta.d.ts +4 -0
  91. package/lib/wai-aria-permitted-roles/meta.js +3 -0
  92. package/lib/wai-aria-presentational-children/index.d.ts +4 -0
  93. package/lib/wai-aria-presentational-children/index.js +20 -0
  94. package/lib/wai-aria-presentational-children/meta.d.ts +5 -0
  95. package/lib/wai-aria-presentational-children/meta.js +4 -0
  96. package/lib/wai-aria-required-owned-elements/index.d.ts +4 -0
  97. package/lib/wai-aria-required-owned-elements/index.js +23 -0
  98. package/lib/wai-aria-required-owned-elements/meta.d.ts +4 -0
  99. package/lib/wai-aria-required-owned-elements/meta.js +3 -0
  100. package/lib/wai-aria-required-parent-role/index.d.ts +4 -0
  101. package/lib/wai-aria-required-parent-role/index.js +25 -0
  102. package/lib/wai-aria-required-parent-role/meta.d.ts +4 -0
  103. package/lib/wai-aria-required-parent-role/meta.js +3 -0
  104. package/lib/wai-aria-required-props/index.d.ts +4 -0
  105. package/lib/wai-aria-required-props/index.js +26 -0
  106. package/lib/wai-aria-required-props/meta.d.ts +4 -0
  107. package/lib/wai-aria-required-props/meta.js +3 -0
  108. package/lib/wai-aria-value/index.d.ts +4 -0
  109. package/lib/wai-aria-value/index.js +28 -0
  110. package/lib/wai-aria-value/meta.d.ts +5 -0
  111. package/lib/wai-aria-value/meta.js +4 -0
  112. package/package.json +15 -11
  113. 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
- const selectorResult = createSelector(selector, specs).search(node);
43
- const matched = selectorResult.filter((r) => r.matched);
44
- if (matched.length > 0) {
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: true,
68
+ matched: false,
69
+ not,
47
70
  };
48
71
  }
49
- const not = selectorResult
50
- .flatMap(r => (r.matched ? [] : (r.not ?? [])))
51
- .flatMap(descendants)
52
- .shift();
53
- return {
54
- matched: false,
55
- not,
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: attr,
21
- 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'),
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 spec-generator changes the field name in the future.
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,5 @@
1
+ import type { Options } from './types.js';
2
+ /**
3
+ * Default options shared by the `wai-aria` rule and all `wai-aria-*` sub-rules.
4
+ */
5
+ export declare const defaultOptions: Options;
@@ -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
+ };
@@ -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
- if (el.rule.options.disallowSetImplicitRole) {
84
- report(checkingImplicitRole({ attr: roleAttr }));
85
- }
86
- if (el.rule.options.permittedAriaRoles) {
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,4 @@
1
+ import type { Options } from '../wai-aria/types.js';
2
+ /** Warns when an abstract WAI-ARIA role is used directly in content. */
3
+ declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, Options>>;
4
+ export default _default;
@@ -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,4 @@
1
+ declare const _default: {
2
+ readonly category: "a11y";
3
+ };
4
+ export default _default;
@@ -0,0 +1,3 @@
1
+ export default {
2
+ category: 'a11y',
3
+ };
@@ -0,0 +1,4 @@
1
+ import type { Options } from '../wai-aria/types.js';
2
+ /** Warns when an ARIA property is explicitly set to its spec-defined default value. */
3
+ declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, Options>>;
4
+ export default _default;
@@ -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,5 @@
1
+ /** Rule metadata for the `wai-aria-default-value` rule, categorized under accessibility. */
2
+ declare const _default: {
3
+ readonly category: "a11y";
4
+ };
5
+ export default _default;
@@ -0,0 +1,4 @@
1
+ /** Rule metadata for the `wai-aria-default-value` rule, categorized under accessibility. */
2
+ export default {
3
+ category: 'a11y',
4
+ };
@@ -0,0 +1,4 @@
1
+ import type { Options } from '../wai-aria/types.js';
2
+ /** Warns when a deprecated ARIA property or state is used on a role. */
3
+ declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, Options>>;
4
+ export default _default;
@@ -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,5 @@
1
+ /** Rule metadata for the `wai-aria-deprecated-props` rule, categorized under accessibility. */
2
+ declare const _default: {
3
+ readonly category: "a11y";
4
+ };
5
+ export default _default;
@@ -0,0 +1,4 @@
1
+ /** Rule metadata for the `wai-aria-deprecated-props` rule, categorized under accessibility. */
2
+ export default {
3
+ category: 'a11y',
4
+ };
@@ -0,0 +1,4 @@
1
+ import type { Options } from '../wai-aria/types.js';
2
+ /** Warns when a deprecated WAI-ARIA role is used. */
3
+ declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, Options>>;
4
+ export default _default;
@@ -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,4 @@
1
+ declare const _default: {
2
+ readonly category: "a11y";
3
+ };
4
+ export default _default;
@@ -0,0 +1,3 @@
1
+ export default {
2
+ category: 'a11y',
3
+ };
@@ -0,0 +1,4 @@
1
+ import type { Options } from '../wai-aria/types.js';
2
+ /** Warns when an ARIA property or state is not allowed on the element's computed role. */
3
+ declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, Options>>;
4
+ export default _default;
@@ -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,5 @@
1
+ /** Rule metadata for the `wai-aria-disallowed-props` rule, categorized under accessibility. */
2
+ declare const _default: {
3
+ readonly category: "a11y";
4
+ };
5
+ export default _default;
@@ -0,0 +1,4 @@
1
+ /** Rule metadata for the `wai-aria-disallowed-props` rule, categorized under accessibility. */
2
+ export default {
3
+ category: 'a11y',
4
+ };
@@ -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
+ });