@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,180 @@
1
+ import { createRule } from '@markuplint/ml-core';
2
+ import { DEF_LINK_TYPE_WHATWG, ALLOWED_LINK_TYPE_MICROFORMATS, DEF_LINK_TYPE_MICROFORMATS_DROPPED, DEF_LINK_TYPE_MICROFORMATS_DROPPED_WITHOUT_PREJUDICE, DEF_LINK_TYPE_MICROFORMATS_REJECTED, DEF_LINK_TYPE_MICROFORMATS_NON_HTML_REL_VALUES, } from '@markuplint/types';
3
+ import meta from './meta.js';
4
+ const TARGET_ELEMENTS = new Set(['link', 'a', 'area', 'form']);
5
+ /**
6
+ * Rule that validates link type keywords in the `rel` attribute on
7
+ * `<link>`, `<a>`, `<area>`, and `<form>` elements against the WHATWG
8
+ * standard and optionally the Microformats registry.
9
+ */
10
+ export default createRule({
11
+ meta: meta,
12
+ defaultOptions: {
13
+ allowMicroformats: false,
14
+ },
15
+ async verify({ document, report, t }) {
16
+ await document.walkOn('Element', el => {
17
+ if (!TARGET_ELEMENTS.has(el.localName)) {
18
+ return;
19
+ }
20
+ const relAttr = el.getAttributeNode('rel');
21
+ if (!relAttr) {
22
+ return;
23
+ }
24
+ if (relAttr.isDynamicValue) {
25
+ return;
26
+ }
27
+ const tokenList = relAttr.tokenList;
28
+ if (!tokenList || tokenList.length === 0) {
29
+ return;
30
+ }
31
+ const context = getElementContext(el);
32
+ if (!context) {
33
+ return;
34
+ }
35
+ const options = el.rule.options;
36
+ for (const tokenInfo of tokenList.allTokens()) {
37
+ const keyword = tokenInfo.raw;
38
+ const keywordLower = keyword.toLowerCase();
39
+ const message = validateKeyword(keywordLower, context, options, t);
40
+ if (message) {
41
+ report({
42
+ scope: el,
43
+ line: tokenInfo.startLine,
44
+ col: tokenInfo.startCol,
45
+ raw: keyword,
46
+ message,
47
+ });
48
+ }
49
+ }
50
+ });
51
+ },
52
+ });
53
+ function getElementContext(
54
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
55
+ el) {
56
+ const tagName = el.localName;
57
+ switch (tagName) {
58
+ case 'link': {
59
+ if (el.closest('body') !== null) {
60
+ return 'body-link';
61
+ }
62
+ return 'link';
63
+ }
64
+ case 'a':
65
+ case 'area': {
66
+ return 'a-area';
67
+ }
68
+ case 'form': {
69
+ return 'form';
70
+ }
71
+ default: {
72
+ return null;
73
+ }
74
+ }
75
+ }
76
+ function validateKeyword(keyword, context, options, t) {
77
+ // Check WHATWG standard keywords
78
+ const whatwgDef = DEF_LINK_TYPE_WHATWG.find(def => def.keyword.toLowerCase() === keyword);
79
+ if (whatwgDef) {
80
+ return validateWhatwgKeyword(whatwgDef, context, t);
81
+ }
82
+ // Check dropped keywords
83
+ if (DEF_LINK_TYPE_MICROFORMATS_DROPPED.some(def => def.keyword.toLowerCase() === keyword)) {
84
+ return t('{0} is {1:c}', `"${keyword}"`, 'dropped');
85
+ }
86
+ // Check dropped without prejudice keywords
87
+ if (DEF_LINK_TYPE_MICROFORMATS_DROPPED_WITHOUT_PREJUDICE.some(def => def.keyword.toLowerCase() === keyword)) {
88
+ return t('{0} is {1:c}', `"${keyword}"`, 'dropped');
89
+ }
90
+ // Check rejected keywords
91
+ if (DEF_LINK_TYPE_MICROFORMATS_REJECTED.some(def => def.keyword.toLowerCase() === keyword)) {
92
+ return t('{0} is {1:c}', `"${keyword}"`, 'rejected');
93
+ }
94
+ // Check non-HTML rel values
95
+ if (DEF_LINK_TYPE_MICROFORMATS_NON_HTML_REL_VALUES.some(def => def.keyword.toLowerCase() === keyword)) {
96
+ return t('{0} is {1:c}', `"${keyword}"`, 'not allowed');
97
+ }
98
+ // Handle Microformats
99
+ const { allowMicroformats } = options;
100
+ if (allowMicroformats === false || allowMicroformats === undefined) {
101
+ return t('The "{0*}" {1} is {2:c}', keyword, 'keyword', 'not allowed');
102
+ }
103
+ if (allowMicroformats === true) {
104
+ return validateMicroformatKeyword(keyword, context, t);
105
+ }
106
+ // allowMicroformats is string[]
107
+ if (Array.isArray(allowMicroformats)) {
108
+ const isAllowed = allowMicroformats.some(allowed => allowed.toLowerCase() === keyword);
109
+ if (!isAllowed) {
110
+ return t('The "{0*}" {1} is {2:c}', keyword, 'keyword', 'not allowed');
111
+ }
112
+ // Even if in the allow list, check element context
113
+ const microDef = ALLOWED_LINK_TYPE_MICROFORMATS.find(def => def.keyword.toLowerCase() === keyword);
114
+ if (microDef) {
115
+ return validateMicroformatContext(microDef, context, t);
116
+ }
117
+ // Keyword is in the user's allow list but not in any registry — allow it
118
+ return null;
119
+ }
120
+ return null;
121
+ }
122
+ function validateWhatwgKeyword(def, context, t) {
123
+ switch (context) {
124
+ case 'link': {
125
+ if (def.link === 'not allowed') {
126
+ return t('The "{0*}" {1} is not allowed on the {2}', def.keyword, 'keyword', '"link" element');
127
+ }
128
+ return null;
129
+ }
130
+ case 'body-link': {
131
+ if (def.link === 'not allowed') {
132
+ return t('The "{0*}" {1} is not allowed on the {2}', def.keyword, 'keyword', '"link" element');
133
+ }
134
+ if (def.bodyOk !== 'Yes') {
135
+ return t('The "{0*}" {1} is not allowed on the {2} inside the {3}', def.keyword, 'keyword', '"link" element', '"body" element');
136
+ }
137
+ return null;
138
+ }
139
+ case 'a-area': {
140
+ if (def.a === 'not allowed') {
141
+ return t('The "{0*}" {1} is not allowed on the {2}', def.keyword, 'keyword', '"a" element');
142
+ }
143
+ return null;
144
+ }
145
+ case 'form': {
146
+ if (def.form === 'not allowed') {
147
+ return t('The "{0*}" {1} is not allowed on the {2}', def.keyword, 'keyword', '"form" element');
148
+ }
149
+ return null;
150
+ }
151
+ }
152
+ }
153
+ function validateMicroformatKeyword(keyword, context, t) {
154
+ const microDef = ALLOWED_LINK_TYPE_MICROFORMATS.find(def => def.keyword.toLowerCase() === keyword);
155
+ if (!microDef) {
156
+ return t('The "{0*}" {1} is {2:c}', keyword, 'keyword', 'not allowed');
157
+ }
158
+ return validateMicroformatContext(microDef, context, t);
159
+ }
160
+ function validateMicroformatContext(def, context, t) {
161
+ switch (context) {
162
+ case 'link':
163
+ case 'body-link': {
164
+ if (!def.link) {
165
+ return t('The "{0*}" {1} is not allowed on the {2}', def.keyword, 'keyword', '"link" element');
166
+ }
167
+ return null;
168
+ }
169
+ case 'a-area': {
170
+ if (!def.a) {
171
+ return t('The "{0*}" {1} is not allowed on the {2}', def.keyword, 'keyword', '"a" element');
172
+ }
173
+ return null;
174
+ }
175
+ case 'form': {
176
+ // Microformats don't define form context; reject
177
+ return t('The "{0*}" {1} is not allowed on the {2}', def.keyword, 'keyword', '"form" element');
178
+ }
179
+ }
180
+ }
@@ -0,0 +1,5 @@
1
+ /** Rule metadata for the `link-types` rule, categorized as validation. */
2
+ declare const _default: {
3
+ readonly category: "validation";
4
+ };
5
+ export default _default;
@@ -0,0 +1,4 @@
1
+ /** Rule metadata for the `link-types` rule, categorized as validation. */
2
+ export default {
3
+ category: 'validation',
4
+ };
@@ -1,9 +1,6 @@
1
1
  import { createRule } from '@markuplint/ml-core';
