@markuplint/rules 4.12.0 → 5.0.0-alpha.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 (83) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/lib/attr-check.d.ts +5 -2
  3. package/lib/attr-check.js +13 -3
  4. package/lib/character-reference/index.d.ts +2 -1
  5. package/lib/character-reference/index.js +5 -1
  6. package/lib/correct-aspect-ratio/index.d.ts +5 -0
  7. package/lib/correct-aspect-ratio/index.js +80 -0
  8. package/lib/correct-aspect-ratio/meta.d.ts +4 -0
  9. package/lib/correct-aspect-ratio/meta.js +3 -0
  10. package/lib/correct-aspect-ratio/resolve-image-size.d.ts +24 -0
  11. package/lib/correct-aspect-ratio/resolve-image-size.js +165 -0
  12. package/lib/create-message.d.ts +2 -2
  13. package/lib/create-message.js +4 -1
  14. package/lib/deprecated-element/index.d.ts +2 -4
  15. package/lib/deprecated-element/index.js +4 -6
  16. package/lib/end-tag/index.js +1 -1
  17. package/lib/heading-levels/index.js +0 -1
  18. package/lib/helpers.d.ts +1 -0
  19. package/lib/helpers.js +8 -15
  20. package/lib/index.d.ts +30 -41
  21. package/lib/index.js +12 -0
  22. package/lib/invalid-attr/index.d.ts +12 -28
  23. package/lib/invalid-attr/index.js +50 -116
  24. package/lib/link-types/index.d.ts +10 -0
  25. package/lib/link-types/index.js +180 -0
  26. package/lib/link-types/meta.d.ts +5 -0
  27. package/lib/link-types/meta.js +4 -0
  28. package/lib/neighbor-popovers/index.js +3 -5
  29. package/lib/no-orphaned-end-tag/index.d.ts +2 -2
  30. package/lib/no-orphaned-end-tag/index.js +3 -4
  31. package/lib/no-refer-to-non-existent-id/index.d.ts +1 -1
  32. package/lib/no-refer-to-non-existent-id/index.js +2 -2
  33. package/lib/no-unsupported-features/compat-data.d.ts +71 -0
  34. package/lib/no-unsupported-features/compat-data.js +204 -0
  35. package/lib/no-unsupported-features/index.d.ts +24 -0
  36. package/lib/no-unsupported-features/index.js +139 -0
  37. package/lib/no-unsupported-features/meta.d.ts +5 -0
  38. package/lib/no-unsupported-features/meta.js +4 -0
  39. package/lib/no-unsupported-features/resolve-browsers.d.ts +30 -0
  40. package/lib/no-unsupported-features/resolve-browsers.js +125 -0
  41. package/lib/permitted-contents/choice.js +1 -1
  42. package/lib/permitted-contents/count-pattern.js +1 -1
  43. package/lib/permitted-contents/order.js +1 -1
  44. package/lib/permitted-contents/represent-transparent-nodes.d.ts +26 -1
  45. package/lib/permitted-contents/represent-transparent-nodes.js +96 -13
  46. package/lib/permitted-contents/utils.d.ts +1 -1
  47. package/lib/permitted-contents/utils.js +22 -35
  48. package/lib/redundant-accessible-name/index.d.ts +10 -0
  49. package/lib/redundant-accessible-name/index.js +123 -0
  50. package/lib/redundant-accessible-name/meta.d.ts +5 -0
  51. package/lib/redundant-accessible-name/meta.js +4 -0
  52. package/lib/require-accessible-name/index.d.ts +3 -8
  53. package/lib/require-accessible-name/index.js +6 -5
  54. package/lib/require-dialog-autofocus/index.d.ts +22 -0
  55. package/lib/require-dialog-autofocus/index.js +62 -0
  56. package/lib/require-dialog-autofocus/meta.d.ts +5 -0
  57. package/lib/require-dialog-autofocus/meta.js +4 -0
  58. package/lib/required-attr/index.d.ts +8 -1
  59. package/lib/required-attr/index.js +27 -4
  60. package/lib/required-element/index.js +1 -1
  61. package/lib/srcset-sizes-constraint/index.d.ts +6 -0
  62. package/lib/srcset-sizes-constraint/index.js +108 -0
  63. package/lib/srcset-sizes-constraint/meta.d.ts +5 -0
  64. package/lib/srcset-sizes-constraint/meta.js +4 -0
  65. package/lib/srcset-sizes-constraint/parse-srcset.d.ts +40 -0
  66. package/lib/srcset-sizes-constraint/parse-srcset.js +56 -0
  67. package/lib/table-row-column-alignment/grid.js +22 -29
  68. package/lib/wai-aria/checkings/abstract-role.js +5 -2
  69. package/lib/wai-aria/checkings/disallowed-prop.js +5 -2
  70. package/lib/wai-aria/checkings/implicit-role.js +5 -2
  71. package/lib/wai-aria/checkings/non-existent-role.d.ts +1 -0
  72. package/lib/wai-aria/checkings/non-existent-role.js +9 -2
  73. package/lib/wai-aria/checkings/permitted-roles.js +5 -2
  74. package/lib/wai-aria/checkings/presentational-children.js +3 -2
  75. package/lib/wai-aria/checkings/required-accessibility-parent-role.d.ts +25 -0
  76. package/lib/wai-aria/checkings/required-accessibility-parent-role.js +56 -0
  77. package/lib/wai-aria/checkings/required-owned-elements.d.ts +5 -4
  78. package/lib/wai-aria/checkings/required-owned-elements.js +57 -28
  79. package/lib/wai-aria/checkings/required-prop.js +5 -2
  80. package/lib/wai-aria/index.js +12 -4
  81. package/lib/wai-aria/types.d.ts +9 -2
  82. package/package.json +15 -10
  83. package/schema.json +18 -0
