@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.
- package/CHANGELOG.md +58 -0
- package/lib/attr-check.d.ts +5 -2
- package/lib/attr-check.js +13 -3
- package/lib/character-reference/index.d.ts +2 -1
- package/lib/character-reference/index.js +5 -1
- package/lib/correct-aspect-ratio/index.d.ts +5 -0
- package/lib/correct-aspect-ratio/index.js +80 -0
- package/lib/correct-aspect-ratio/meta.d.ts +4 -0
- package/lib/correct-aspect-ratio/meta.js +3 -0
- package/lib/correct-aspect-ratio/resolve-image-size.d.ts +24 -0
- package/lib/correct-aspect-ratio/resolve-image-size.js +165 -0
- package/lib/create-message.d.ts +2 -2
- package/lib/create-message.js +4 -1
- package/lib/deprecated-element/index.d.ts +2 -4
- package/lib/deprecated-element/index.js +4 -6
- package/lib/end-tag/index.js +1 -1
- package/lib/heading-levels/index.js +0 -1
- package/lib/helpers.d.ts +1 -0
- package/lib/helpers.js +8 -15
- package/lib/index.d.ts +30 -41
- package/lib/index.js +12 -0
- package/lib/invalid-attr/index.d.ts +12 -28
- package/lib/invalid-attr/index.js +50 -116
- package/lib/link-types/index.d.ts +10 -0
- package/lib/link-types/index.js +180 -0
- package/lib/link-types/meta.d.ts +5 -0
- package/lib/link-types/meta.js +4 -0
- package/lib/neighbor-popovers/index.js +3 -5
- package/lib/no-orphaned-end-tag/index.d.ts +2 -2
- package/lib/no-orphaned-end-tag/index.js +3 -4
- package/lib/no-refer-to-non-existent-id/index.d.ts +1 -1
- package/lib/no-refer-to-non-existent-id/index.js +2 -2
- package/lib/no-unsupported-features/compat-data.d.ts +71 -0
- package/lib/no-unsupported-features/compat-data.js +204 -0
- package/lib/no-unsupported-features/index.d.ts +24 -0
- package/lib/no-unsupported-features/index.js +139 -0
- package/lib/no-unsupported-features/meta.d.ts +5 -0
- package/lib/no-unsupported-features/meta.js +4 -0
- package/lib/no-unsupported-features/resolve-browsers.d.ts +30 -0
- package/lib/no-unsupported-features/resolve-browsers.js +125 -0
- package/lib/permitted-contents/choice.js +1 -1
- package/lib/permitted-contents/count-pattern.js +1 -1
- package/lib/permitted-contents/order.js +1 -1
- package/lib/permitted-contents/represent-transparent-nodes.d.ts +26 -1
- package/lib/permitted-contents/represent-transparent-nodes.js +96 -13
- package/lib/permitted-contents/utils.d.ts +1 -1
- package/lib/permitted-contents/utils.js +22 -35
- package/lib/redundant-accessible-name/index.d.ts +10 -0
- package/lib/redundant-accessible-name/index.js +123 -0
- package/lib/redundant-accessible-name/meta.d.ts +5 -0
- package/lib/redundant-accessible-name/meta.js +4 -0
- package/lib/require-accessible-name/index.d.ts +3 -8
- package/lib/require-accessible-name/index.js +6 -5
- package/lib/require-dialog-autofocus/index.d.ts +22 -0
- package/lib/require-dialog-autofocus/index.js +62 -0
- package/lib/require-dialog-autofocus/meta.d.ts +5 -0
- package/lib/require-dialog-autofocus/meta.js +4 -0
- package/lib/required-attr/index.d.ts +8 -1
- package/lib/required-attr/index.js +27 -4
- package/lib/required-element/index.js +1 -1
- package/lib/srcset-sizes-constraint/index.d.ts +6 -0
- package/lib/srcset-sizes-constraint/index.js +108 -0
- package/lib/srcset-sizes-constraint/meta.d.ts +5 -0
- package/lib/srcset-sizes-constraint/meta.js +4 -0
- package/lib/srcset-sizes-constraint/parse-srcset.d.ts +40 -0
- package/lib/srcset-sizes-constraint/parse-srcset.js +56 -0
- package/lib/table-row-column-alignment/grid.js +22 -29
- package/lib/wai-aria/checkings/abstract-role.js +5 -2
- package/lib/wai-aria/checkings/disallowed-prop.js +5 -2
- package/lib/wai-aria/checkings/implicit-role.js +5 -2
- package/lib/wai-aria/checkings/non-existent-role.d.ts +1 -0
- package/lib/wai-aria/checkings/non-existent-role.js +9 -2
- package/lib/wai-aria/checkings/permitted-roles.js +5 -2
- package/lib/wai-aria/checkings/presentational-children.js +3 -2
- package/lib/wai-aria/checkings/required-accessibility-parent-role.d.ts +25 -0
- package/lib/wai-aria/checkings/required-accessibility-parent-role.js +56 -0
- package/lib/wai-aria/checkings/required-owned-elements.d.ts +5 -4
- package/lib/wai-aria/checkings/required-owned-elements.js +57 -28
- package/lib/wai-aria/checkings/required-prop.js +5 -2
- package/lib/wai-aria/index.js +12 -4
- package/lib/wai-aria/types.d.ts +9 -2
- package/package.json +15 -10
- 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,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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
this.theadGrid = createGrid(
|
|
43
|
-
this.tbodyGrid = createGrid(
|
|
44
|
-
this.tfootGrid = createGrid(
|
|
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 [...
|
|
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:
|
|
85
|
+
elements: this.#theadRowElements,
|
|
92
86
|
},
|
|
93
87
|
{
|
|
94
88
|
section: this.tbodyGrid,
|
|
95
|
-
elements:
|
|
89
|
+
elements: this.#tbodyRowElements,
|
|
96
90
|
},
|
|
97
91
|
{
|
|
98
92
|
section: this.tfootGrid,
|
|
99
|
-
elements:
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
6
|
-
*
|
|
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
|
|
14
|
-
* @param role - The computed ARIA role of the element, which defines
|
|
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, {
|