@markuplint/rules 4.11.2 → 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 +63 -2
- package/SKILL.md +76 -0
- package/lib/attr-check.d.ts +46 -7
- package/lib/attr-check.js +42 -8
- package/lib/attr-duplication/index.d.ts +7 -0
- package/lib/attr-duplication/index.js +7 -0
- package/lib/attr-duplication/meta.d.ts +1 -0
- package/lib/attr-duplication/meta.js +1 -0
- package/lib/attr-value-quotes/index.d.ts +10 -0
- package/lib/attr-value-quotes/index.js +10 -0
- package/lib/attr-value-quotes/meta.d.ts +1 -0
- package/lib/attr-value-quotes/meta.js +1 -0
- package/lib/case-sensitive-attr-name/index.d.ts +9 -0
- package/lib/case-sensitive-attr-name/index.js +8 -0
- package/lib/case-sensitive-attr-name/meta.d.ts +1 -0
- package/lib/case-sensitive-attr-name/meta.js +1 -0
- package/lib/case-sensitive-tag-name/index.d.ts +9 -0
- package/lib/case-sensitive-tag-name/index.js +8 -0
- package/lib/case-sensitive-tag-name/meta.d.ts +1 -0
- package/lib/case-sensitive-tag-name/meta.js +1 -0
- package/lib/character-reference/index.d.ts +9 -0
- package/lib/character-reference/index.js +20 -0
- package/lib/character-reference/meta.d.ts +1 -0
- package/lib/character-reference/meta.js +1 -0
- package/lib/class-naming/index.d.ts +9 -0
- package/lib/class-naming/index.js +8 -0
- package/lib/class-naming/meta.d.ts +1 -0
- package/lib/class-naming/meta.js +1 -0
- 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 +29 -2
- package/lib/create-message.js +60 -2
- package/lib/debug.d.ts +9 -0
- package/lib/debug.js +5 -0
- package/lib/deprecated-attr/index.d.ts +6 -0
- package/lib/deprecated-attr/index.js +6 -0
- package/lib/deprecated-attr/meta.d.ts +1 -0
- package/lib/deprecated-attr/meta.js +1 -0
- package/lib/deprecated-element/index.d.ts +6 -0
- package/lib/deprecated-element/index.js +8 -2
- package/lib/deprecated-element/meta.d.ts +1 -0
- package/lib/deprecated-element/meta.js +1 -0
- package/lib/disallowed-element/index.d.ts +7 -0
- package/lib/disallowed-element/index.js +7 -0
- package/lib/disallowed-element/meta.d.ts +1 -0
- package/lib/disallowed-element/meta.js +1 -0
- package/lib/doctype/index.d.ts +10 -0
- package/lib/doctype/index.js +8 -0
- package/lib/doctype/meta.d.ts +1 -0
- package/lib/doctype/meta.js +1 -0
- package/lib/end-tag/index.d.ts +7 -0
- package/lib/end-tag/index.js +8 -1
- package/lib/end-tag/meta.d.ts +1 -0
- package/lib/end-tag/meta.js +1 -0
- package/lib/heading-levels/index.d.ts +6 -0
- package/lib/heading-levels/index.js +6 -1
- package/lib/heading-levels/meta.d.ts +1 -0
- package/lib/heading-levels/meta.js +1 -0
- package/lib/helpers.d.ts +105 -0
- package/lib/helpers.js +112 -16
- package/lib/id-duplication/index.d.ts +7 -0
- package/lib/id-duplication/index.js +7 -0
- package/lib/id-duplication/meta.d.ts +1 -0
- package/lib/id-duplication/meta.js +1 -0
- package/lib/index.d.ts +43 -41
- package/lib/index.js +24 -0
- package/lib/ineffective-attr/index.d.ts +7 -0
- package/lib/ineffective-attr/index.js +7 -0
- package/lib/ineffective-attr/meta.d.ts +1 -0
- package/lib/ineffective-attr/meta.js +1 -0
- package/lib/invalid-attr/index.d.ts +29 -21
- package/lib/invalid-attr/index.js +60 -109
- package/lib/invalid-attr/meta.d.ts +1 -0
- package/lib/invalid-attr/meta.js +1 -0
- package/lib/label-has-control/index.d.ts +7 -0
- package/lib/label-has-control/index.js +11 -0
- package/lib/label-has-control/meta.d.ts +1 -0
- package/lib/label-has-control/meta.js +1 -0
- package/lib/landmark-roles/index.d.ts +14 -0
- package/lib/landmark-roles/index.js +21 -0
- package/lib/landmark-roles/meta.d.ts +1 -0
- package/lib/landmark-roles/meta.js +1 -0
- 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.d.ts +8 -0
- package/lib/neighbor-popovers/index.js +28 -6
- package/lib/neighbor-popovers/meta.d.ts +1 -0
- package/lib/neighbor-popovers/meta.js +1 -0
- package/lib/no-ambiguous-navigable-target-names/index.d.ts +8 -0
- package/lib/no-ambiguous-navigable-target-names/index.js +8 -0
- package/lib/no-ambiguous-navigable-target-names/meta.d.ts +1 -0
- package/lib/no-ambiguous-navigable-target-names/meta.js +1 -0
- package/lib/no-boolean-attr-value/index.d.ts +7 -0
- package/lib/no-boolean-attr-value/index.js +7 -0
- package/lib/no-boolean-attr-value/meta.d.ts +1 -0
- package/lib/no-boolean-attr-value/meta.js +1 -0
- package/lib/no-consecutive-br/index.d.ts +8 -0
- package/lib/no-consecutive-br/index.js +8 -0
- package/lib/no-consecutive-br/meta.d.ts +1 -0
- package/lib/no-consecutive-br/meta.js +1 -0
- package/lib/no-default-value/index.d.ts +7 -0
- package/lib/no-default-value/index.js +7 -0
- package/lib/no-default-value/meta.d.ts +1 -0
- package/lib/no-default-value/meta.js +1 -0
- package/lib/no-duplicate-dt/index.d.ts +6 -0
- package/lib/no-duplicate-dt/index.js +6 -0
- package/lib/no-duplicate-dt/meta.d.ts +1 -0
- package/lib/no-duplicate-dt/meta.js +1 -0
- package/lib/no-empty-palpable-content/index.d.ts +13 -0
- package/lib/no-empty-palpable-content/index.js +12 -0
- package/lib/no-empty-palpable-content/meta.d.ts +1 -0
- package/lib/no-empty-palpable-content/meta.js +1 -0
- package/lib/no-hard-code-id/index.d.ts +8 -0
- package/lib/no-hard-code-id/index.js +8 -0
- package/lib/no-hard-code-id/meta.d.ts +1 -0
- package/lib/no-hard-code-id/meta.js +1 -0
- package/lib/no-orphaned-end-tag/index.d.ts +7 -0
- package/lib/no-orphaned-end-tag/index.js +8 -2
- package/lib/no-orphaned-end-tag/meta.d.ts +1 -0
- package/lib/no-orphaned-end-tag/meta.js +1 -0
- package/lib/no-refer-to-non-existent-id/index.d.ts +9 -1
- package/lib/no-refer-to-non-existent-id/index.js +11 -2
- package/lib/no-refer-to-non-existent-id/meta.d.ts +1 -0
- package/lib/no-refer-to-non-existent-id/meta.js +1 -0
- 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/no-use-event-handler-attr/index.d.ts +10 -0
- package/lib/no-use-event-handler-attr/index.js +8 -0
- package/lib/no-use-event-handler-attr/meta.d.ts +1 -0
- package/lib/no-use-event-handler-attr/meta.js +1 -0
- package/lib/permitted-contents/choice.d.ts +17 -0
- package/lib/permitted-contents/choice.js +31 -1
- package/lib/permitted-contents/complex-branch.d.ts +11 -7
- package/lib/permitted-contents/complex-branch.js +11 -7
- package/lib/permitted-contents/content-model.d.ts +11 -0
- package/lib/permitted-contents/content-model.js +32 -0
- package/lib/permitted-contents/count-pattern.d.ts +13 -7
- package/lib/permitted-contents/count-pattern.js +24 -8
- package/lib/permitted-contents/debug.browser.d.ts +12 -0
- package/lib/permitted-contents/debug.browser.js +12 -0
- package/lib/permitted-contents/debug.d.ts +12 -0
- package/lib/permitted-contents/debug.js +12 -0
- package/lib/permitted-contents/index.d.ts +12 -0
- package/lib/permitted-contents/index.js +21 -0
- package/lib/permitted-contents/matches-selector.d.ts +19 -0
- package/lib/permitted-contents/matches-selector.js +33 -0
- package/lib/permitted-contents/meta.d.ts +1 -0
- package/lib/permitted-contents/meta.js +1 -0
- package/lib/permitted-contents/order.d.ts +14 -7
- package/lib/permitted-contents/order.js +15 -8
- package/lib/permitted-contents/recursive-branch.d.ts +17 -0
- package/lib/permitted-contents/recursive-branch.js +17 -0
- package/lib/permitted-contents/represent-transparent-nodes.d.ts +54 -0
- package/lib/permitted-contents/represent-transparent-nodes.js +119 -12
- package/lib/permitted-contents/start.d.ts +12 -6
- package/lib/permitted-contents/start.js +12 -6
- package/lib/permitted-contents/transparent.d.ts +12 -0
- package/lib/permitted-contents/transparent.js +12 -0
- package/lib/permitted-contents/types.d.ts +67 -0
- package/lib/permitted-contents/utils.d.ts +160 -0
- package/lib/permitted-contents/utils.js +216 -35
- package/lib/placeholder-label-option/index.d.ts +7 -0
- package/lib/placeholder-label-option/index.js +19 -15
- package/lib/placeholder-label-option/meta.d.ts +1 -0
- package/lib/placeholder-label-option/meta.js +1 -0
- 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 +10 -4
- package/lib/require-accessible-name/index.js +13 -5
- package/lib/require-accessible-name/meta.d.ts +1 -0
- package/lib/require-accessible-name/meta.js +1 -0
- package/lib/require-datetime/index.d.ts +12 -0
- package/lib/require-datetime/index.js +8 -0
- package/lib/require-datetime/meta.d.ts +1 -0
- package/lib/require-datetime/meta.js +1 -0
- package/lib/require-datetime/types.d.ts +7 -0
- package/lib/require-datetime/utils.d.ts +18 -5
- package/lib/require-datetime/utils.js +53 -9
- 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 +21 -1
- package/lib/required-attr/index.js +34 -4
- package/lib/required-attr/meta.d.ts +1 -0
- package/lib/required-attr/meta.js +1 -0
- package/lib/required-element/index.d.ts +14 -0
- package/lib/required-element/index.js +14 -2
- package/lib/required-element/meta.d.ts +1 -0
- package/lib/required-element/meta.js +1 -0
- package/lib/required-h1/index.d.ts +12 -0
- package/lib/required-h1/index.js +7 -0
- package/lib/required-h1/meta.d.ts +1 -0
- package/lib/required-h1/meta.js +1 -0
- 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/find-children.d.ts +10 -0
- package/lib/table-row-column-alignment/find-children.js +11 -0
- package/lib/table-row-column-alignment/grid.d.ts +60 -0
- package/lib/table-row-column-alignment/grid.js +104 -26
- package/lib/table-row-column-alignment/index.d.ts +8 -0
- package/lib/table-row-column-alignment/index.js +8 -0
- package/lib/table-row-column-alignment/meta.d.ts +1 -0
- package/lib/table-row-column-alignment/meta.js +1 -0
- package/lib/table-row-column-alignment/types.d.ts +10 -0
- package/lib/use-list/index.d.ts +16 -0
- package/lib/use-list/index.js +20 -0
- package/lib/use-list/meta.d.ts +1 -0
- package/lib/use-list/meta.js +1 -0
- package/lib/wai-aria/checkings/abstract-role.d.ts +9 -0
- package/lib/wai-aria/checkings/abstract-role.js +14 -2
- package/lib/wai-aria/checkings/default-value.d.ts +11 -0
- package/lib/wai-aria/checkings/default-value.js +11 -0
- package/lib/wai-aria/checkings/deprecated-props.d.ts +11 -0
- package/lib/wai-aria/checkings/deprecated-props.js +11 -0
- package/lib/wai-aria/checkings/deprecated-role.d.ts +10 -0
- package/lib/wai-aria/checkings/deprecated-role.js +10 -0
- package/lib/wai-aria/checkings/disallowed-prop.d.ts +14 -0
- package/lib/wai-aria/checkings/disallowed-prop.js +19 -2
- package/lib/wai-aria/checkings/implicit-props.d.ts +13 -0
- package/lib/wai-aria/checkings/implicit-props.js +13 -0
- package/lib/wai-aria/checkings/implicit-role.d.ts +9 -0
- package/lib/wai-aria/checkings/implicit-role.js +14 -2
- package/lib/wai-aria/checkings/interaction-in-hidden.d.ts +9 -0
- package/lib/wai-aria/checkings/interaction-in-hidden.js +15 -4
- package/lib/wai-aria/checkings/no-global-prop.d.ts +10 -0
- package/lib/wai-aria/checkings/no-global-prop.js +10 -0
- package/lib/wai-aria/checkings/non-existent-role.d.ts +11 -0
- package/lib/wai-aria/checkings/non-existent-role.js +19 -2
- package/lib/wai-aria/checkings/permitted-roles.d.ts +10 -0
- package/lib/wai-aria/checkings/permitted-roles.js +15 -2
- package/lib/wai-aria/checkings/presentational-children.d.ts +7 -1
- package/lib/wai-aria/checkings/presentational-children.js +17 -3
- 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 +10 -1
- package/lib/wai-aria/checkings/required-owned-elements.js +69 -25
- package/lib/wai-aria/checkings/required-prop.d.ts +13 -0
- package/lib/wai-aria/checkings/required-prop.js +18 -2
- package/lib/wai-aria/checkings/value.d.ts +23 -0
- package/lib/wai-aria/checkings/value.js +32 -0
- package/lib/wai-aria/index.d.ts +10 -0
- package/lib/wai-aria/index.js +22 -4
- package/lib/wai-aria/meta.d.ts +1 -0
- package/lib/wai-aria/meta.js +1 -0
- package/lib/wai-aria/types.d.ts +25 -1
- package/package.json +16 -11
- package/schema.json +18 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mapping from browserslist browser names to BCD browser names.
|
|
3
|
+
*/
|
|
4
|
+
const BROWSERSLIST_TO_BCD = new Map([
|
|
5
|
+
['chrome', 'chrome'],
|
|
6
|
+
['firefox', 'firefox'],
|
|
7
|
+
['safari', 'safari'],
|
|
8
|
+
['edge', 'edge'],
|
|
9
|
+
['ie', 'ie'],
|
|
10
|
+
['opera', 'opera'],
|
|
11
|
+
['and_chr', 'chrome_android'],
|
|
12
|
+
['and_ff', 'firefox_android'],
|
|
13
|
+
['ios_saf', 'safari_ios'],
|
|
14
|
+
['samsung', 'samsunginternet_android'],
|
|
15
|
+
['op_mob', 'opera_android'],
|
|
16
|
+
['android', 'webview_android'],
|
|
17
|
+
]);
|
|
18
|
+
let bcdPromise;
|
|
19
|
+
/**
|
|
20
|
+
* Dynamically import @mdn/browser-compat-data.
|
|
21
|
+
*
|
|
22
|
+
* This uses dynamic import because BCD's main entry points to a ~70MB JSON
|
|
23
|
+
* file. Static import would force every markuplint user to parse that JSON
|
|
24
|
+
* at startup, even when this rule is disabled. Dynamic import defers the
|
|
25
|
+
* cost until the rule actually runs.
|
|
26
|
+
*
|
|
27
|
+
* Uses a Promise-based cache to prevent race conditions when multiple
|
|
28
|
+
* concurrent calls trigger the import simultaneously.
|
|
29
|
+
*/
|
|
30
|
+
function loadBcd() {
|
|
31
|
+
if (!bcdPromise) {
|
|
32
|
+
bcdPromise = import('@mdn/browser-compat-data').then(mod => mod.default ?? mod);
|
|
33
|
+
}
|
|
34
|
+
return bcdPromise;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Convert a browserslist browser name to a BCD browser name.
|
|
38
|
+
*
|
|
39
|
+
* @param browserslistName - The browser name from browserslist (e.g., "and_chr")
|
|
40
|
+
* @returns The corresponding BCD browser name, or null if unknown
|
|
41
|
+
*/
|
|
42
|
+
export function toBcdBrowserId(browserslistName) {
|
|
43
|
+
return BROWSERSLIST_TO_BCD.get(browserslistName) ?? null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse a version string into a comparable numeric tuple.
|
|
47
|
+
*
|
|
48
|
+
* Handles BCD version prefixes like "≤37" by stripping the prefix.
|
|
49
|
+
*
|
|
50
|
+
* @param version - Version string (e.g., "16.4", "≤37", "preview")
|
|
51
|
+
* @returns Tuple of [major, minor, patch]
|
|
52
|
+
*/
|
|
53
|
+
export function parseVersion(version) {
|
|
54
|
+
const cleaned = version.replace(/^≤/, '');
|
|
55
|
+
const parts = cleaned.split('.').map(Number);
|
|
56
|
+
return [parts[0] ?? Number.NaN, parts[1] ?? 0, parts[2] ?? 0];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Check if the target browser version satisfies the required version.
|
|
60
|
+
*
|
|
61
|
+
* Returns true if the target version is greater than or equal to the
|
|
62
|
+
* version the feature was added in.
|
|
63
|
+
*
|
|
64
|
+
* @param targetVersion - The browser version the user targets
|
|
65
|
+
* @param addedVersion - The version the feature was added in
|
|
66
|
+
* @returns Whether the target version supports the feature
|
|
67
|
+
*/
|
|
68
|
+
export function isVersionSatisfied(targetVersion, addedVersion) {
|
|
69
|
+
const [tMajor, tMinor, tPatch] = parseVersion(targetVersion);
|
|
70
|
+
const [aMajor, aMinor, aPatch] = parseVersion(addedVersion);
|
|
71
|
+
if (Number.isNaN(tMajor) || Number.isNaN(aMajor)) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
if (tMajor !== aMajor) {
|
|
75
|
+
return tMajor > aMajor;
|
|
76
|
+
}
|
|
77
|
+
if (tMinor !== aMinor) {
|
|
78
|
+
return tMinor > aMinor;
|
|
79
|
+
}
|
|
80
|
+
return tPatch >= aPatch;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get the standard (non-flagged, non-prefixed) support statement
|
|
84
|
+
* from a SupportStatement which may be an array.
|
|
85
|
+
*/
|
|
86
|
+
function getStandardSupport(
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
88
|
+
support) {
|
|
89
|
+
if (Array.isArray(support)) {
|
|
90
|
+
const standard = support.find(s => !s.flags && !s.prefix && !s.alternative_name);
|
|
91
|
+
return standard ?? null;
|
|
92
|
+
}
|
|
93
|
+
if (support.flags || support.prefix || support.alternative_name) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return support;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Check if a feature is supported by a specific browser version.
|
|
100
|
+
*
|
|
101
|
+
* @param support - The BCD support statement for the feature
|
|
102
|
+
* @param target - The target browser to check
|
|
103
|
+
* @returns null if supported or unknown, UnsupportedResult if not supported
|
|
104
|
+
*/
|
|
105
|
+
export function checkSupport(
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
107
|
+
support, target) {
|
|
108
|
+
if (!support) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const stmt = getStandardSupport(support);
|
|
112
|
+
if (!stmt) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const { version_added, version_removed } = stmt;
|
|
116
|
+
if (version_added === false) {
|
|
117
|
+
return {
|
|
118
|
+
browser: target.browser,
|
|
119
|
+
displayName: target.displayName,
|
|
120
|
+
targetVersion: target.version,
|
|
121
|
+
addedVersion: false,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (!isVersionSatisfied(target.version, version_added)) {
|
|
125
|
+
return {
|
|
126
|
+
browser: target.browser,
|
|
127
|
+
displayName: target.displayName,
|
|
128
|
+
targetVersion: target.version,
|
|
129
|
+
addedVersion: version_added,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Feature was added but later removed
|
|
133
|
+
if (version_removed != null && isVersionSatisfied(target.version, version_removed)) {
|
|
134
|
+
return {
|
|
135
|
+
browser: target.browser,
|
|
136
|
+
displayName: target.displayName,
|
|
137
|
+
targetVersion: target.version,
|
|
138
|
+
addedVersion: false,
|
|
139
|
+
removedVersion: version_removed,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Look up the BCD identifier for an HTML element.
|
|
146
|
+
*/
|
|
147
|
+
async function getElementIdentifier(elementName) {
|
|
148
|
+
const bcd = await loadBcd();
|
|
149
|
+
const elements = bcd.html.elements;
|
|
150
|
+
if (!elements) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const el = elements[elementName];
|
|
154
|
+
return el ?? null;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Check browser support for an HTML element.
|
|
158
|
+
*
|
|
159
|
+
* @param elementName - The HTML element name (e.g., "dialog")
|
|
160
|
+
* @param targets - Array of target browsers to check
|
|
161
|
+
* @returns Array of unsupported results (empty if all supported)
|
|
162
|
+
*/
|
|
163
|
+
export async function checkElementSupport(elementName, targets) {
|
|
164
|
+
const identifier = await getElementIdentifier(elementName);
|
|
165
|
+
if (!identifier?.__compat) {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
const results = [];
|
|
169
|
+
for (const target of targets) {
|
|
170
|
+
const support = identifier.__compat.support[target.browser];
|
|
171
|
+
const result = checkSupport(support, target);
|
|
172
|
+
if (result) {
|
|
173
|
+
results.push(result);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return results;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Check browser support for an HTML attribute on a specific element.
|
|
180
|
+
*
|
|
181
|
+
* @param elementName - The HTML element name (e.g., "input")
|
|
182
|
+
* @param attrName - The attribute name (e.g., "list")
|
|
183
|
+
* @param targets - Array of target browsers to check
|
|
184
|
+
* @returns Array of unsupported results (empty if all supported)
|
|
185
|
+
*/
|
|
186
|
+
export async function checkAttributeSupport(elementName, attrName, targets) {
|
|
187
|
+
const identifier = await getElementIdentifier(elementName);
|
|
188
|
+
if (!identifier) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
const attrIdentifier = identifier[attrName];
|
|
192
|
+
if (!attrIdentifier?.__compat) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
const results = [];
|
|
196
|
+
for (const target of targets) {
|
|
197
|
+
const support = attrIdentifier.__compat.support[target.browser];
|
|
198
|
+
const result = checkSupport(support, target);
|
|
199
|
+
if (result) {
|
|
200
|
+
results.push(result);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return results;
|
|
204
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for the `no-unsupported-features` rule.
|
|
3
|
+
*/
|
|
4
|
+
type Options = {
|
|
5
|
+
/** A browserslist query to override project configuration. */
|
|
6
|
+
readonly browserslist?: string | readonly string[];
|
|
7
|
+
/** Explicit path to a browserslist configuration file. */
|
|
8
|
+
readonly browserslistConfig?: string;
|
|
9
|
+
/** Browserslist environment name (e.g., "production"). */
|
|
10
|
+
readonly browserslistEnv?: string;
|
|
11
|
+
/** Features to ignore. Element name ("dialog") or attribute pattern ("input[list]"). */
|
|
12
|
+
readonly ignoreFeatures?: readonly string[];
|
|
13
|
+
/** Whether to warn about experimental elements and attributes. */
|
|
14
|
+
readonly checkExperimental?: boolean;
|
|
15
|
+
/** Whether to warn about non-standard elements and attributes. */
|
|
16
|
+
readonly checkNonStandard?: boolean;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Rule that warns when using HTML elements or attributes that are not
|
|
20
|
+
* supported by the project's target browsers (via browserslist),
|
|
21
|
+
* or that are experimental/non-standard.
|
|
22
|
+
*/
|
|
23
|
+
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, Options>>;
|
|
24
|
+
export default _default;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { createRule, getAttrSpecs, getSpec } from '@markuplint/ml-core';
|
|
2
|
+
import { checkAttributeSupport, checkElementSupport } from './compat-data.js';
|
|
3
|
+
import meta from './meta.js';
|
|
4
|
+
import { clearBrowserslistCache, resolveTargetBrowsers } from './resolve-browsers.js';
|
|
5
|
+
/**
|
|
6
|
+
* Rule that warns when using HTML elements or attributes that are not
|
|
7
|
+
* supported by the project's target browsers (via browserslist),
|
|
8
|
+
* or that are experimental/non-standard.
|
|
9
|
+
*/
|
|
10
|
+
export default createRule({
|
|
11
|
+
meta: meta,
|
|
12
|
+
defaultSeverity: 'warning',
|
|
13
|
+
defaultOptions: {
|
|
14
|
+
checkExperimental: false,
|
|
15
|
+
checkNonStandard: false,
|
|
16
|
+
},
|
|
17
|
+
async verify({ document, report, t }) {
|
|
18
|
+
const options = document.rule.options;
|
|
19
|
+
const targetBrowsers = resolveTargetBrowsers(document.filename, {
|
|
20
|
+
browserslist: options.browserslist,
|
|
21
|
+
browserslistConfig: options.browserslistConfig,
|
|
22
|
+
browserslistEnv: options.browserslistEnv,
|
|
23
|
+
});
|
|
24
|
+
const hasTargetBrowsers = targetBrowsers != null && targetBrowsers.length > 0;
|
|
25
|
+
const checkExperimental = options.checkExperimental ?? false;
|
|
26
|
+
const checkNonStandard = options.checkNonStandard ?? false;
|
|
27
|
+
// If no browserslist config and no experimental/nonStandard check, rule is no-op
|
|
28
|
+
if (!hasTargetBrowsers && !checkExperimental && !checkNonStandard) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const ignoreFeatures = new Set(options.ignoreFeatures);
|
|
32
|
+
await document.walkOn('Element', async (el) => {
|
|
33
|
+
// Only check HTML elements
|
|
34
|
+
if (el.namespaceURI !== 'http://www.w3.org/1999/xhtml' || el.elementType !== 'html') {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const elName = el.localName;
|
|
38
|
+
const elOptions = el.rule.options;
|
|
39
|
+
const elIgnoreFeatures = new Set(elOptions.ignoreFeatures ?? ignoreFeatures);
|
|
40
|
+
// Check element itself
|
|
41
|
+
if (!elIgnoreFeatures.has(elName)) {
|
|
42
|
+
// Experimental check (spec-based, no BCD needed)
|
|
43
|
+
if (elOptions.checkExperimental ?? checkExperimental) {
|
|
44
|
+
const spec = getSpec(el, document.specs.specs);
|
|
45
|
+
if (spec?.experimental) {
|
|
46
|
+
report({
|
|
47
|
+
scope: el,
|
|
48
|
+
message: t('{0} is {1:c}', t('the "{0*}" {1}', elName, 'element'), 'experimental'),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// NonStandard check (spec-based, no BCD needed)
|
|
53
|
+
if (elOptions.checkNonStandard ?? checkNonStandard) {
|
|
54
|
+
const spec = getSpec(el, document.specs.specs);
|
|
55
|
+
if (spec?.nonStandard) {
|
|
56
|
+
report({
|
|
57
|
+
scope: el,
|
|
58
|
+
message: t('{0} is {1:c}', t('the "{0*}" {1}', elName, 'element'), 'non-standard'),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Browser support check (BCD-based)
|
|
63
|
+
if (hasTargetBrowsers) {
|
|
64
|
+
const unsupported = await checkElementSupport(elName, targetBrowsers);
|
|
65
|
+
if (unsupported.length > 0) {
|
|
66
|
+
report({
|
|
67
|
+
scope: el,
|
|
68
|
+
message: formatUnsupportedMessage(t, elName, 'element', unsupported),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Check attributes
|
|
74
|
+
const attrSpecs = getAttrSpecs(el, document.specs);
|
|
75
|
+
for (const attr of el.attributes) {
|
|
76
|
+
if (attr.isDirective) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const attrName = attr.name;
|
|
80
|
+
const ignoreKey = `${elName}[${attrName}]`;
|
|
81
|
+
if (elIgnoreFeatures.has(ignoreKey)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const attrSpec = attrSpecs?.find(s => s.name === attrName);
|
|
85
|
+
// Experimental attribute check
|
|
86
|
+
if ((elOptions.checkExperimental ?? checkExperimental) && attrSpec?.experimental) {
|
|
87
|
+
report({
|
|
88
|
+
scope: attr,
|
|
89
|
+
line: attr.nameNode?.startLine,
|
|
90
|
+
col: attr.nameNode?.startCol,
|
|
91
|
+
raw: attr.nameNode?.raw,
|
|
92
|
+
message: t('{0} is {1:c}', t('the "{0*}" {1}', attrName, 'attribute'), 'experimental'),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// NonStandard attribute check
|
|
96
|
+
if ((elOptions.checkNonStandard ?? checkNonStandard) && attrSpec?.nonStandard) {
|
|
97
|
+
report({
|
|
98
|
+
scope: attr,
|
|
99
|
+
line: attr.nameNode?.startLine,
|
|
100
|
+
col: attr.nameNode?.startCol,
|
|
101
|
+
raw: attr.nameNode?.raw,
|
|
102
|
+
message: t('{0} is {1:c}', t('the "{0*}" {1}', attrName, 'attribute'), 'non-standard'),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
// Browser support attribute check
|
|
106
|
+
if (hasTargetBrowsers) {
|
|
107
|
+
const unsupported = await checkAttributeSupport(elName, attrName, targetBrowsers);
|
|
108
|
+
if (unsupported.length > 0) {
|
|
109
|
+
report({
|
|
110
|
+
scope: attr,
|
|
111
|
+
line: attr.nameNode?.startLine,
|
|
112
|
+
col: attr.nameNode?.startCol,
|
|
113
|
+
raw: attr.nameNode?.raw,
|
|
114
|
+
message: formatUnsupportedMessage(t, attrName, 'attribute', unsupported),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
clearBrowserslistCache();
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
/**
|
|
124
|
+
* Format an unsupported feature message listing affected browsers.
|
|
125
|
+
*/
|
|
126
|
+
function formatUnsupportedMessage(t, featureName, featureType, unsupported) {
|
|
127
|
+
const browserDetails = unsupported
|
|
128
|
+
.map(u => {
|
|
129
|
+
if (u.addedVersion === false) {
|
|
130
|
+
if (u.removedVersion) {
|
|
131
|
+
return `${u.displayName} (removed in ${u.removedVersion})`;
|
|
132
|
+
}
|
|
133
|
+
return `${u.displayName} (not supported)`;
|
|
134
|
+
}
|
|
135
|
+
return `${u.displayName} (added in ${u.addedVersion}, target: ${u.targetVersion})`;
|
|
136
|
+
})
|
|
137
|
+
.join(', ');
|
|
138
|
+
return t('{0} is not supported in {1}', t('the "{0*}" {1}', featureName, featureType), browserDetails);
|
|
139
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TargetBrowser } from './compat-data.js';
|
|
2
|
+
/**
|
|
3
|
+
* Options for resolving target browsers.
|
|
4
|
+
*/
|
|
5
|
+
export interface BrowserslistOptions {
|
|
6
|
+
readonly browserslist?: string | readonly string[];
|
|
7
|
+
readonly browserslistConfig?: string;
|
|
8
|
+
readonly browserslistEnv?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Resolve target browsers from browserslist configuration.
|
|
12
|
+
*
|
|
13
|
+
* Priority:
|
|
14
|
+
* 1. `options.browserslist` (explicit query)
|
|
15
|
+
* 2. `options.browserslistConfig` (explicit config file path)
|
|
16
|
+
* 3. Auto-detect from `filename` path
|
|
17
|
+
*
|
|
18
|
+
* When no configuration is found, returns null (rule becomes no-op).
|
|
19
|
+
* For duplicate browsers, the minimum version is kept.
|
|
20
|
+
*
|
|
21
|
+
* @param filename - The document filename for auto-detection
|
|
22
|
+
* @param options - Browserslist resolution options
|
|
23
|
+
* @returns Array of target browsers, or null if no config found
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveTargetBrowsers(filename: string | undefined, options: BrowserslistOptions): readonly TargetBrowser[] | null;
|
|
26
|
+
/**
|
|
27
|
+
* Clear the internal browserslist config cache.
|
|
28
|
+
* Useful for testing.
|
|
29
|
+
*/
|
|
30
|
+
export declare function clearBrowserslistCache(): void;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import browserslist from 'browserslist';
|
|
2
|
+
import { isVersionSatisfied, parseVersion, toBcdBrowserId } from './compat-data.js';
|
|
3
|
+
/**
|
|
4
|
+
* Display names for BCD browser identifiers.
|
|
5
|
+
*/
|
|
6
|
+
const BROWSER_DISPLAY_NAMES = new Map([
|
|
7
|
+
['chrome', 'Chrome'],
|
|
8
|
+
['chrome_android', 'Chrome Android'],
|
|
9
|
+
['edge', 'Edge'],
|
|
10
|
+
['firefox', 'Firefox'],
|
|
11
|
+
['firefox_android', 'Firefox Android'],
|
|
12
|
+
['ie', 'Internet Explorer'],
|
|
13
|
+
['opera', 'Opera'],
|
|
14
|
+
['opera_android', 'Opera Android'],
|
|
15
|
+
['safari', 'Safari'],
|
|
16
|
+
['safari_ios', 'Safari iOS'],
|
|
17
|
+
['samsunginternet_android', 'Samsung Internet'],
|
|
18
|
+
['webview_android', 'WebView Android'],
|
|
19
|
+
]);
|
|
20
|
+
const configCache = new Map();
|
|
21
|
+
/**
|
|
22
|
+
* Resolve target browsers from browserslist configuration.
|
|
23
|
+
*
|
|
24
|
+
* Priority:
|
|
25
|
+
* 1. `options.browserslist` (explicit query)
|
|
26
|
+
* 2. `options.browserslistConfig` (explicit config file path)
|
|
27
|
+
* 3. Auto-detect from `filename` path
|
|
28
|
+
*
|
|
29
|
+
* When no configuration is found, returns null (rule becomes no-op).
|
|
30
|
+
* For duplicate browsers, the minimum version is kept.
|
|
31
|
+
*
|
|
32
|
+
* @param filename - The document filename for auto-detection
|
|
33
|
+
* @param options - Browserslist resolution options
|
|
34
|
+
* @returns Array of target browsers, or null if no config found
|
|
35
|
+
*/
|
|
36
|
+
export function resolveTargetBrowsers(filename, options) {
|
|
37
|
+
if (options.browserslist != null) {
|
|
38
|
+
const queries = Array.isArray(options.browserslist) ? options.browserslist : [options.browserslist];
|
|
39
|
+
const cacheKey = `query:${queries.join(',')}`;
|
|
40
|
+
if (configCache.has(cacheKey)) {
|
|
41
|
+
return configCache.get(cacheKey) ?? null;
|
|
42
|
+
}
|
|
43
|
+
const result = parseBrowsersList(browserslist(queries));
|
|
44
|
+
configCache.set(cacheKey, result);
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
const configPath = options.browserslistConfig ?? filename;
|
|
48
|
+
if (!configPath) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const env = options.browserslistEnv;
|
|
52
|
+
const cacheKey = `path:${configPath}:${env ?? ''}`;
|
|
53
|
+
if (configCache.has(cacheKey)) {
|
|
54
|
+
return configCache.get(cacheKey) ?? null;
|
|
55
|
+
}
|
|
56
|
+
const config = browserslist.loadConfig({
|
|
57
|
+
path: configPath,
|
|
58
|
+
env,
|
|
59
|
+
...(options.browserslistConfig ? { config: options.browserslistConfig } : {}),
|
|
60
|
+
});
|
|
61
|
+
if (!config) {
|
|
62
|
+
configCache.set(cacheKey, null);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const result = parseBrowsersList(browserslist(config));
|
|
66
|
+
configCache.set(cacheKey, result);
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Parse browserslist output into TargetBrowser array.
|
|
71
|
+
*
|
|
72
|
+
* For duplicate browsers, keeps the minimum version to ensure
|
|
73
|
+
* compatibility with the widest range of target browsers.
|
|
74
|
+
*/
|
|
75
|
+
function parseBrowsersList(browsers) {
|
|
76
|
+
const browserMap = new Map();
|
|
77
|
+
for (const entry of browsers) {
|
|
78
|
+
const parts = entry.split(' ');
|
|
79
|
+
if (parts.length < 2) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const [name, rawVersion] = parts;
|
|
83
|
+
if (!name || !rawVersion) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// Handle hyphenated version ranges (e.g., "16.3-16.4") by taking the minimum
|
|
87
|
+
const version = rawVersion.includes('-') ? rawVersion.split('-')[0] : rawVersion;
|
|
88
|
+
const bcdId = toBcdBrowserId(name);
|
|
89
|
+
if (!bcdId) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const existing = browserMap.get(bcdId);
|
|
93
|
+
const displayName = BROWSER_DISPLAY_NAMES.get(bcdId) ?? name;
|
|
94
|
+
if (existing) {
|
|
95
|
+
// Keep minimum version — treat "preview" (NaN) as the highest possible version
|
|
96
|
+
const [newMajor] = parseVersion(version);
|
|
97
|
+
if (!Number.isNaN(newMajor)) {
|
|
98
|
+
// New version is concrete — replace if existing is "preview" or new < existing
|
|
99
|
+
const [existingMajor] = parseVersion(existing.version);
|
|
100
|
+
if (Number.isNaN(existingMajor) || !isVersionSatisfied(version, existing.version)) {
|
|
101
|
+
browserMap.set(bcdId, { version, displayName });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// If new version is "preview", never replace (preview is the highest)
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
browserMap.set(bcdId, { version, displayName });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (browserMap.size === 0) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const result = [];
|
|
114
|
+
for (const [browser, { version, displayName }] of browserMap) {
|
|
115
|
+
result.push({ browser, version, displayName });
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Clear the internal browserslist config cache.
|
|
121
|
+
* Useful for testing.
|
|
122
|
+
*/
|
|
123
|
+
export function clearBrowserslistCache() {
|
|
124
|
+
configCache.clear();
|
|
125
|
+
}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
/** Configuration options for the `no-use-event-handler-attr` rule. */
|
|
1
2
|
type Options = {
|
|
3
|
+
/** Attribute name pattern(s) to exclude from the check. */
|
|
2
4
|
ignore?: string | string[];
|
|
3
5
|
};
|
|
6
|
+
/**
|
|
7
|
+
* Rule that disallows inline event handler attributes (e.g., `onclick`,
|
|
8
|
+
* `onchange`).
|
|
9
|
+
*
|
|
10
|
+
* Reports any attribute on an HTML element whose name starts with `on`,
|
|
11
|
+
* indicating an inline event handler. An `ignore` option allows specific
|
|
12
|
+
* attribute names or patterns to be excluded from the check.
|
|
13
|
+
*/
|
|
4
14
|
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, Options>>;
|
|
5
15
|
export default _default;
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
2
|
import { match } from '../helpers.js';
|
|
3
3
|
import meta from './meta.js';
|
|
4
|
+
/**
|
|
5
|
+
* Rule that disallows inline event handler attributes (e.g., `onclick`,
|
|
6
|
+
* `onchange`).
|
|
7
|
+
*
|
|
8
|
+
* Reports any attribute on an HTML element whose name starts with `on`,
|
|
9
|
+
* indicating an inline event handler. An `ignore` option allows specific
|
|
10
|
+
* attribute names or patterns to be excluded from the check.
|
|
11
|
+
*/
|
|
4
12
|
export default createRule({
|
|
5
13
|
meta: meta,
|
|
6
14
|
defaultSeverity: 'warning',
|
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import type { ChildNode, Options, Result, Specs } from './types.js';
|
|
2
2
|
import type { PermittedContentChoice } from '@markuplint/ml-spec';
|
|
3
3
|
import type { ReadonlyDeep } from 'type-fest';
|
|
4
|
+
/**
|
|
5
|
+
* Evaluates a choice (alternation) pattern against a list of child nodes.
|
|
6
|
+
* Tries each branch of the choice in order and returns the first successful match.
|
|
7
|
+
* If no branch fully matches, selects the "barely matched" result that consumed
|
|
8
|
+
* the most nodes, preferring `UNEXPECTED_EXTRA_NODE` results (which indicate
|
|
9
|
+
* partial progress) over missing-node results.
|
|
10
|
+
*
|
|
11
|
+
* This implements the alternation (`|`) semantics found in content model definitions,
|
|
12
|
+
* e.g., "either flow content or phrasing content".
|
|
13
|
+
*
|
|
14
|
+
* @param pattern - The choice pattern containing multiple alternative content model branches.
|
|
15
|
+
* @param childNodes - The child nodes to validate against the choice branches.
|
|
16
|
+
* @param specs - The resolved spec data for content model lookups.
|
|
17
|
+
* @param options - Validation behavior options.
|
|
18
|
+
* @param depth - The current recursion depth, used for debug logging and nested evaluation.
|
|
19
|
+
* @returns A result from the best-matching branch, or the branch that came closest to matching.
|
|
20
|
+
*/
|
|
4
21
|
export declare function choice(pattern: ReadonlyDeep<PermittedContentChoice>, childNodes: readonly ChildNode[], specs: Specs, options: Options, depth: number): Result;
|
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
import { bgBlue, bgGreen, cmLog } from './debug.js';
|
|
2
2
|
import { order } from './order.js';
|
|
3
3
|
import { Collection, modelLog } from './utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* WeakMap that tracks which choice branch index produced each result,
|
|
6
|
+
* used for debug logging to identify the best-matching branch.
|
|
7
|
+
*/
|
|
4
8
|
const indexes = new WeakMap();
|
|
9
|
+
/**
|
|
10
|
+
* Evaluates a choice (alternation) pattern against a list of child nodes.
|
|
11
|
+
* Tries each branch of the choice in order and returns the first successful match.
|
|
12
|
+
* If no branch fully matches, selects the "barely matched" result that consumed
|
|
13
|
+
* the most nodes, preferring `UNEXPECTED_EXTRA_NODE` results (which indicate
|
|
14
|
+
* partial progress) over missing-node results.
|
|
15
|
+
*
|
|
16
|
+
* This implements the alternation (`|`) semantics found in content model definitions,
|
|
17
|
+
* e.g., "either flow content or phrasing content".
|
|
18
|
+
*
|
|
19
|
+
* @param pattern - The choice pattern containing multiple alternative content model branches.
|
|
20
|
+
* @param childNodes - The child nodes to validate against the choice branches.
|
|
21
|
+
* @param specs - The resolved spec data for content model lookups.
|
|
22
|
+
* @param options - Validation behavior options.
|
|
23
|
+
* @param depth - The current recursion depth, used for debug logging and nested evaluation.
|
|
24
|
+
* @returns A result from the best-matching branch, or the branch that came closest to matching.
|
|
25
|
+
*/
|
|
5
26
|
export function choice(pattern,
|
|
6
27
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
7
28
|
childNodes, specs, options, depth) {
|
|
@@ -30,7 +51,7 @@ childNodes, specs, options, depth) {
|
|
|
30
51
|
indexes.set(result, i);
|
|
31
52
|
i++;
|
|
32
53
|
}
|
|
33
|
-
const barelyMatchedResult = unmatchedResults.
|
|
54
|
+
const barelyMatchedResult = unmatchedResults.toSorted((a, b) => {
|
|
34
55
|
if (a.type !== b.type) {
|
|
35
56
|
if (a.type === 'UNEXPECTED_EXTRA_NODE') {
|
|
36
57
|
return -1;
|
|
@@ -64,6 +85,15 @@ childNodes, specs, options, depth) {
|
|
|
64
85
|
hint: barelyMatchedResult.hint,
|
|
65
86
|
};
|
|
66
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Formats a debug log string for a choice pattern, highlighting the selected
|
|
90
|
+
* branch index with a color (green for a full match, blue for a barely-matched fallback).
|
|
91
|
+
*
|
|
92
|
+
* @param choice - The array of choice branches from the pattern.
|
|
93
|
+
* @param index - The index of the selected branch.
|
|
94
|
+
* @param barely - Whether this is a barely-matched fallback (uses blue) or a full match (uses green).
|
|
95
|
+
* @returns A formatted string showing all branches with the selected one highlighted.
|
|
96
|
+
*/
|
|
67
97
|
function choiceLogString(choice, index, barely = false) {
|
|
68
98
|
const colorFn = barely ? bgBlue : bgGreen;
|
|
69
99
|
return choice
|