@@ -0,0 +1,108 @@
1
+ import { createRule } from '@markuplint/ml-core';
2
+ import meta from './meta.js';
3
+ import { hasSizesAuto, parseSrcset } from './parse-srcset.js';
4
+ /**
5
+ * Rule that enforces WHATWG constraints between the `srcset`, `sizes`,
6
+ * and `loading` attributes on `<img>` and `<source>` elements.
7
+ */
8
+ export default createRule({
9
+ meta: meta,
10
+ async verify({ document, report }) {
11
+ await document.walkOn('Element', el => {
12
+ const localName = el.localName;
13
+ // Only check img and source (inside picture) elements
14
+ if (localName !== 'img' && localName !== 'source') {
15
+ return;
16
+ }
17
+ // source is only relevant inside <picture>
18
+ if (localName === 'source' && el.parentElement?.localName !== 'picture') {
19
+ return;
20
+ }
21
+ // Skip if element has spread attributes (dynamic props)
22
+ if (el.hasSpreadAttr) {
23
+ return;
24
+ }
25
+ const srcsetAttr = el.getAttributeNode('srcset');
26
+ if (!srcsetAttr) {
27
+ return;
28
+ }
29
+ const srcsetIsDynamic = srcsetAttr.isDynamicValue;
30
+ const sizesAttr = el.getAttributeNode('sizes');
31
+ const sizesValue = sizesAttr?.value ?? null;
32
+ const sizesIsDynamic = sizesAttr?.isDynamicValue;
33
+ // Parse srcset unless dynamic
34
+ const parsed = srcsetIsDynamic ? null : parseSrcset(srcsetAttr.value);
35
+ // Check 2: width and density descriptors must not be mixed
36
+ if (parsed && parsed.hasWidth && (parsed.hasDensity || parsed.hasImplied)) {
37
+ report({
38
+ scope: el,
39
+ line: srcsetAttr.valueNode?.startLine,
40
+ col: srcsetAttr.valueNode?.startCol,
41
+ raw: srcsetAttr.valueNode?.raw,
42
+ message: 'The "srcset" attribute must not mix width and pixel density descriptors',
43
+ });
44
+ }
45
+ // Check 1: sizes present → srcset must use width descriptors
46
+ if (sizesAttr && !sizesIsDynamic && parsed && !parsed.hasWidth) {
47
+ report({
48
+ scope: el,
49
+ line: srcsetAttr.valueNode?.startLine,
50
+ col: srcsetAttr.valueNode?.startCol,
51
+ raw: srcsetAttr.valueNode?.raw,
52
+ message: 'The "srcset" attribute requires width descriptors when the "sizes" attribute is present',
53
+ });
54
+ }
55
+ // Check 3: img[sizes=auto] → loading=lazy required
56
+ if (localName === 'img' && sizesValue != null && !sizesIsDynamic && hasSizesAuto(sizesValue)) {
57
+ const loading = el.getAttribute('loading');
58
+ if (loading !== 'lazy') {
59
+ report({
60
+ scope: el,
61
+ line: sizesAttr.valueNode?.startLine,
62
+ col: sizesAttr.valueNode?.startCol,
63
+ raw: sizesAttr.valueNode?.raw,
64
+ message: 'The "sizes" attribute with "auto" requires the "loading" attribute to be "lazy"',
65
+ });
66
+ }
67
+ }
68
+ // Check 4: source[sizes=auto] → following sibling img must have loading=lazy
69
+ if (localName === 'source' && sizesValue != null && !sizesIsDynamic && hasSizesAuto(sizesValue)) {
70
+ const img = findFollowingImg(el);
71
+ if (!img || img.getAttribute('loading') !== 'lazy') {
72
+ report({
73
+ scope: el,
74
+ line: sizesAttr.valueNode?.startLine,
75
+ col: sizesAttr.valueNode?.startCol,
76
+ raw: sizesAttr.valueNode?.raw,
77
+ message: 'The "source" element with sizes="auto" requires the following sibling "img" element to have loading="lazy"',
78
+ });
79
+ }
80
+ }
81
+ // Check 5: img with w descriptors → sizes required
82
+ if (localName === 'img' && parsed && parsed.hasWidth && !sizesAttr) {
83
+ report({
84
+ scope: el,
85
+ message: 'The "sizes" attribute is required when the "srcset" attribute uses width descriptors',
86
+ });
87
+ }
88
+ });
89
+ },
90
+ });
91
+ /**
92
+ * Find the first following sibling `<img>` element.
93
+ * Per the spec, the img does not have to be the immediately next sibling.
94
+ *
95
+ * @param el - The starting element to search from
96
+ * @returns The first following sibling `<img>` element, or `null` if none found
97
+ */
98
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
99
+ function findFollowingImg(el) {
100
+ let sibling = el.nextElementSibling;
101
+ while (sibling != null) {
102
+ if (sibling.localName === 'img') {
103
+ return sibling;
104
+ }
105
+ sibling = sibling.nextElementSibling;
106
+ }
107
+ return null;
108
+ }
@@ -0,0 +1,5 @@
1
+ /** Rule metadata for `srcset-sizes-constraint`: categorized as a validation rule. */
2
+ declare const _default: {
3
+ readonly category: "validation";
4
+ };
5
+ export default _default;
@@ -0,0 +1,4 @@
1
+ /** Rule metadata for `srcset-sizes-constraint`: categorized as a validation rule. */
2
+ export default {
3
+ category: 'validation',
4
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Descriptor type for an image candidate in a srcset attribute.
3
+ * - `width`: ends with `w` (e.g., `480w`)
4
+ * - `density`: ends with `x` (e.g., `2x`)
5
+ * - `none`: no descriptor (implies `1x`)
6
+ */
7
+ type DescriptorType = 'width' | 'density' | 'none';
8
+ type SrcsetCandidate = {
9
+ readonly url: string;
10
+ readonly descriptorType: DescriptorType;
11
+ };
12
+ type ParseResult = {
13
+ readonly candidates: readonly SrcsetCandidate[];
14
+ /** At least one candidate has a width descriptor (`w`). */
15
+ readonly hasWidth: boolean;
16
+ /** At least one candidate has a pixel density descriptor (`x`). */
17
+ readonly hasDensity: boolean;
18
+ /** At least one candidate has no descriptor (implied `1x`). */
19
+ readonly hasImplied: boolean;
20
+ };
21
+ /**
22
+ * Parse a `srcset` attribute value into its image candidates.
23
+ *
24
+ * @param value - The raw `srcset` attribute value string
25
+ * @returns Parsed result with candidates and descriptor flags
26
+ * @see https://html.spec.whatwg.org/multipage/images.html#srcset-attributes
27
+ */
28
+ export declare function parseSrcset(value: string): ParseResult;
29
+ /**
30
+ * Check whether a `sizes` attribute value starts with `auto`.
31
+ *
32
+ * Per the spec, `auto` is only valid as the first entry in a sizes list.
33
+ * Examples: `"auto"`, `"auto, 100vw"`, `"AUTO"`.
34
+ *
35
+ * @param value - The raw `sizes` attribute value string
36
+ * @returns `true` if the value starts with `auto` (case-insensitive)
37
+ * @see https://html.spec.whatwg.org/multipage/embedded-content.html#the-img-element
38
+ */
39
+ export declare function hasSizesAuto(value: string): boolean;
40
+ export {};
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Parse a `srcset` attribute value into its image candidates.
3
+ *
4
+ * @param value - The raw `srcset` attribute value string
5
+ * @returns Parsed result with candidates and descriptor flags
6
+ * @see https://html.spec.whatwg.org/multipage/images.html#srcset-attributes
7
+ */
8
+ export function parseSrcset(value) {
9
+ const raw = value.trim();
10
+ if (raw === '') {
11
+ return { candidates: [], hasWidth: false, hasDensity: false, hasImplied: false };
12
+ }
13
+ const candidates = [];
14
+ let hasWidth = false;
15
+ let hasDensity = false;
16
+ let hasImplied = false;
17
+ for (const segment of raw.split(',')) {
18
+ const tokens = segment.trim().split(/\s+/);
19
+ const url = tokens[0];
20
+ if (!url) {
21
+ continue;
22
+ }
23
+ const descriptor = tokens[1];
24
+ let descriptorType = 'none';
25
+ if (descriptor) {
26
+ if (/^[1-9]\d*w$/.test(descriptor)) {
27
+ descriptorType = 'width';
28
+ hasWidth = true;
29
+ }
30
+ else if (/^\d+(?:\.\d+)?x$/.test(descriptor)) {
31
+ descriptorType = 'density';
32
+ hasDensity = true;
33
+ }
34
+ // Invalid descriptors are ignored here; type validation handles them.
35
+ }
36
+ else {
37
+ hasImplied = true;
38
+ }
39
+ candidates.push({ url, descriptorType });
40
+ }
41
+ return { candidates, hasWidth, hasDensity, hasImplied };
42
+ }
43
+ /**
44
+ * Check whether a `sizes` attribute value starts with `auto`.
45
+ *
46
+ * Per the spec, `auto` is only valid as the first entry in a sizes list.
47
+ * Examples: `"auto"`, `"auto, 100vw"`, `"AUTO"`.
48
+ *
49
+ * @param value - The raw `sizes` attribute value string
50
+ * @returns `true` if the value starts with `auto` (case-insensitive)
51
+ * @see https://html.spec.whatwg.org/multipage/embedded-content.html#the-img-element
52
+ */
53
+ export function hasSizesAuto(value) {
54
+ const v = value.trim().toLowerCase();
55
+ return v === 'auto' || /^auto[\s,]/.test(v);
56
+ }
@@ -1,15 +1,3 @@
1
- var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
- if (kind === "m") throw new TypeError("Private method is not writable");
3
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
- return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
- };
7
- var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
- };
12
- var _Grid_tbodyRowElements, _Grid_tfootRowElements, _Grid_theadRowElements;
13
1
  import { findChildren } from './find-children.js';