2
- import { mayBeFocusable } from '@markuplint/ml-spec';
2
+ import { ARIA_RECOMMENDED_VERSION, mayBeFocusable } from '@markuplint/ml-spec';
3
3
  import meta from './meta.js';
4
- // TODO: It will be received from config
5
- /** The ARIA specification version used for accessible name computation. */
6
- const ARIA_VERSION = '1.2';
7
4
  /**
8
5
  * Command values from the Invoker Commands API that relate to popover behavior.
9
6
  * Comparison is case-insensitive per the HTML spec.
@@ -20,6 +17,7 @@ const POPOVER_COMMANDS = new Set(['toggle-popover', 'show-popover', 'hide-popove
20
17
  export default createRule({
21
18
  meta: meta,
22
19
  verify({ document, report, t }) {
20
+ const ariaVersion = document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
23
21
  const triggers = document.querySelectorAll('[popovertarget], [commandfor]');
24
22
  Triggers: for (const trigger of triggers) {
25
23
  let targetId = null;
@@ -50,7 +48,7 @@ export default createRule({
50
48
  }
51
49
  if ((node.is(node.ELEMENT_NODE) &&
52
50
  // Element has accessible name
53
- (node.getAccessibleName(ARIA_VERSION) ||
51
+ (node.getAccessibleName(ariaVersion) ||
54
52
  // Element is focusable
55
53
  mayBeFocusable(node, node.ownerMLDocument.specs))) ||
56
54
  (node.is(node.TEXT_NODE) &&
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Rule that detects orphaned end tags with no matching start tag.
3
3
  *
4
- * Scans text nodes for content that looks like a closing tag (starts with
5
- * `</`) which indicates a stray end tag that the parser could not match
4
+ * Scans text nodes and reports any that originated from invalid (bogus) AST
5
+ * nodes, which indicates a stray end tag that the parser could not match
6
6
  * to an opening element.
7
7
  */
