@markuplint/rules 5.1.1 → 5.3.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 +47 -0
- package/CLAUDE.md +19 -19
- package/lib/aria-prop-requires-role/index.js +2 -1
- package/lib/attr-check.js +6 -12
- package/lib/attr-eligibility.d.ts +15 -8
- package/lib/attr-eligibility.js +19 -10
- package/lib/attr-order/index.js +24 -18
- package/lib/case-sensitive-attr-name/index.js +24 -5
- package/lib/class-naming/index.js +6 -0
- package/lib/debug.d.ts +8 -1
- package/lib/debug.js +8 -1
- package/lib/element-supports-aria-prop/index.js +2 -1
- package/lib/form-attr-references-form/index.d.ts +6 -3
- package/lib/form-attr-references-form/index.js +6 -3
- package/lib/helpers.d.ts +111 -1
- package/lib/helpers.js +212 -2
- package/lib/input-list-references-datalist/index.d.ts +8 -6
- package/lib/input-list-references-datalist/index.js +8 -6
- package/lib/label-for-references-labelable/index.d.ts +8 -5
- package/lib/label-for-references-labelable/index.js +8 -5
- package/lib/landmark-roles/roles.js +14 -2
- package/lib/map-id-name-match/index.d.ts +3 -3
- package/lib/map-id-name-match/index.js +3 -3
- package/lib/no-ambiguous-navigable-target-names/index.js +2 -1
- package/lib/no-aria-hidden-on-hidden-until-found/index.d.ts +9 -6
- package/lib/no-aria-hidden-on-hidden-until-found/index.js +9 -6
- package/lib/no-aria-hidden-on-hidden-until-found/meta.d.ts +1 -1
- package/lib/no-aria-hidden-on-hidden-until-found/meta.js +1 -1
- package/lib/no-aria-on-unsupported-element/index.js +2 -2
- package/lib/no-aria-on-unsupported-element/meta.d.ts +1 -1
- package/lib/no-aria-on-unsupported-element/meta.js +1 -1
- package/lib/no-boolean-attr-value/index.js +2 -2
- package/lib/no-content-after-body/index.d.ts +2 -2
- package/lib/no-content-after-body/index.js +2 -2
- package/lib/no-contradictory-aria-prop/index.js +2 -1
- package/lib/no-default-aria-value/index.js +2 -1
- package/lib/no-default-value/index.js +2 -2
- package/lib/no-deprecated-aria-prop/index.js +2 -1
- package/lib/no-deprecated-attr/index.js +2 -1
- package/lib/no-disallowed-attr/index.d.ts +7 -1
- package/lib/no-disallowed-attr/index.js +13 -20
- package/lib/no-duplicate-attr/index.js +1 -2
- package/lib/no-duplicate-autofocus/index.js +2 -2
- package/lib/no-duplicate-h1/index.d.ts +1 -1
- package/lib/no-duplicate-h1/index.js +1 -1
- package/lib/no-empty-table-track/index.d.ts +6 -2
- package/lib/no-empty-table-track/index.js +6 -2
- package/lib/no-experimental-features/index.d.ts +5 -1
- package/lib/no-experimental-features/index.js +6 -8
- package/lib/no-hardcoded-id/index.js +1 -1
- package/lib/no-ineffective-attr/index.js +2 -2
- package/lib/no-input-file-value/index.d.ts +10 -4
- package/lib/no-input-file-value/index.js +10 -4
- package/lib/no-invalid-aria-prop-value/index.js +2 -1
- package/lib/no-invalid-attr-value/index.d.ts +3 -0
- package/lib/no-invalid-attr-value/index.js +9 -24
- package/lib/no-nonstandard-features/index.d.ts +5 -1
- package/lib/no-nonstandard-features/index.js +6 -8
- package/lib/no-obsolete-attr/index.js +3 -3
- package/lib/no-prohibited-naming/index.js +2 -1
- package/lib/no-redundant-accessible-name/index.js +17 -1
- package/lib/no-redundant-aria-prop/index.js +2 -1
- package/lib/no-refer-to-non-existent-id/index.js +4 -4
- package/lib/no-restricted-attr/index.d.ts +4 -0
- package/lib/no-restricted-attr/index.js +5 -22
- package/lib/no-stray-head-or-body-tag/index.d.ts +6 -4
- package/lib/no-stray-head-or-body-tag/index.js +11 -6
- package/lib/no-unknown-attr/index.d.ts +7 -1
- package/lib/no-unknown-attr/index.js +11 -25
- package/lib/no-unpaired-srcset-sizes/index.d.ts +3 -2
- package/lib/no-unpaired-srcset-sizes/index.js +23 -17
- package/lib/no-unsupported-browser-features/compat-data.d.ts +15 -2
- package/lib/no-unsupported-browser-features/compat-data.js +48 -3
- package/lib/no-unsupported-browser-features/index.d.ts +5 -1
- package/lib/no-unsupported-browser-features/index.js +6 -7
- package/lib/permitted-contents/choice.js +19 -8
- package/lib/permitted-contents/content-model.d.ts +24 -1
- package/lib/permitted-contents/content-model.js +56 -9
- package/lib/permitted-contents/count-pattern.js +36 -13
- package/lib/permitted-contents/debug.browser.d.ts +18 -0
- package/lib/permitted-contents/debug.browser.js +27 -0
- package/lib/permitted-contents/debug.d.ts +18 -0
- package/lib/permitted-contents/debug.js +27 -0
- package/lib/permitted-contents/index.js +2 -2
- package/lib/permitted-contents/matches-selector.js +40 -18
- package/lib/permitted-contents/order.js +23 -11
- package/lib/permitted-contents/represent-transparent-nodes.js +3 -1
- package/lib/permitted-contents/transparent.js +6 -2
- package/lib/placeholder-label-option/index.d.ts +3 -3
- package/lib/placeholder-label-option/index.js +163 -37
- package/lib/progress-value-bounds/index.d.ts +1 -1
- package/lib/progress-value-bounds/index.js +1 -1
- package/lib/require-attr/index.d.ts +6 -1
- package/lib/require-attr/index.js +13 -5
- package/lib/require-h1/index.d.ts +1 -1
- package/lib/require-h1/index.js +1 -1
- package/lib/role-supports-aria-prop/index.js +2 -1
- package/lib/sizes-auto-requires-lazy-loading/index.d.ts +3 -1
- package/lib/sizes-auto-requires-lazy-loading/index.js +30 -16
- package/lib/srcset-sizes-constraint/parse-srcset.d.ts +6 -5
- package/lib/srcset-sizes-constraint/parse-srcset.js +8 -7
- package/lib/srcset-sizes-constraint/sibling-helpers.d.ts +31 -0
- package/lib/srcset-sizes-constraint/sibling-helpers.js +58 -0
- package/lib/valid-speculation-rules/index.js +1 -1
- package/lib/wai-aria/checkings/contradictory-aria-prop.js +3 -5
- package/lib/wai-aria/checkings/default-value.js +1 -1
- package/lib/wai-aria/checkings/deprecated-props.js +2 -2
- package/lib/wai-aria/checkings/element-supports-aria-prop.js +5 -4
- package/lib/wai-aria/checkings/interaction-in-hidden.d.ts +1 -1
- package/lib/wai-aria/checkings/interaction-in-hidden.js +1 -1
- package/lib/wai-aria/checkings/no-global-prop.js +1 -1
- package/lib/wai-aria/checkings/presentational-children.d.ts +4 -4
- package/lib/wai-aria/checkings/presentational-children.js +6 -5
- package/lib/wai-aria/checkings/prohibited-naming.js +5 -4
- package/lib/wai-aria/checkings/redundant-aria-prop.js +3 -5
- package/lib/wai-aria/checkings/required-prop.js +1 -4
- package/lib/wai-aria/checkings/role-supports-aria-prop.js +4 -3
- package/lib/wai-aria/checkings/tab-requires-tabpanel.d.ts +3 -2
- package/lib/wai-aria/checkings/tab-requires-tabpanel.js +3 -2
- package/lib/wai-aria/checkings/value.d.ts +1 -1
- package/lib/wai-aria/checkings/value.js +2 -2
- package/package.json +19 -19
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRule, getAttrSpecs, ariaSpecs } from '@markuplint/ml-core';
|
|
2
2
|
import { ARIA_RECOMMENDED_VERSION } from '@markuplint/ml-spec';
|
|
3
3
|
import { decodeEntities } from '@markuplint/shared';
|
|
4
|
+
import { findAttrSpec } from '../helpers.js';
|
|
4
5
|
import meta from './meta.js';
|
|
5
6
|
export default createRule({
|
|
6
7
|
meta: meta,
|
|
@@ -37,12 +38,11 @@ export default createRule({
|
|
|
37
38
|
if (attr.isDynamicValue) {
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
-
if (name.toLowerCase() === 'id') {
|
|
41
|
+
if (attr.nameKey === 'id') {
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
const value = attr.value;
|
|
45
|
-
const spec = attrSpec
|
|
45
|
+
const spec = findAttrSpec(attrSpec, attr);
|
|
46
46
|
if (spec) {
|
|
47
47
|
// DOMID and DOMID List do not become in the array type.
|
|
48
48
|
if (Array.isArray(spec.type)) {
|
|
@@ -76,7 +76,7 @@ export default createRule({
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
const { props } = ariaSpecs(document.specs, attr.rule.options.ariaVersion ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION);
|
|
79
|
-
const aria = props.find(prop => prop.name ===
|
|
79
|
+
const aria = props.find(prop => prop.name === attr.nameKey);
|
|
80
80
|
if (aria) {
|
|
81
81
|
if (aria.value === 'ID reference' && !idList.has(value)) {
|
|
82
82
|
report({
|
|
@@ -10,6 +10,10 @@ type Option = {
|
|
|
10
10
|
* already restricts. This is the rule's only concern — narrow, purely
|
|
11
11
|
* user-defined denylisting, independent of spec conformance.
|
|
12
12
|
*
|
|
13
|
+
* A name is compared as the DOM compares attribute names, so it is ASCII case-insensitive in the
|
|
14
|
+
* HTML syntax and for an HTML element that code sets through the DOM API (JSX, MDX, Vue), exact for an
|
|
15
|
+
* SVG/MathML element there, and a component's props are not folded.
|
|
16
|
+
*
|
|
13
17
|
* @since 3.7.0
|
|
14
18
|
*/
|
|
15
19
|
disallowAttrs?: (string | Attr)[] | Record<string, AttributeType | Pattern>;
|
|
@@ -1,32 +1,15 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
2
|
import { check, isEnum, isPattern } from '@markuplint/types';
|
|
3
3
|
import { attrCheck } from '../attr-check.js';
|
|
4
|
+
import { createNameOptionLookup } from '../helpers.js';
|
|
4
5
|
import meta from './meta.js';
|
|
5
|
-
function disallowedTypes(disallowAttrs) {
|
|
6
|
-
const map = new Map();
|
|
7
|
-
if (!disallowAttrs) {
|
|
8
|
-
return map;
|
|
9
|
-
}
|
|
10
|
-
if (Array.isArray(disallowAttrs)) {
|
|
11
|
-
for (const disallowAttr of disallowAttrs) {
|
|
12
|
-
if (typeof disallowAttr === 'string') {
|
|
13
|
-
map.set(disallowAttr, 'Any');
|
|
14
|
-
continue;
|
|
15
|
-
}
|
|
16
|
-
map.set(disallowAttr.name, disallowAttr.value);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
20
|
-
for (const [name, type] of Object.entries(disallowAttrs)) {
|
|
21
|
-
map.set(name, type);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return map;
|
|
25
|
-
}
|
|
26
6
|
export default createRule({
|
|
27
7
|
meta: meta,
|
|
28
8
|
defaultOptions: {},
|
|
29
9
|
async verify({ document, report, t }) {
|
|
10
|
+
// Whether an entry of `disallowAttrs` is the attribute is up to the attribute: the DOM decides
|
|
11
|
+
// whether two spellings are one name (`MLAttr#isNamedBy`).
|
|
12
|
+
const lookupDisallowed = createNameOptionLookup('Any');
|
|
30
13
|
await document.walkOn('Attr', attr => {
|
|
31
14
|
if (attr.isDirective) {
|
|
32
15
|
return;
|
|
@@ -35,7 +18,7 @@ export default createRule({
|
|
|
35
18
|
const value = attr.value;
|
|
36
19
|
const valueNode = attr.valueNode;
|
|
37
20
|
const attrName = attr.nameNode;
|
|
38
|
-
const disallowValue =
|
|
21
|
+
const disallowValue = lookupDisallowed(attr.rule.options.disallowAttrs, attr);
|
|
39
22
|
if (disallowValue === undefined) {
|
|
40
23
|
return;
|
|
41
24
|
}
|
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
* > A start tag whose tag name is "body": Parse error. If the stack of open
|
|
5
5
|
* > elements has only one node on it, or if the second element on the stack
|
|
6
6
|
* > of open elements is not a body element, or if there is a template
|
|
7
|
-
* > element on the stack of open elements, then ignore the token […]
|
|
8
|
-
* > Otherwise
|
|
7
|
+
* > element on the stack of open elements, then ignore the token. […]
|
|
8
|
+
* > Otherwise: Set the frameset-ok flag to "not ok" […]
|
|
9
9
|
*
|
|
10
10
|
* "in body" insertion mode, "Any other end tag":
|
|
11
11
|
*
|
|
12
|
-
* > Initialize node to be the current node
|
|
13
|
-
* >
|
|
12
|
+
* > Initialize node to be the current node (the bottommost node of the
|
|
13
|
+
* > stack). Loop: If node is an HTML element with the same tag name as the
|
|
14
|
+
* > token: […] Otherwise, if node is in the special category, then this is a
|
|
15
|
+
* > parse error; ignore the token, and return.
|
|
14
16
|
*
|
|
15
17
|
* Neither of these fires by the presence of disallowed `<head>` content
|
|
16
18
|
* itself (the "in head" insertion mode's own "Anything else" clause that
|
|
@@ -5,9 +5,12 @@ import meta from './meta.js';
|
|
|
5
5
|
* "generic raw text element parsing algorithm" / "generic RCDATA element
|
|
6
6
|
* parsing algorithm"): the parser never re-tokenizes tag-shaped substrings
|
|
7
7
|
* inside it, so a literal `<body` or `</head` there is ordinary text, not a
|
|
8
|
-
* candidate for either check below.
|
|
8
|
+
* candidate for either check below. `iframe`, `noembed` and `noframes` are
|
|
9
|
+
* RAWTEXT too but `@markuplint/parser-utils` still tokenizes their content
|
|
10
|
+
* as markup (`permitted-contents` relies on that), so they are skipped here
|
|
11
|
+
* rather than in the parser (#4180).
|
|
9
12
|
*/
|
|
10
|
-
const RAW_TEXT_CONTAINERS = new Set(['script', 'style', 'title', 'textarea', 'xmp']);
|
|
13
|
+
const RAW_TEXT_CONTAINERS = new Set(['script', 'style', 'title', 'textarea', 'xmp', 'iframe', 'noembed', 'noframes']);
|
|
11
14
|
const STRAY_HEAD_END_TAG = /<\/head(?=[\s/>])/i;
|
|
12
15
|
const DUPLICATE_BODY_START_TAG = /<body(?=[\s/>])/i;
|
|
13
16
|
/**
|
|
@@ -16,13 +19,15 @@ const DUPLICATE_BODY_START_TAG = /<body(?=[\s/>])/i;
|
|
|
16
19
|
* > A start tag whose tag name is "body": Parse error. If the stack of open
|
|
17
20
|
* > elements has only one node on it, or if the second element on the stack
|
|
18
21
|
* > of open elements is not a body element, or if there is a template
|
|
19
|
-
* > element on the stack of open elements, then ignore the token […]
|
|
20
|
-
* > Otherwise
|
|
22
|
+
* > element on the stack of open elements, then ignore the token. […]
|
|
23
|
+
* > Otherwise: Set the frameset-ok flag to "not ok" […]
|
|
21
24
|
*
|
|
22
25
|
* "in body" insertion mode, "Any other end tag":
|
|
23
26
|
*
|
|
24
|
-
* > Initialize node to be the current node
|
|
25
|
-
* >
|
|
27
|
+
* > Initialize node to be the current node (the bottommost node of the
|
|
28
|
+
* > stack). Loop: If node is an HTML element with the same tag name as the
|
|
29
|
+
* > token: […] Otherwise, if node is in the special category, then this is a
|
|
30
|
+
* > parse error; ignore the token, and return.
|
|
26
31
|
*
|
|
27
32
|
* Neither of these fires by the presence of disallowed `<head>` content
|
|
28
33
|
* itself (the "in head" insertion mode's own "Anything else" clause that
|
|
@@ -9,10 +9,16 @@ type Option = {
|
|
|
9
9
|
* The value type is used by `no-invalid-attr-value` to validate the
|
|
10
10
|
* attribute's value; here it only determines whether the name is known.
|
|
11
11
|
*
|
|
12
|
+
* A name is compared as the DOM compares attribute names.
|
|
13
|
+
*
|
|
14
|
+
* @see https://markuplint.dev/docs/rules/no-unknown-attr#attribute-name-case
|
|
12
15
|
* @since 3.7.0
|
|
13
16
|
*/
|
|
14
17
|
allowAttrs?: (string | Attr)[] | Record<string, AttributeType | Pattern>;
|
|
15
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Attribute name prefix(es) to ignore during validation, compared as the DOM compares attribute
|
|
20
|
+
* names, like `allowAttrs`.
|
|
21
|
+
*/
|
|
16
22
|
ignoreAttrNamePrefix?: string | string[];
|
|
17
23
|
/** Whether to allow additional properties for pretender elements (defaults to `true`). */
|
|
18
24
|
allowToAddPropertiesForPretender?: boolean;
|
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import { createRule, getAttrSpecs, getSpec } from '@markuplint/ml-core';
|
|
2
2
|
import { resolveAttrEligibility, suggestionSuffix } from '../attr-eligibility.js';
|
|
3
|
+
import { createNameOptionLookup, createPrefixMatcher } from '../helpers.js';
|
|
3
4
|
import meta from './meta.js';
|
|
4
|
-
function allowedNames(allowAttrs) {
|
|
5
|
-
if (!allowAttrs) {
|
|
6
|
-
return new Set();
|
|
7
|
-
}
|
|
8
|
-
if (Array.isArray(allowAttrs)) {
|
|
9
|
-
return new Set(allowAttrs.map(a => (typeof a === 'string' ? a : a.name)));
|
|
10
|
-
}
|
|
11
|
-
return new Set(Object.keys(allowAttrs));
|
|
12
|
-
}
|
|
13
5
|
export default createRule({
|
|
14
6
|
meta: meta,
|
|
15
7
|
defaultOptions: {},
|
|
16
8
|
async verify({ document, report, t }) {
|
|
9
|
+
// Whether an entry of the options is the attribute is up to the attribute: the DOM decides
|
|
10
|
+
// whether two spellings are one name (`MLAttr#isNamedBy`, `MLAttr#isPrefixedBy`).
|
|
11
|
+
const lookupAllowed = createNameOptionLookup('Any');
|
|
12
|
+
const hasIgnoredPrefix = createPrefixMatcher();
|
|
17
13
|
await document.walkOn('Attr', attr => {
|
|
18
14
|
if (attr.isDirective) {
|
|
19
15
|
return;
|
|
@@ -31,19 +27,15 @@ export default createRule({
|
|
|
31
27
|
raw: attrName?.raw,
|
|
32
28
|
message: t('{0} is {1:c}', t('the "{0*}" {1}', name, 'attribute'), 'disallowed') +
|
|
33
29
|
t('. ') +
|
|
34
|
-
t('Did you mean "{0}"?', attr.candidate),
|
|
30
|
+
t('Did you mean "{0*}"?', attr.candidate),
|
|
35
31
|
});
|
|
36
32
|
return;
|
|
37
33
|
}
|
|
38
|
-
if (attr.rule.options.ignoreAttrNamePrefix
|
|
39
|
-
|
|
40
|
-
? attr.rule.options.ignoreAttrNamePrefix
|
|
41
|
-
: [attr.rule.options.ignoreAttrNamePrefix];
|
|
42
|
-
if (prefixes.some(prefix => name.indexOf(prefix) === 0)) {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
34
|
+
if (hasIgnoredPrefix(attr.rule.options.ignoreAttrNamePrefix, attr)) {
|
|
35
|
+
return;
|
|
45
36
|
}
|
|
46
|
-
|
|
37
|
+
// A name listed in `allowAttrs` is allowed whatever its value
|
|
38
|
+
if (lookupAllowed(attr.rule.options.allowAttrs, attr) !== undefined) {
|
|
47
39
|
return;
|
|
48
40
|
}
|
|
49
41
|
if (attr.ownerElement.elementType !== 'html') {
|
|
@@ -53,7 +45,7 @@ export default createRule({
|
|
|
53
45
|
if (!attrSpecs) {
|
|
54
46
|
return;
|
|
55
47
|
}
|
|
56
|
-
const eligibility = resolveAttrEligibility(name, attr.ownerElement, attrSpecs);
|
|
48
|
+
const eligibility = resolveAttrEligibility(name, attr.ownerElement, attrSpecs, attr);
|
|
57
49
|
let message;
|
|
58
50
|
switch (eligibility.status) {
|
|
59
51
|
case 'unknown': {
|
|
@@ -62,12 +54,6 @@ export default createRule({
|
|
|
62
54
|
suggestionSuffix(t, eligibility.candidate);
|
|
63
55
|
break;
|
|
64
56
|
}
|
|
65
|
-
case 'case-mismatch': {
|
|
66
|
-
message =
|
|
67
|
-
t('{0} is {1:c}', t('the "{0*}" {1}', name, 'attribute'), 'disallowed') +
|
|
68
|
-
suggestionSuffix(t, eligibility.expectedName);
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
57
|
default: {
|
|
72
58
|
return;
|
|
73
59
|
}
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* one constraint.
|
|
6
6
|
*
|
|
7
7
|
* `<source>`'s direction (Check 5) is conditional: `sizes` "may" be omitted
|
|
8
|
-
* when the following sibling `<img>`
|
|
9
|
-
*
|
|
8
|
+
* when the following sibling `<img>` allows auto-sizes (`loading="lazy"` and
|
|
9
|
+
* `sizes="auto"`; see `allowsAutoSizes`). `<img>`'s direction (Check 1) has no
|
|
10
|
+
* such escape.
|
|
10
11
|
*/
|
|
11
12
|
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, undefined>>;
|
|
12
13
|
export default _default;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
2
|
import { parseSrcset } from '../srcset-sizes-constraint/parse-srcset.js';
|
|
3
|
-
import { findFollowingImg } from '../srcset-sizes-constraint/sibling-helpers.js';
|
|
3
|
+
import { allowsAutoSizes, findFollowingImg, isLazyLoading } from '../srcset-sizes-constraint/sibling-helpers.js';
|
|
4
4
|
import meta from './meta.js';
|
|
5
5
|
/**
|
|
6
6
|
* HTML LS § img/source `srcset` attributes: `sizes` and a width-descriptor
|
|
@@ -9,8 +9,9 @@ import meta from './meta.js';
|
|
|
9
9
|
* one constraint.
|
|
10
10
|
*
|
|
11
11
|
* `<source>`'s direction (Check 5) is conditional: `sizes` "may" be omitted
|
|
12
|
-
* when the following sibling `<img>`
|
|
13
|
-
*
|
|
12
|
+
* when the following sibling `<img>` allows auto-sizes (`loading="lazy"` and
|
|
13
|
+
* `sizes="auto"`; see `allowsAutoSizes`). `<img>`'s direction (Check 1) has no
|
|
14
|
+
* such escape.
|
|
14
15
|
*/
|
|
15
16
|
export default createRule({
|
|
16
17
|
meta,
|
|
@@ -50,25 +51,30 @@ export default createRule({
|
|
|
50
51
|
}
|
|
51
52
|
// img / source-in-picture with w descriptors → sizes required.
|
|
52
53
|
//
|
|
53
|
-
// HTML LS § img srcset: unconditional —
|
|
54
|
-
// also be present
|
|
54
|
+
// HTML LS § img srcset: unconditional — the sizes attribute must
|
|
55
|
+
// also be present when any candidate uses a width descriptor.
|
|
55
56
|
//
|
|
56
|
-
// HTML LS § source: conditional — sizes
|
|
57
|
-
//
|
|
58
|
-
// auto
|
|
59
|
-
// loading
|
|
60
|
-
//
|
|
57
|
+
// HTML LS § source: conditional — sizes may be omitted only when
|
|
58
|
+
// the following sibling img allows auto-sizes (loading=lazy and
|
|
59
|
+
// sizes=auto; the same condition is the second source of truth for
|
|
60
|
+
// `sizes-auto-requires-lazy-loading`).
|
|
61
|
+
//
|
|
62
|
+
// @see https://html.spec.whatwg.org/multipage/embedded-content.html#allows-auto-sizes
|
|
61
63
|
if (parsed && parsed.hasWidth && !sizesAttr) {
|
|
62
64
|
// For `<source>`, `findFollowingImg` may return null (malformed
|
|
63
|
-
// <picture> with no img)
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
// <picture> with no img): an absent img cannot supply
|
|
66
|
+
// auto-sizes, so sizes is required (fail-closed). A dynamic
|
|
67
|
+
// `loading` / `sizes` on the img is not known → not reported.
|
|
68
|
+
const img = localName === 'source' ? findFollowingImg(el) : null;
|
|
69
|
+
if (!img || allowsAutoSizes(img) === false) {
|
|
70
|
+
// A lazy img that is not auto-sized is the case a reader
|
|
71
|
+
// least expects to be rejected: name what would allow it.
|
|
72
|
+
const needsImgSizes = img != null && isLazyLoading(img) === true;
|
|
69
73
|
report({
|
|
70
74
|
scope: el,
|
|
71
|
-
message:
|
|
75
|
+
message: needsImgSizes
|
|
76
|
+
? 'The "sizes" attribute is required when the "srcset" attribute uses width descriptors, unless the following sibling "img" element has loading="lazy" and sizes="auto"'
|
|
77
|
+
: 'The "sizes" attribute is required when the "srcset" attribute uses width descriptors',
|
|
72
78
|
});
|
|
73
79
|
}
|
|
74
80
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { PlainData } from '@markuplint/ml-config';
|
|
2
|
+
import type { Attr, RuleConfigValue } from '@markuplint/ml-core';
|
|
1
3
|
import type { BrowserName, SupportStatement } from '@mdn/browser-compat-data';
|
|
2
4
|
/**
|
|
3
5
|
* Target browser with name and minimum version.
|
|
@@ -60,12 +62,23 @@ export declare function checkSupport(support: SupportStatement | undefined, targ
|
|
|
60
62
|
* @returns Array of unsupported results (empty if all supported)
|
|
61
63
|
*/
|
|
62
64
|
export declare function checkElementSupport(elementName: string, targets: readonly TargetBrowser[]): Promise<readonly UnsupportedResult[]>;
|
|
65
|
+
/**
|
|
66
|
+
* What the lookup of an attribute asks of the attribute: its name as the DOM gives it, and the name
|
|
67
|
+
* the DOM would give a key of BCD (`MLAttr#nameKey`, `MLAttr#keyOf`).
|
|
68
|
+
*
|
|
69
|
+
* It does not ask `MLAttr#isNamedBy`: that reads a name with a capital letter as the prop of a
|
|
70
|
+
* framework (`defaultValue`) and matches only its exact spelling, but a key of BCD is the spelling of
|
|
71
|
+
* an attribute of the browser, which the DOM folds (`privateToken` is `privatetoken`).
|
|
72
|
+
*/
|
|
73
|
+
export type CompatAttr = Pick<Attr<RuleConfigValue, PlainData>, 'nameKey' | 'keyOf'>;
|
|
63
74
|
/**
|
|
64
75
|
* Check browser support for an HTML attribute on a specific element.
|
|
65
76
|
*
|
|
66
77
|
* @param elementName - The HTML element name (e.g., "input")
|
|
67
|
-
* @param attrName - The attribute name (e.g., "list")
|
|
78
|
+
* @param attrName - The attribute name (e.g., "list"), as written
|
|
68
79
|
* @param targets - Array of target browsers to check
|
|
80
|
+
* @param attr - The attribute, which decides whether a key of BCD that is spelled in another case is
|
|
81
|
+
* the attribute (the DOM folds a name that the HTML parser reads, and not a prop of a framework)
|
|
69
82
|
* @returns Array of unsupported results (empty if all supported)
|
|
70
83
|
*/
|
|
71
|
-
export declare function checkAttributeSupport(elementName: string, attrName: string, targets: readonly TargetBrowser[]): Promise<readonly UnsupportedResult[]>;
|
|
84
|
+
export declare function checkAttributeSupport(elementName: string, attrName: string, targets: readonly TargetBrowser[], attr?: CompatAttr): Promise<readonly UnsupportedResult[]>;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { asciiLowerCase } from '@markuplint/shared';
|
|
2
|
+
import { memoizeByObject } from '../helpers.js';
|
|
1
3
|
/**
|
|
2
4
|
* Mapping from browserslist browser names to BCD browser names.
|
|
3
5
|
*/
|
|
@@ -187,20 +189,63 @@ export async function checkElementSupport(elementName, targets) {
|
|
|
187
189
|
}
|
|
188
190
|
return results;
|
|
189
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* The keys of an element identifier by the key in ASCII lower case. BCD spells a few keys in camel
|
|
194
|
+
* case (`iframe.privateToken`), and the DOM can make a written name one of them (`PRIVATETOKEN` that
|
|
195
|
+
* the HTML parser folds), so the keys that a written name can be are found by this index and then
|
|
196
|
+
* asked of the attribute. It is built once per identifier, which BCD hands out as one object.
|
|
197
|
+
*/
|
|
198
|
+
const foldedKeysOf = memoizeByObject((identifier) => {
|
|
199
|
+
const folded = new Map();
|
|
200
|
+
for (const key of Object.keys(identifier)) {
|
|
201
|
+
if (key === '__compat') {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const lower = asciiLowerCase(key);
|
|
205
|
+
const keys = folded.get(lower);
|
|
206
|
+
if (keys) {
|
|
207
|
+
keys.push(key);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
folded.set(lower, [key]);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return folded;
|
|
214
|
+
});
|
|
215
|
+
/**
|
|
216
|
+
* Finds the identifier of an attribute in the identifier of its element. The key as written comes
|
|
217
|
+
* first. With the attribute, another key is the attribute when the attribute says its name is that
|
|
218
|
+
* key; without it, only the name in lower case is tried, as there is nothing to ask.
|
|
219
|
+
*/
|
|
220
|
+
function findAttrIdentifier(identifier, attrName, attr) {
|
|
221
|
+
const exact = identifier[attrName];
|
|
222
|
+
if (exact?.__compat) {
|
|
223
|
+
return exact;
|
|
224
|
+
}
|
|
225
|
+
if (!attr) {
|
|
226
|
+
return identifier[asciiLowerCase(attrName)];
|
|
227
|
+
}
|
|
228
|
+
const key = foldedKeysOf(identifier)
|
|
229
|
+
.get(asciiLowerCase(attr.nameKey))
|
|
230
|
+
?.find(key => attr.keyOf(key) === attr.nameKey);
|
|
231
|
+
return key === undefined ? undefined : identifier[key];
|
|
232
|
+
}
|
|
190
233
|
/**
|
|
191
234
|
* Check browser support for an HTML attribute on a specific element.
|
|
192
235
|
*
|
|
193
236
|
* @param elementName - The HTML element name (e.g., "input")
|
|
194
|
-
* @param attrName - The attribute name (e.g., "list")
|
|
237
|
+
* @param attrName - The attribute name (e.g., "list"), as written
|
|
195
238
|
* @param targets - Array of target browsers to check
|
|
239
|
+
* @param attr - The attribute, which decides whether a key of BCD that is spelled in another case is
|
|
240
|
+
* the attribute (the DOM folds a name that the HTML parser reads, and not a prop of a framework)
|
|
196
241
|
* @returns Array of unsupported results (empty if all supported)
|
|
197
242
|
*/
|
|
198
|
-
export async function checkAttributeSupport(elementName, attrName, targets) {
|
|
243
|
+
export async function checkAttributeSupport(elementName, attrName, targets, attr) {
|
|
199
244
|
const identifier = await getElementIdentifier(elementName);
|
|
200
245
|
if (!identifier) {
|
|
201
246
|
return [];
|
|
202
247
|
}
|
|
203
|
-
const attrIdentifier = identifier
|
|
248
|
+
const attrIdentifier = findAttrIdentifier(identifier, attrName, attr);
|
|
204
249
|
if (!attrIdentifier?.__compat) {
|
|
205
250
|
return [];
|
|
206
251
|
}
|
|
@@ -8,7 +8,11 @@ type Options = {
|
|
|
8
8
|
readonly browserslistConfig?: string;
|
|
9
9
|
/** Browserslist environment name (e.g., "production"). */
|
|
10
10
|
readonly browserslistEnv?: string;
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Features to ignore: an element name ("dialog") or an attribute of an element ("input[list]"). The
|
|
13
|
+
* element name is compared as the document compares tag names, the attribute name as the DOM
|
|
14
|
+
* compares it with the attribute that is checked.
|
|
15
|
+
*/
|
|
12
16
|
readonly ignoreFeatures?: readonly string[];
|
|
13
17
|
};
|
|
14
18
|
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, Options>>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
+
import { createIgnoreFeaturesLookup } from '../helpers.js';
|
|
2
3
|
import { checkAttributeSupport, checkElementSupport } from './compat-data.js';
|
|
3
4
|
import meta from './meta.js';
|
|
4
5
|
import { clearBrowserslistCache, resolveTargetBrowsers } from './resolve-browsers.js';
|
|
@@ -17,7 +18,7 @@ export default createRule({
|
|
|
17
18
|
if (targetBrowsers == null || targetBrowsers.length === 0) {
|
|
18
19
|
return;
|
|
19
20
|
}
|
|
20
|
-
const
|
|
21
|
+
const ignored = createIgnoreFeaturesLookup(document.tagNameCaseSensitive, options.ignoreFeatures);
|
|
21
22
|
await document.walkOn('Element', async (el) => {
|
|
22
23
|
// Web components and authored elements (JSX/Vue/Svelte) reach this rule
|
|
23
24
|
// via `pretenders`: they masquerade as a known HTML element on `el.localName`,
|
|
@@ -27,9 +28,8 @@ export default createRule({
|
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
29
30
|
const elName = el.localName;
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
if (!elIgnoreFeatures.has(elName)) {
|
|
31
|
+
const elIgnoreFeatures = el.rule.options.ignoreFeatures;
|
|
32
|
+
if (!ignored.isElementIgnored(elIgnoreFeatures, elName)) {
|
|
33
33
|
const unsupported = await checkElementSupport(elName, targetBrowsers);
|
|
34
34
|
if (unsupported.length > 0) {
|
|
35
35
|
report({
|
|
@@ -43,11 +43,10 @@ export default createRule({
|
|
|
43
43
|
continue;
|
|
44
44
|
}
|
|
45
45
|
const attrName = attr.name;
|
|
46
|
-
|
|
47
|
-
if (elIgnoreFeatures.has(ignoreKey)) {
|
|
46
|
+
if (ignored.isAttrIgnored(elIgnoreFeatures, elName, attr)) {
|
|
48
47
|
continue;
|
|
49
48
|
}
|
|
50
|
-
const unsupported = await checkAttributeSupport(elName, attrName, targetBrowsers);
|
|
49
|
+
const unsupported = await checkAttributeSupport(elName, attrName, targetBrowsers, attr);
|
|
51
50
|
if (unsupported.length > 0) {
|
|
52
51
|
report({
|
|
53
52
|
scope: attr,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { bgBlue, bgGreen,
|
|
1
|
+
import { bgBlue, bgGreen, depthLog } from './debug.js';
|
|
2
2
|
import { order } from './order.js';
|
|
3
3
|
import { Collection, modelLog } from './utils.js';
|
|
4
4
|
/**
|
|
@@ -29,17 +29,22 @@ const indexes = new WeakMap();
|
|
|
29
29
|
export function choice(pattern,
|
|
30
30
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
31
31
|
childNodes, rules, specs, options, depth, mode) {
|
|
32
|
-
const choiceLog =
|
|
32
|
+
const choiceLog = depthLog('choice', depth);
|
|
33
|
+
const choiceEnabled = choiceLog.enabled;
|
|
33
34
|
const collection = new Collection(childNodes);
|
|
34
35
|
const unmatchedResults = [];
|
|
35
36
|
let i = 0;
|
|
36
37
|
for (const some of pattern.choice) {
|
|
37
|
-
|
|
38
|
+
if (choiceEnabled) {
|
|
39
|
+
choiceLog('Patterns[%s]: %s', i, modelLog(some, ''));
|
|
40
|
+
}
|
|
38
41
|
const result = order(some, collection.unmatched, rules, specs, options, depth + 1, mode);
|
|
39
42
|
if (result.type === 'MATCHED' ||
|
|
40
43
|
result.type === 'MATCHED_ZERO' ||
|
|
41
44
|
(result.type === 'UNEXPECTED_EXTRA_NODE' && result.matched.length > 0)) {
|
|
42
|
-
|
|
45
|
+
if (choiceEnabled) {
|
|
46
|
+
choiceLog('Results[%s]: %s', i, choiceLogString(pattern.choice, i));
|
|
47
|
+
}
|
|
43
48
|
return {
|
|
44
49
|
type: result.type,
|
|
45
50
|
matched: result.matched,
|
|
@@ -51,7 +56,11 @@ childNodes, rules, specs, options, depth, mode) {
|
|
|
51
56
|
}
|
|
52
57
|
unmatchedResults.push(result);
|
|
53
58
|
collection.addMatched(result.matched);
|
|
54
|
-
indexes.
|
|
59
|
+
// Log-only bookkeeping. The `indexes.get()` below is guarded by the same `choiceEnabled`
|
|
60
|
+
// snapshot, so the two stay in step.
|
|
61
|
+
if (choiceEnabled) {
|
|
62
|
+
indexes.set(result, i);
|
|
63
|
+
}
|
|
55
64
|
i++;
|
|
56
65
|
}
|
|
57
66
|
const barelyMatchedResult = unmatchedResults.toSorted((a, b) => {
|
|
@@ -75,9 +84,11 @@ childNodes, rules, specs, options, depth, mode) {
|
|
|
75
84
|
if (!barelyMatchedResult) {
|
|
76
85
|
throw new Error('Unreachable code');
|
|
77
86
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
if (choiceEnabled) {
|
|
88
|
+
const index = indexes.get(barelyMatchedResult);
|
|
89
|
+
if (index != null) {
|
|
90
|
+
choiceLog('Results[%s]: %s', index, choiceLogString(pattern.choice, index, true));
|
|
91
|
+
}
|
|
81
92
|
}
|
|
82
93
|
return {
|
|
83
94
|
type: barelyMatchedResult.type,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ContentModelResult, Element, Mode, Options, Specs, TagRule } from './types.js';
|
|
2
|
-
import type { ContentModel } from '@markuplint/ml-spec';
|
|
2
|
+
import type { ContentModel, MLMLSpec } from '@markuplint/ml-spec';
|
|
3
3
|
import type { ReadonlyDeep } from 'type-fest';
|
|
4
4
|
/**
|
|
5
5
|
* Top-level entry point for content model validation of a single element.
|
|
@@ -30,3 +30,26 @@ export declare function contentModel(el: Element, rules: readonly TagRule[], opt
|
|
|
30
30
|
* resolution logic when recursing into child content models.
|
|
31
31
|
*/
|
|
32
32
|
export declare function resolveContentModel(el: Element, rules: readonly TagRule[], specs: Specs, mode: Mode): ReadonlyDeep<ContentModel['contents']> | null;
|
|
33
|
+
/**
|
|
34
|
+
* Whether the user's tag rule is written for the element: the name of the rule is the name
|
|
35
|
+
* the element is written with, or another spelling of it that the document resolves to the
|
|
36
|
+
* same component (see `MLElement#matchesTagNameVariant`).
|
|
37
|
+
*
|
|
38
|
+
* @param el - The element
|
|
39
|
+
* @param rule - The user's tag rule
|
|
40
|
+
* @returns Whether the rule is written for the element
|
|
41
|
+
*/
|
|
42
|
+
export declare function hasTagRule(el: Element, rule: TagRule): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Returns a Specs object that merges the base ML spec with user-defined
|
|
45
|
+
* tag rules. Results are cached by the specs of the document and the serialized rules to avoid redundant
|
|
46
|
+
* merging: the specs differ by document (the `specs` of the config is by file), so what is merged
|
|
47
|
+
* for one must not be given to another.
|
|
48
|
+
*
|
|
49
|
+
* Exported for the spec of the cache.
|
|
50
|
+
*
|
|
51
|
+
* @param specs - The base ML spec from the document.
|
|
52
|
+
* @param rules - User-defined tag rules to merge.
|
|
53
|
+
* @returns The merged Specs, either from cache or freshly computed.
|
|
54
|
+
*/
|
|
55
|
+
export declare function cachedSpecs(specs: MLMLSpec, rules: readonly TagRule[]): Specs;
|