14
2
  /**
15
3
  * Represents a table as a grid model, splitting it into thead, tbody, and tfoot sections.
@@ -19,6 +7,18 @@ import { findChildren } from './find-children.js';
19
7
  * rule to detect misaligned rows and overlapping cells.
20
8
  */
21
9
  export class Grid {
10
+ /** The grid model for the `<tbody>` section. */
11
+ tbodyGrid;
12
+ /** The grid model for the `<tfoot>` section. */
13
+ tfootGrid;
14
+ /** The grid model for the `<thead>` section. */
15
+ theadGrid;
16
+ /** The `<tr>` elements within `<tbody>`. */
17
+ #tbodyRowElements;
18
+ /** The `<tr>` elements within `<tfoot>`. */
19
+ #tfootRowElements;
20
+ /** The `<tr>` elements within `<thead>`. */
21
+ #theadRowElements;
22
22
  /**
23
23
  * Constructs a grid model from a `<table>` element.
24
24
  *
@@ -27,21 +27,15 @@ export class Grid {
27
27
  constructor(
28
28
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
29
29
  table) {
30
- /** The `<tr>` elements within `<tbody>`. */
31
- _Grid_tbodyRowElements.set(this, void 0);
32
- /** The `<tr>` elements within `<tfoot>`. */
33
- _Grid_tfootRowElements.set(this, void 0);
34
- /** The `<tr>` elements within `<thead>`. */
35
- _Grid_theadRowElements.set(this, void 0);
36
30
  const thead = findChildren(table, 'thead')[0];
37
31
  const tbody = findChildren(table, 'tbody')[0];
38
32
  const tfoot = findChildren(table, 'tfoot')[0];
39
- __classPrivateFieldSet(this, _Grid_theadRowElements, thead ? findChildren(thead, 'tr') : [], "f");
40
- __classPrivateFieldSet(this, _Grid_tbodyRowElements, tbody ? findChildren(tbody, 'tr') : [], "f");
41
- __classPrivateFieldSet(this, _Grid_tfootRowElements, tfoot ? findChildren(tfoot, 'tr') : [], "f");
42
- this.theadGrid = createGrid(__classPrivateFieldGet(this, _Grid_theadRowElements, "f"));
43
- this.tbodyGrid = createGrid(__classPrivateFieldGet(this, _Grid_tbodyRowElements, "f"));
44
- this.tfootGrid = createGrid(__classPrivateFieldGet(this, _Grid_tfootRowElements, "f"));
33
+ this.#theadRowElements = thead ? findChildren(thead, 'tr') : [];
34
+ this.#tbodyRowElements = tbody ? findChildren(tbody, 'tr') : [];
35
+ this.#tfootRowElements = tfoot ? findChildren(tfoot, 'tr') : [];
36
+ this.theadGrid = createGrid(this.#theadRowElements);
37
+ this.tbodyGrid = createGrid(this.#tbodyRowElements);
38
+ this.tfootGrid = createGrid(this.#tfootRowElements);
45
39
  }
46
40
  /**
47
41
  * Returns all `<tr>` elements across all table sections in document order.
@@ -49,7 +43,7 @@ export class Grid {
49
43
  * @returns Combined array of row elements from thead, tbody, and tfoot.
50
44
  */
51
45
  getAllRowElements() {
52
- return [...__classPrivateFieldGet(this, _Grid_theadRowElements, "f"), ...__classPrivateFieldGet(this, _Grid_tbodyRowElements, "f"), ...__classPrivateFieldGet(this, _Grid_tfootRowElements, "f")];
46
+ return [...this.#theadRowElements, ...this.#tbodyRowElements, ...this.#tfootRowElements];
53
47
  }
54
48
  /**
55
49
  * Returns all grid rows that contain at least one actual cell element.
@@ -88,15 +82,15 @@ export class Grid {
88
82
  return [
89
83
  {
90
84
  section: this.theadGrid,
91
- elements: __classPrivateFieldGet(this, _Grid_theadRowElements, "f"),
85
+ elements: this.#theadRowElements,
92
86
  },
93
87
  {
94
88
  section: this.tbodyGrid,
95
- elements: __classPrivateFieldGet(this, _Grid_tbodyRowElements, "f"),
89
+ elements: this.#tbodyRowElements,
96
90
  },
97
91
  {
98
92
  section: this.tfootGrid,
99
- elements: __classPrivateFieldGet(this, _Grid_tfootRowElements, "f"),
93
+ elements: this.#tfootRowElements,
100
94
  },
101
95
  ];
102
96
  }
@@ -120,7 +114,6 @@ export class Grid {
120
114
  console.table(this.tfootGrid);
121
115
  }
122
116
  }
123
- _Grid_tbodyRowElements = new WeakMap(), _Grid_tfootRowElements = new WeakMap(), _Grid_theadRowElements = new WeakMap();
124
117
  /**
125
118
  * Detects a rowspan that extends beyond the available rows in a table section.
126
119
  *
@@ -1,4 +1,4 @@
1
- import { ariaSpecs } from '@markuplint/ml-spec';
1
+ import { ARIA_RECOMMENDED_VERSION, ariaSpecs } from '@markuplint/ml-spec';
2
2
  /**
3
3
  * Checks whether the `role` attribute value refers to an abstract WAI-ARIA role.
4
4
  *
@@ -9,7 +9,10 @@ import { ariaSpecs } from '@markuplint/ml-spec';
9
9
  * @returns A violation if any token in the role attribute is an abstract role.
10
10
  */
11
11
  export const checkingAbstractRole = ({ attr }) => t => {
12
- const { roles } = ariaSpecs(attr.ownerMLDocument.specs, attr.rule.options.version);
12
+ const ariaVersion = attr.rule.options?.version ??
13
+ attr.ownerMLDocument.ruleCommonSettings?.ariaVersion ??
14
+ ARIA_RECOMMENDED_VERSION;
15
+ const { roles } = ariaSpecs(attr.ownerMLDocument.specs, ariaVersion);
13
16
  const tokens = attr.tokenList?.allTokens();
14
17
  if (!tokens) {
15
18
  return;
@@ -1,4 +1,4 @@
1
- import { getARIA } from '@markuplint/ml-spec';
1
+ import { ARIA_RECOMMENDED_VERSION, getARIA } from '@markuplint/ml-spec';
2
2
  /**
3
3
  * Checks whether an ARIA property or state is disallowed on the element's computed role.
4
4
  *
@@ -20,9 +20,12 @@ export const checkingDisallowedProp = ({ attr, role, propSpecs, disallowSetImpli
20
20
  if (!/^aria-/i.test(attr.name)) {
21
21
  return;
22
22
  }
23
+ const ariaVersion = attr.rule.options?.version ??
24
+ attr.ownerMLDocument.ruleCommonSettings?.ariaVersion ??
25
+ ARIA_RECOMMENDED_VERSION;
23
26
  const statesAndProp = role.ownedProperties.find(p => p.name === attr.name);
24
27
  const propSpec = propSpecs.find(p => p.name === attr.name);
25
- const elAriaSpec = getARIA(attr.ownerMLDocument.specs, attr.ownerElement.localName, attr.ownerElement.namespaceURI, attr.rule.options.version, attr.ownerElement.matches.bind(attr.ownerElement));
28
+ const elAriaSpec = getARIA(attr.ownerMLDocument.specs, attr.ownerElement.localName, attr.ownerElement.namespaceURI, ariaVersion, attr.ownerElement.matches.bind(attr.ownerElement));
26
29
  if (disallowSetImplicitProps && elAriaSpec?.properties !== false && elAriaSpec?.properties?.without) {
27
30
  for (const ignore of elAriaSpec.properties.without) {
28
31
  if (ignore.name === attr.name) {
@@ -1,4 +1,4 @@
1
- import { getImplicitRoleName } from '@markuplint/ml-spec';
1
+ import { ARIA_RECOMMENDED_VERSION, getImplicitRoleName } from '@markuplint/ml-spec';
2
2
  /**
3
3
  * Checks whether the explicit `role` attribute duplicates the element's implicit (native) role.
4
4
  *
@@ -13,7 +13,10 @@ export const checkingImplicitRole = ({ attr }) => t => {
13
13
  if (!tokens) {
14
14
  return;
15
15
  }
16
- const implicitRole = getImplicitRoleName(attr.ownerElement, attr.rule.options.version, attr.ownerMLDocument.specs);
16
+ const ariaVersion = attr.rule.options?.version ??
17
+ attr.ownerMLDocument.ruleCommonSettings?.ariaVersion ??
18
+ ARIA_RECOMMENDED_VERSION;
19
+ const implicitRole = getImplicitRoleName(attr.ownerElement, ariaVersion, attr.ownerMLDocument.specs);
17
20
  for (const token of tokens) {
18
21
  if (implicitRole === token.raw) {
19
22
  return {
@@ -6,6 +6,7 @@ import type { AttrChecker } from '@markuplint/ml-core';
6
6
  *
7
7
  * Validates each token in the role attribute against the known ARIA roles list.
8
8
  * For SVG elements, the WAI-ARIA Graphics Module roles are also accepted.
9
+ * DPub ARIA roles (Digital Publishing WAI-ARIA Module) are accepted for all elements.
9
10
  *
10
11
  * @param attr - The `role` attribute node to inspect.
11
12
  * @returns A violation if any token does not correspond to a defined ARIA role.
@@ -1,16 +1,20 @@
1
- import { ariaSpecs } from '@markuplint/ml-spec';
1
+ import { ARIA_RECOMMENDED_VERSION, ariaSpecs } from '@markuplint/ml-spec';
2
2
  /**
3
3
  * Checks whether the `role` attribute value refers to a role that does not exist
4
4
  * in the WAI-ARIA specification.
5
5
  *
6
6
  * Validates each token in the role attribute against the known ARIA roles list.
7
7
  * For SVG elements, the WAI-ARIA Graphics Module roles are also accepted.
8
+ * DPub ARIA roles (Digital Publishing WAI-ARIA Module) are accepted for all elements.
8
9
  *
9
10
  * @param attr - The `role` attribute node to inspect.
10
11
  * @returns A violation if any token does not correspond to a defined ARIA role.
11
12
  */
12
13
  export const checkingNonExistentRole = ({ attr }) => t => {
13
- const { roles, graphicsRoles } = ariaSpecs(attr.ownerMLDocument.specs, attr.rule.options.version);
14
+ const ariaVersion = attr.rule.options?.version ??
15
+ attr.ownerMLDocument.ruleCommonSettings?.ariaVersion ??
16
+ ARIA_RECOMMENDED_VERSION;
17
+ const { roles, graphicsRoles, dpubRoles } = ariaSpecs(attr.ownerMLDocument.specs, ariaVersion);
14
18
  const tokens = attr.tokenList?.allTokens();
15
19
  if (!tokens) {
16
20
  return;
@@ -20,6 +24,9 @@ export const checkingNonExistentRole = ({ attr }) => t => {
20
24
  if (!role && attr.ownerElement.namespaceURI === 'http://www.w3.org/2000/svg') {
21
25
  role = graphicsRoles.find(r => r.name === token.raw);
22
26
  }
27
+ if (!role) {
28
+ role = dpubRoles.find(r => r.name === token.raw);
29
+ }
23
30
  if (!role) {
24
31
  return {
25
32
  scope: token,
@@ -1,4 +1,4 @@
1
- import { getPermittedRoles } from '@markuplint/ml-spec';
1
+ import { ARIA_RECOMMENDED_VERSION, getPermittedRoles } from '@markuplint/ml-spec';
2
2
  /**
3
3
  * Checks whether the explicit `role` attribute value is permitted on the element
4
4
  * according to the ARIA in HTML specification.
@@ -10,8 +10,11 @@ import { getPermittedRoles } from '@markuplint/ml-spec';
10
10
  * @returns A violation if the role is not in the element's list of permitted roles.
11
11
  */
12
12
  export const checkingPermittedRoles = ({ attr }) => t => {
13
+ const ariaVersion = attr.rule.options?.version ??
14
+ attr.ownerMLDocument.ruleCommonSettings?.ariaVersion ??
15
+ ARIA_RECOMMENDED_VERSION;
13
16
  const el = attr.ownerElement;
14
- const permittedRoles = getPermittedRoles(el, el.rule.options.version, attr.ownerMLDocument.specs);
17
+ const permittedRoles = getPermittedRoles(el, ariaVersion, attr.ownerMLDocument.specs);
15
18
  if (permittedRoles.length === 0) {
16
19
  return {
17
20
  scope: attr,
@@ -1,4 +1,4 @@
1
- import { getComputedRole } from '@markuplint/ml-spec';
1
+ import { ARIA_RECOMMENDED_VERSION, getComputedRole } from '@markuplint/ml-spec';
2
2
  /**
3
3
  * Checks whether ARIA attributes are applied to descendants of an element whose
4
4
  * role has the `childrenArePresentational` characteristic.
@@ -58,9 +58,10 @@ export const checkingPresentationalChildren = ({ el }) => t => {
58
58
  function getAncestorHasPresentationalChildren(
59
59
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
60
60
  el) {
61
+ const ariaVersion = el.rule.options?.version ?? el.ownerMLDocument.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
61
62
  let current = el.parentElement;
62
63
  while (current) {
63
- const computed = getComputedRole(el.ownerMLDocument.specs, current, el.rule.options.version);
64
+ const computed = getComputedRole(el.ownerMLDocument.specs, current, ariaVersion);
64
65
  if (computed.role?.childrenPresentational) {
65
66
  return computed;
66
67
  }
@@ -0,0 +1,25 @@
1
+ import type { Options } from '../types.js';
2
+ import type { ElementChecker } from '@markuplint/ml-core';
3
+ import type { ComputedRole } from '@markuplint/ml-spec';
4
+ /**
5
+ * Checks whether an element with an explicit role satisfies its
6
+ * "Required Accessibility Parent Role" (called "Required Context Role" in ARIA 1.2).
7
+ *
8
+ * When `getComputedRole()` detects that the parent hierarchy does not include
9
+ * one of the required context roles, it sets `errorType` to
10
+ * `'INVALID_REQUIRED_CONTEXT_ROLE'` (mismatch) or `'NO_OWNER'`
11
+ * (no non-transparent ancestor found) and resets `role` to `null`.
12
+ * This checker translates either result into a user-facing violation.
13
+ *
14
+ * Only explicit roles (set via the `role` attribute) are checked;
15
+ * implicit roles are skipped because native HTML parent-child semantics
16
+ * are already guaranteed by the HTML specification.
17
+ *
18
+ * @see https://w3c.github.io/aria/#scope
19
+ * @param el - The element to inspect.
20
+ * @param computed - The computed role result from `getComputedRole()`.
21
+ * @returns A violation if the role's required context is not satisfied.
22
+ */
23
+ export declare const checkingRequiredAccessibilityParentRole: ElementChecker<boolean, Options, {
24
+ computed?: ComputedRole;
25
+ }>;
@@ -0,0 +1,56 @@
1
+ import { ARIA_RECOMMENDED_VERSION, ariaSpecs } from '@markuplint/ml-spec';
2
+ /**
3
+ * Checks whether an element with an explicit role satisfies its
4
+ * "Required Accessibility Parent Role" (called "Required Context Role" in ARIA 1.2).
5
+ *
6
+ * When `getComputedRole()` detects that the parent hierarchy does not include
7
+ * one of the required context roles, it sets `errorType` to
8
+ * `'INVALID_REQUIRED_CONTEXT_ROLE'` (mismatch) or `'NO_OWNER'`
9
+ * (no non-transparent ancestor found) and resets `role` to `null`.
10
+ * This checker translates either result into a user-facing violation.
11
+ *
12
+ * Only explicit roles (set via the `role` attribute) are checked;
13
+ * implicit roles are skipped because native HTML parent-child semantics
14
+ * are already guaranteed by the HTML specification.
15
+ *
16
+ * @see https://w3c.github.io/aria/#scope
17
+ * @param el - The element to inspect.
18
+ * @param computed - The computed role result from `getComputedRole()`.
19
+ * @returns A violation if the role's required context is not satisfied.
20
+ */
21
+ export const checkingRequiredAccessibilityParentRole = ({ el, computed }) => t => {
22
+ if (!computed ||
23
+ (computed.errorType !== 'INVALID_REQUIRED_CONTEXT_ROLE' &&
24
+ // NO_OWNER with role: null means all ancestors were transparent
25
+ // and no valid context could be found. NO_OWNER with role kept
26
+ // (parentElement === null) is a root fragment — skip.
27
+ !(computed.errorType === 'NO_OWNER' && !computed.role))) {
28
+ return;
29
+ }
30
+ const roleAttr = el.getAttributeNode('role');
31
+ if (!roleAttr) {
32
+ return;
33
+ }
34
+ const ariaVersion = el.rule.options?.version ?? el.ownerMLDocument.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
35
+ const { roles } = ariaSpecs(el.ownerMLDocument.specs, ariaVersion);
36
+ for (const token of roleAttr.tokenList?.allTokens() ?? []) {
37
+ const roleSpec = roles.find(r => r.name === token.raw);
38
+ if (!roleSpec) {
39
+ continue;
40
+ }
41
+ // `ariaSpecs().roles` returns raw spec data where the field is always
42
+ // `requiredContextRole` for all versions. The fallback to
43
+ // `requiredAccessibilityParentRole` is a defensive guard in case
44
+ // the spec-generator changes the field name in the future.
45
+ const parentRoles = roleSpec.requiredContextRole ?? roleSpec.requiredAccessibilityParentRole;
46
+ if (parentRoles && parentRoles.length > 0) {
47
+ return {
48
+ scope: token,
49
+ message: t('{0} requires {1}', t('the "{0*}" {1}', roleSpec.name, 'role'), parentRoles.length === 1 && parentRoles[0]
50
+ ? t('an accessibility parent with the "{0*}" {1}', parentRoles[0], 'role')
51
+ : t('an accessibility parent with one of the {0}', 'roles') +
52
+ `: ${t(parentRoles)}`),
53
+ };
54
+ }
55
+ }
56
+ };
@@ -2,16 +2,17 @@ import type { Options } from '../types.js';
2
2
  import type { ElementChecker } from '@markuplint/ml-core';
3
3
  import type { ARIARole } from '@markuplint/ml-spec';
4
4
  /**
5
- * Checks whether an element with a role that requires specific owned elements
6
- * actually contains children with the expected roles.
5
+ * Checks whether an element with a role that has "Allowed Accessibility Child Roles"
6
+ * (called "Required Owned Elements" in ARIA 1.2) actually contains children
7
+ * with the expected roles.
7
8
  *
8
9
  * For example, a `list` role must own at least one element with the `listitem` role.
9
10
  * This checker respects `aria-busy="true"` (which signals that content is still loading),
10
11
  * preprocessor blocks, and mutable children from template engines.
11
12
  *
12
13
  * @see https://w3c.github.io/aria/#mustContain
13
- * @param el - The element node to inspect for required owned elements.
14
- * @param role - The computed ARIA role of the element, which defines required owned elements.
14
+ * @param el - The element node to inspect for allowed accessibility child roles.
15
+ * @param role - The computed ARIA role of the element, which defines allowed accessibility child roles.
15
16
  * @returns A violation if the role requires owned elements and none are found.
16
17
  */
17
18
  export declare const checkingRequiredOwnedElements: ElementChecker<boolean, Options, {