@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,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
|
+
}
|
|
@@ -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(
|
|
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
|
|
5
|
-
*
|
|
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
|
|
7
|
-
*
|
|
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
|
-
|
|
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:
|
|
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;
|