8
8
  declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, null>>;
@@ -3,16 +3,15 @@ import meta from './meta.js';
3
3
  /**
4
4
  * Rule that detects orphaned end tags with no matching start tag.
5
5
  *
6
- * Scans text nodes for content that looks like a closing tag (starts with
7
- * `</`) which indicates a stray end tag that the parser could not match
6
+ * Scans text nodes and reports any that originated from invalid (bogus) AST
7
+ * nodes, which indicates a stray end tag that the parser could not match
8
8
  * to an opening element.
9
9
  */
10
10
  export default createRule({
11
11
  meta,
12
12
  async verify({ document, report, t }) {
13
13
  await document.walkOn('Text', text => {
14
- const raw = text.raw.trim();
15
- if (/^<\//.test(raw)) {
14
+ if (text.isBogus) {
16
15
  report({
17
16
  scope: text,
18
17
  message: t('{0} detected', t('Orphaned end tag')),
@@ -8,7 +8,7 @@ import type { ARIAVersion } from '@markuplint/ml-spec';
8
8
  * validation when dynamic IDs or preprocessor blocks are detected.
9
9
  */
10
10
  declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<import("@markuplint/ml-core").RuleConfigValue, {
11
- ariaVersion: ARIAVersion;
11
+ ariaVersion: ARIAVersion | undefined;
12
12
  fragmentRefersNameAttr: boolean;
13
13
  }>>;
14
14
  export default _default;
@@ -15,7 +15,7 @@ const HYPERLINK_SELECTOR = 'a[href], area[href]';
15
15
  export default createRule({
16
16
  meta: meta,
17
17
  defaultOptions: {
18
- ariaVersion: ARIA_RECOMMENDED_VERSION,
18
+ ariaVersion: undefined,
19
19
  fragmentRefersNameAttr: false,
20
20
  },
21
21
  async verify({ document, report, t }) {
@@ -100,7 +100,7 @@ export default createRule({
100
100
  }
101
101
  }
102
102
  }
103
- const { props } = ariaSpecs(document.specs, attr.rule.options.ariaVersion);
103
+ const { props } = ariaSpecs(document.specs, attr.rule.options.ariaVersion ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION);
104
104
  const aria = props.find(prop => prop.name === name);
105
105
  if (aria) {
106
106
  if (aria.value === 'ID reference' && !idList.has(value)) {
@@ -0,0 +1,71 @@
1
+ import type { BrowserName, SupportStatement } from '@mdn/browser-compat-data';
2
+ /**
3
+ * Target browser with name and minimum version.
4
+ */
5
+ export interface TargetBrowser {
6
+ readonly browser: BrowserName;
7
+ readonly version: string;
8
+ readonly displayName: string;
9
+ }
10
+ /**
11
+ * Result of checking browser support for a feature.
12
+ */
13
+ export interface UnsupportedResult {
14
+ readonly browser: BrowserName;
15
+ readonly displayName: string;
16
+ readonly targetVersion: string;
17
+ readonly addedVersion: string | false;
18
+ readonly removedVersion?: string;
19
+ }
20
+ /**
21
+ * Convert a browserslist browser name to a BCD browser name.
22
+ *
23
+ * @param browserslistName - The browser name from browserslist (e.g., "and_chr")
24
+ * @returns The corresponding BCD browser name, or null if unknown
25
+ */
26
+ export declare function toBcdBrowserId(browserslistName: string): BrowserName | null;
27
+ /**
28
+ * Parse a version string into a comparable numeric tuple.
29
+ *
30
+ * Handles BCD version prefixes like "≤37" by stripping the prefix.
31
+ *
32
+ * @param version - Version string (e.g., "16.4", "≤37", "preview")
33
+ * @returns Tuple of [major, minor, patch]
34
+ */
35
+ export declare function parseVersion(version: string): readonly [number, number, number];
36
+ /**
37
+ * Check if the target browser version satisfies the required version.
38
+ *
39
+ * Returns true if the target version is greater than or equal to the
40
+ * version the feature was added in.
41
+ *
42
+ * @param targetVersion - The browser version the user targets
43
+ * @param addedVersion - The version the feature was added in
44
+ * @returns Whether the target version supports the feature
45
+ */
46
+ export declare function isVersionSatisfied(targetVersion: string, addedVersion: string): boolean;
47
+ /**
48
+ * Check if a feature is supported by a specific browser version.
49
+ *
50
+ * @param support - The BCD support statement for the feature
51
+ * @param target - The target browser to check
52
+ * @returns null if supported or unknown, UnsupportedResult if not supported
53
+ */
54
+ export declare function checkSupport(support: SupportStatement | undefined, target: TargetBrowser): UnsupportedResult | null;
55
+ /**
56
+ * Check browser support for an HTML element.
57
+ *
58
+ * @param elementName - The HTML element name (e.g., "dialog")
59
+ * @param targets - Array of target browsers to check
60
+ * @returns Array of unsupported results (empty if all supported)
61
+ */
62
+ export declare function checkElementSupport(elementName: string, targets: readonly TargetBrowser[]): Promise<readonly UnsupportedResult[]>;
63
+ /**
64
+ * Check browser support for an HTML attribute on a specific element.
65
+ *
66
+ * @param elementName - The HTML element name (e.g., "input")
67
+ * @param attrName - The attribute name (e.g., "list")
68
+ * @param targets - Array of target browsers to check
69
+ * @returns Array of unsupported results (empty if all supported)
70
+ */
71
+ export declare function checkAttributeSupport(elementName: string, attrName: string, targets: readonly TargetBrowser[]): Promise<readonly UnsupportedResult[]>;
@@ -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;