@markuplint/rules 5.3.0 → 5.5.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 +28 -0
- package/CLAUDE.md +4 -3
- package/lib/aria-prop-requires-role/index.js +2 -2
- package/lib/attr-eligibility.d.ts +21 -0
- package/lib/attr-eligibility.js +86 -6
- package/lib/attr-order/index.js +2 -1
- package/lib/attr-value-quotes/index.js +1 -1
- package/lib/consistent-table-row-length/index.js +1 -1
- package/lib/element-supports-aria-prop/index.js +2 -2
- package/lib/form-attr-references-form/index.js +2 -2
- package/lib/helpers.d.ts +43 -5
- package/lib/helpers.js +88 -26
- package/lib/input-list-references-datalist/index.js +2 -2
- package/lib/itemprop-requires-itemscope/index.js +27 -10
- package/lib/label-for-references-labelable/index.js +2 -2
- package/lib/label-has-control/index.d.ts +17 -0
- package/lib/label-has-control/index.js +28 -1
- package/lib/label-no-multiple-controls/index.d.ts +4 -5
- package/lib/label-no-multiple-controls/index.js +6 -6
- package/lib/landmark-roles/roles.d.ts +13 -1
- package/lib/landmark-roles/roles.js +22 -0
- package/lib/no-ambiguous-navigable-target-names/index.js +4 -0
- package/lib/no-aria-on-presentational-children/index.js +7 -1
- package/lib/no-aria-on-unsupported-element/index.js +2 -2
- package/lib/no-boolean-attr-value/index.js +2 -1
- package/lib/no-broken-fragment-link/index.d.ts +5 -0
- package/lib/no-broken-fragment-link/index.js +8 -2
- package/lib/no-consecutive-br/index.js +1 -1
- package/lib/no-contradictory-aria-prop/index.js +2 -2
- package/lib/no-default-aria-value/index.js +2 -2
- package/lib/no-default-value/index.js +4 -0
- package/lib/no-deprecated-aria-prop/index.js +2 -2
- package/lib/no-disallowed-ancestor/index.d.ts +16 -1
- package/lib/no-disallowed-ancestor/index.js +78 -4
- package/lib/no-disallowed-attr/index.js +4 -0
- package/lib/no-duplicate-dt/index.js +1 -1
- package/lib/no-duplicate-h1/index.js +18 -14
- package/lib/no-duplicate-id/index.d.ts +9 -0
- package/lib/no-duplicate-id/index.js +23 -4
- package/lib/no-empty-table-track/index.js +1 -1
- package/lib/no-ineffective-attr/index.js +4 -0
- package/lib/no-invalid-aria-prop-value/index.js +3 -3
- package/lib/no-invalid-attr-value/index.js +12 -0
- package/lib/no-nested-top-level-landmark/index.js +20 -17
- package/lib/no-prohibited-naming/index.js +2 -2
- package/lib/no-redundant-accessible-name/index.js +8 -7
- package/lib/no-redundant-aria-prop/index.js +2 -2
- package/lib/no-refer-to-non-existent-id/index.d.ts +15 -0
- package/lib/no-refer-to-non-existent-id/index.js +47 -16
- package/lib/no-restricted-element/index.js +4 -2
- package/lib/no-skipped-heading-level/index.js +9 -4
- package/lib/no-stray-head-or-body-tag/index.d.ts +2 -1
- package/lib/no-stray-head-or-body-tag/index.js +2 -1
- package/lib/no-table-cell-overlap/index.js +1 -1
- package/lib/no-table-span-overflow/index.js +1 -1
- package/lib/no-unescaped-char/index.js +1 -1
- package/lib/permitted-contents/index.js +2 -4
- package/lib/permitted-contents/matches-selector.js +28 -5
- package/lib/permitted-contents/represent-transparent-nodes.js +100 -68
- package/lib/permitted-contents/slot-content.d.ts +2 -1
- package/lib/permitted-contents/slot-content.js +28 -2
- package/lib/permitted-contents/start.js +4 -2
- package/lib/permitted-contents/utils.d.ts +11 -0
- package/lib/permitted-contents/utils.js +15 -0
- package/lib/placeholder-label-option/index.js +4 -26
- package/lib/require-accessible-name/index.js +2 -2
- package/lib/require-adjacent-popover/index.js +4 -2
- package/lib/require-ancestor/index.d.ts +20 -0
- package/lib/require-ancestor/index.js +33 -7
- package/lib/require-datetime/index.js +1 -1
- package/lib/require-dialog-autofocus/index.js +8 -6
- package/lib/require-h1/index.js +4 -8
- package/lib/require-landmark-label/index.js +17 -15
- package/lib/role-supports-aria-prop/index.js +2 -2
- package/lib/sizes-auto-requires-lazy-loading/index.d.ts +4 -2
- package/lib/sizes-auto-requires-lazy-loading/index.js +4 -2
- package/lib/usemap-references-map/index.js +11 -2
- package/lib/valid-speculation-rules/index.js +1 -1
- package/lib/wai-aria/checkings/interaction-in-hidden.js +6 -2
- package/lib/wai-aria/checkings/presentational-children.d.ts +17 -6
- package/lib/wai-aria/checkings/presentational-children.js +259 -25
- package/lib/wai-aria/checkings/required-accessibility-parent-role.js +2 -1
- package/lib/wai-aria/checkings/required-prop.js +2 -1
- package/lib/wai-aria/checkings/tab-requires-tabpanel.js +10 -6
- package/lib/wai-aria/checkings/value.d.ts +1 -4
- package/lib/wai-aria/checkings/value.js +5 -17
- package/package.json +11 -11
|
@@ -2,7 +2,7 @@ import { createRule, getComputedRole, ariaSpecs, getSpec } from '@markuplint/ml-
|
|
|
2
2
|
import { ARIA_RECOMMENDED_VERSION } from '@markuplint/ml-spec';
|
|
3
3
|
import { checkingValue } from '../wai-aria/checkings/value.js';
|
|
4
4
|
import { defaultOptions } from '../wai-aria/default-options.js';
|
|
5
|
-
import {
|
|
5
|
+
import { getAriaAttrs } from '../helpers.js';
|
|
6
6
|
import meta from './meta.js';
|
|
7
7
|
export default createRule({
|
|
8
8
|
meta,
|
|
@@ -14,14 +14,14 @@ export default createRule({
|
|
|
14
14
|
return;
|
|
15
15
|
if (!elSpec.globalAttrs['#ARIAAttrs'])
|
|
16
16
|
return;
|
|
17
|
-
const propAttrs = el
|
|
17
|
+
const propAttrs = getAriaAttrs(el);
|
|
18
18
|
if (propAttrs.length === 0)
|
|
19
19
|
return;
|
|
20
20
|
const ariaVersion = el.rule.options?.version ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
21
21
|
const computed = getComputedRole(document.specs, el, ariaVersion);
|
|
22
22
|
const { props: propSpecs } = ariaSpecs(document.specs, ariaVersion);
|
|
23
23
|
for (const attr of propAttrs) {
|
|
24
|
-
report(checkingValue({ attr, role: computed.role, propSpecs
|
|
24
|
+
report(checkingValue({ attr, role: computed.role, propSpecs }));
|
|
25
25
|
}
|
|
26
26
|
});
|
|
27
27
|
},
|
|
@@ -53,6 +53,18 @@ export default createRule({
|
|
|
53
53
|
if (one === false || one.invalidType !== 'invalid-value') {
|
|
54
54
|
continue;
|
|
55
55
|
}
|
|
56
|
+
// The value of an expression is what the code results in, so a position in it is not a position
|
|
57
|
+
// in the source (an escape, a quote): the whole value that is written is reported
|
|
58
|
+
if (attr.isExpressionValue && valueNode) {
|
|
59
|
+
report({
|
|
60
|
+
scope: attr,
|
|
61
|
+
message: one.message,
|
|
62
|
+
line: valueNode.startLine,
|
|
63
|
+
col: valueNode.startCol,
|
|
64
|
+
raw: valueNode.raw,
|
|
65
|
+
});
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
56
68
|
report({
|
|
57
69
|
scope: attr,
|
|
58
70
|
message: one.message,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
-
import {
|
|
2
|
+
import { computeLandmarkRolesOfEachTree, landmarkRoleElementUUIDList } from '../landmark-roles/roles.js';
|
|
3
3
|
import meta from './meta.js';
|
|
4
4
|
/**
|
|
5
5
|
* Roles this rule requires to be top-level landmarks — not nested inside any
|
|
@@ -29,22 +29,25 @@ export default createRule({
|
|
|
29
29
|
if (document.isFragment) {
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
for (const
|
|
35
|
-
const
|
|
36
|
-
for (const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
32
|
+
// Landmarks are nested in a landmark of their own tree: the content of a `template` element is
|
|
33
|
+
// a tree of its own, so a landmark outside of it does not make what is in it nested
|
|
34
|
+
for (const roles of computeLandmarkRolesOfEachTree(document)) {
|
|
35
|
+
const uuidList = landmarkRoleElementUUIDList(roles);
|
|
36
|
+
for (const role of topLevelRoles) {
|
|
37
|
+
const elements = roles[role];
|
|
38
|
+
for (const el of elements) {
|
|
39
|
+
if (el.rule.disabled) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (el.rule.options.ignoreRoles?.includes(role)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (el.isDescendantByUUIDList(uuidList)) {
|
|
46
|
+
report({
|
|
47
|
+
scope: el,
|
|
48
|
+
message: t('{0} should be {1}', t('the "{0*}" {1}', role, 'role'), 'top level'),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
}
|
|
@@ -2,7 +2,7 @@ import { createRule, getComputedRole, ariaSpecs, getSpec } from '@markuplint/ml-
|
|
|
2
2
|
import { ARIA_RECOMMENDED_VERSION } from '@markuplint/ml-spec';
|
|
3
3
|
import { checkingProhibitedNaming } from '../wai-aria/checkings/prohibited-naming.js';
|
|
4
4
|
import { defaultOptions } from '../wai-aria/default-options.js';
|
|
5
|
-
import {
|
|
5
|
+
import { getAriaAttrs } from '../helpers.js';
|
|
6
6
|
import meta from './meta.js';
|
|
7
7
|
/**
|
|
8
8
|
* Split from the former `wai-aria-disallowed-props` rule (#3989): the naming-
|
|
@@ -23,7 +23,7 @@ export default createRule({
|
|
|
23
23
|
return;
|
|
24
24
|
if (elSpec && !elSpec.globalAttrs['#ARIAAttrs'])
|
|
25
25
|
return;
|
|
26
|
-
const propAttrs = el
|
|
26
|
+
const propAttrs = getAriaAttrs(el);
|
|
27
27
|
if (propAttrs.length === 0)
|
|
28
28
|
return;
|
|
29
29
|
const ariaVersion = el.rule.options?.version ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRule, getComputedRole } from '@markuplint/ml-core';
|
|
2
2
|
import { ARIA_RECOMMENDED_VERSION, isExposed } from '@markuplint/ml-spec';
|
|
3
|
-
import { accnameMayBeMutable, getOwnedLabel } from '../helpers.js';
|
|
3
|
+
import { accnameMayBeMutable, getElementByIdInTree, getOwnedLabel } from '../helpers.js';
|
|
4
4
|
import meta from './meta.js';
|
|
5
5
|
export default createRule({
|
|
6
6
|
meta,
|
|
@@ -13,7 +13,7 @@ export default createRule({
|
|
|
13
13
|
await document.walkOn('Element', el => {
|
|
14
14
|
const ariaVersion = ARIA_RECOMMENDED_VERSION;
|
|
15
15
|
// Skip mutable elements (template expressions etc.)
|
|
16
|
-
if (accnameMayBeMutable(el
|
|
16
|
+
if (accnameMayBeMutable(el)) {
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
19
|
// Skip hidden elements
|
|
@@ -61,13 +61,14 @@ computed) {
|
|
|
61
61
|
//
|
|
62
62
|
// Known limitation: step 2C (Embedded Control) is not modeled, so a self-referencing
|
|
63
63
|
// textbox, combobox, listbox or range is treated like any other element.
|
|
64
|
-
const
|
|
65
|
-
|
|
64
|
+
const ariaLabelledbyAttr = el.getAttributeNode('aria-labelledby');
|
|
65
|
+
const ariaLabelledby = ariaLabelledbyAttr?.value;
|
|
66
|
+
if (ariaLabelledbyAttr && ariaLabelledby && ariaLabelledby.trim()) {
|
|
66
67
|
const ids = ariaLabelledby.trim().split(/\s+/);
|
|
67
68
|
// An IDREF resolves to the first element with that id, so with duplicate ids a
|
|
68
69
|
// reference only counts as self-referencing when this element is that first match.
|
|
69
|
-
const isSelfReferencing = ids.some(id =>
|
|
70
|
-
const hasResolvableId = ids.some(id =>
|
|
70
|
+
const isSelfReferencing = ids.some(id => getElementByIdInTree(el, id, ariaLabelledbyAttr.isExpressionValue) === el);
|
|
71
|
+
const hasResolvableId = ids.some(id => getElementByIdInTree(el, id, ariaLabelledbyAttr.isExpressionValue));
|
|
71
72
|
if (hasResolvableId && !isSelfReferencing) {
|
|
72
73
|
sources.push({ label: 'aria-labelledby' });
|
|
73
74
|
}
|
|
@@ -78,7 +79,7 @@ computed) {
|
|
|
78
79
|
sources.push({ label: 'aria-label' });
|
|
79
80
|
}
|
|
80
81
|
// 3. label (explicit or implicit) — only for labelable elements
|
|
81
|
-
const ownedLabel = getOwnedLabel(el
|
|
82
|
+
const ownedLabel = getOwnedLabel(el);
|
|
82
83
|
if (ownedLabel) {
|
|
83
84
|
sources.push({ label: 'label' });
|
|
84
85
|
}
|
|
@@ -2,7 +2,7 @@ import { createRule, ariaSpecs, getAttrSpecs, getSpec } from '@markuplint/ml-cor
|
|
|
2
2
|
import { ARIA_RECOMMENDED_VERSION } from '@markuplint/ml-spec';
|
|
3
3
|
import { checkingRedundantAriaProp } from '../wai-aria/checkings/redundant-aria-prop.js';
|
|
4
4
|
import { defaultOptions } from '../wai-aria/default-options.js';
|
|
5
|
-
import {
|
|
5
|
+
import { getAriaAttrs } from '../helpers.js';
|
|
6
6
|
import meta from './meta.js';
|
|
7
7
|
/**
|
|
8
8
|
* Split from the former `wai-aria-implicit-props` rule (#3989): the
|
|
@@ -22,7 +22,7 @@ export default createRule({
|
|
|
22
22
|
return;
|
|
23
23
|
if (!elSpec.globalAttrs['#ARIAAttrs'])
|
|
24
24
|
return;
|
|
25
|
-
const propAttrs = el
|
|
25
|
+
const propAttrs = getAriaAttrs(el);
|
|
26
26
|
if (propAttrs.length === 0)
|
|
27
27
|
return;
|
|
28
28
|
const ariaVersion = el.rule.options?.version ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import type { ARIAVersion } from '@markuplint/ml-spec';
|
|
2
|
+
/**
|
|
3
|
+
* Reports a reference (an attribute of the `DOMID` type, or an ARIA property of the ID reference
|
|
4
|
+
* type) to an id that does not exist.
|
|
5
|
+
*
|
|
6
|
+
* An id is resolved in the tree of the reference: the document, the content of a `template` element
|
|
7
|
+
* and a declarative shadow root are trees of their own. The reference and the id are compared with
|
|
8
|
+
* the character references in the markup resolved.
|
|
9
|
+
*
|
|
10
|
+
* What it cannot tell it does not report: a document with a template-language block is skipped, an id
|
|
11
|
+
* that is not known until it is rendered (a dynamic value) suppresses the report in its tree only, and
|
|
12
|
+
* a `template` that is in no tree (a declarative shadow root is attached to its parent, or may be)
|
|
13
|
+
* refers to nothing.
|
|
14
|
+
*
|
|
15
|
+
* @see https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute
|
|
16
|
+
*/
|
|
2
17
|
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<import("@markuplint/ml-core").RuleConfigValue, {
|
|
3
18
|
ariaVersion: ARIAVersion | undefined;
|
|
4
19
|
}>>;
|
|
@@ -1,35 +1,55 @@
|
|
|
1
1
|
import { createRule, getAttrSpecs, ariaSpecs } from '@markuplint/ml-core';
|
|
2
2
|
import { ARIA_RECOMMENDED_VERSION } from '@markuplint/ml-spec';
|
|
3
|
-
import {
|
|
4
|
-
import { findAttrSpec } from '../helpers.js';
|
|
3
|
+
import { findAttrSpec, getDOMValue } from '../helpers.js';
|
|
5
4
|
import meta from './meta.js';
|
|
5
|
+
/**
|
|
6
|
+
* Reports a reference (an attribute of the `DOMID` type, or an ARIA property of the ID reference
|
|
7
|
+
* type) to an id that does not exist.
|
|
8
|
+
*
|
|
9
|
+
* An id is resolved in the tree of the reference: the document, the content of a `template` element
|
|
10
|
+
* and a declarative shadow root are trees of their own. The reference and the id are compared with
|
|
11
|
+
* the character references in the markup resolved.
|
|
12
|
+
*
|
|
13
|
+
* What it cannot tell it does not report: a document with a template-language block is skipped, an id
|
|
14
|
+
* that is not known until it is rendered (a dynamic value) suppresses the report in its tree only, and
|
|
15
|
+
* a `template` that is in no tree (a declarative shadow root is attached to its parent, or may be)
|
|
16
|
+
* refers to nothing.
|
|
17
|
+
*
|
|
18
|
+
* @see https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute
|
|
19
|
+
*/
|
|
6
20
|
export default createRule({
|
|
7
21
|
meta: meta,
|
|
8
22
|
defaultOptions: {
|
|
9
23
|
ariaVersion: undefined,
|
|
10
24
|
},
|
|
11
25
|
async verify({ document, report, t }) {
|
|
12
|
-
|
|
13
|
-
|
|
26
|
+
// An id is resolved in the tree of the reference: the document, the content of a `template`
|
|
27
|
+
// element and a declarative shadow root are trees of their own
|
|
28
|
+
const idListOfTree = new Map();
|
|
29
|
+
const treesWithDynamicId = new Set();
|
|
14
30
|
const isMutable = document.nodeList.some(node => node.is(node.MARKUPLINT_PREPROCESSOR_BLOCK));
|
|
15
31
|
if (isMutable) {
|
|
16
32
|
return;
|
|
17
33
|
}
|
|
18
|
-
for (const el of document.
|
|
34
|
+
for (const el of document.deepQuerySelectorAll('[id]')) {
|
|
19
35
|
const attr = el.getAttributeNode('id');
|
|
20
36
|
if (!attr) {
|
|
21
37
|
continue;
|
|
22
38
|
}
|
|
39
|
+
const root = el.getRootNode();
|
|
23
40
|
if (attr.isDynamicValue) {
|
|
24
|
-
|
|
41
|
+
treesWithDynamicId.add(root);
|
|
25
42
|
}
|
|
26
|
-
|
|
27
|
-
|
|
43
|
+
const id = attr.valueType === 'code' ? null : getDOMValue(attr);
|
|
44
|
+
if (id != null) {
|
|
45
|
+
let idList = idListOfTree.get(root);
|
|
46
|
+
if (!idList) {
|
|
47
|
+
idList = new Set();
|
|
48
|
+
idListOfTree.set(root, idList);
|
|
49
|
+
}
|
|
50
|
+
idList.add(id);
|
|
28
51
|
}
|
|
29
52
|
}
|
|
30
|
-
if (hasDynamicId) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
53
|
await document.walkOn('Attr', attr => {
|
|
34
54
|
const attrSpec = getAttrSpecs(attr.ownerElement, document.specs);
|
|
35
55
|
if (!attrSpec) {
|
|
@@ -41,7 +61,16 @@ export default createRule({
|
|
|
41
61
|
if (attr.nameKey === 'id') {
|
|
42
62
|
return;
|
|
43
63
|
}
|
|
44
|
-
const
|
|
64
|
+
const root = attr.ownerElement.getRootNode();
|
|
65
|
+
// An element in no tree (a `template` that a declarative shadow root is attached to) refers
|
|
66
|
+
// to nothing, and an id that is not known until it is rendered may be what the reference means
|
|
67
|
+
if (root === attr.ownerElement || treesWithDynamicId.has(root)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const idList = idListOfTree.get(root) ?? new Set();
|
|
71
|
+
// The value is split before the character references are resolved: ` ` is not a separator
|
|
72
|
+
const decode = (id) => getDOMValue(attr, id) ?? id;
|
|
73
|
+
const value = decode(attr.value);
|
|
45
74
|
const spec = findAttrSpec(attrSpec, attr);
|
|
46
75
|
if (spec) {
|
|
47
76
|
// DOMID and DOMID List do not become in the array type.
|
|
@@ -58,10 +87,11 @@ export default createRule({
|
|
|
58
87
|
});
|
|
59
88
|
}
|
|
60
89
|
if (typeof spec.type !== 'string' && 'separator' in spec.type && spec.type.token === 'DOMID') {
|
|
61
|
-
const refs = value
|
|
90
|
+
const refs = attr.value
|
|
62
91
|
.split(spec.type.separator === 'space' ? /\s/ : ',')
|
|
63
92
|
.map(id => id.trim())
|
|
64
|
-
.filter(Boolean)
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.map(id => decode(id));
|
|
65
95
|
for (const ref of refs) {
|
|
66
96
|
if (!idList.has(ref)) {
|
|
67
97
|
report({
|
|
@@ -88,10 +118,11 @@ export default createRule({
|
|
|
88
118
|
});
|
|
89
119
|
}
|
|
90
120
|
else if (aria.value === 'ID reference list') {
|
|
91
|
-
const refs = value
|
|
121
|
+
const refs = attr.value
|
|
92
122
|
.split(/\s/)
|
|
93
123
|
.map(id => id.trim())
|
|
94
|
-
.filter(Boolean)
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
.map(id => decode(id));
|
|
95
126
|
for (const ref of refs) {
|
|
96
127
|
if (!idList.has(ref)) {
|
|
97
128
|
report({
|
|
@@ -4,8 +4,10 @@ export default createRule({
|
|
|
4
4
|
meta: meta,
|
|
5
5
|
defaultValue: [],
|
|
6
6
|
async verify({ document, report, t }) {
|
|
7
|
+
// A restricted element is restricted wherever it is: the content of a `template` element and a
|
|
8
|
+
// declarative shadow root are searched as well
|
|
7
9
|
for (const query of document.rule.value) {
|
|
8
|
-
const elements = document.
|
|
10
|
+
const elements = document.deepQuerySelectorAll(query);
|
|
9
11
|
for (const el of elements) {
|
|
10
12
|
const message = t('{0} is disallowed', t('the "{0*}" {1}', query, 'element'));
|
|
11
13
|
report({
|
|
@@ -19,7 +21,7 @@ export default createRule({
|
|
|
19
21
|
return;
|
|
20
22
|
}
|
|
21
23
|
for (const query of el.rule.value) {
|
|
22
|
-
const elements = el.
|
|
24
|
+
const elements = el.deepQuerySelectorAll(query);
|
|
23
25
|
for (const found of elements) {
|
|
24
26
|
const message = t('{0} is disallowed', t('the "{0*}" {1}', query, 'element'));
|
|
25
27
|
report({
|
|
@@ -15,17 +15,22 @@ export default createRule({
|
|
|
15
15
|
meta: meta,
|
|
16
16
|
defaultOptions: null,
|
|
17
17
|
verify({ document, report, t }) {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// The content of a `template` element and a declarative shadow root are trees of their own, and
|
|
19
|
+
// where they are inserted is not known from the markup: the headings of each tree are the
|
|
20
|
+
// sequence that the heading lead is judged in, as the headings of the document are.
|
|
21
|
+
const headings = document.deepQuerySelectorAll('h1, h2, h3, h4, h5, h6');
|
|
22
|
+
const prevLevelOfTree = new Map();
|
|
20
23
|
for (const heading of headings) {
|
|
21
24
|
const level = Number.parseInt(heading.nodeName.slice(1));
|
|
22
|
-
|
|
25
|
+
const root = heading.getRootNode();
|
|
26
|
+
const prevLevel = prevLevelOfTree.get(root);
|
|
27
|
+
if (prevLevel !== undefined && prevLevel + 1 < level) {
|
|
23
28
|
report({
|
|
24
29
|
scope: heading,
|
|
25
30
|
message: t('{0} must not be skipped', t('Heading levels')),
|
|
26
31
|
});
|
|
27
32
|
}
|
|
28
|
-
|
|
33
|
+
prevLevelOfTree.set(root, level);
|
|
29
34
|
}
|
|
30
35
|
},
|
|
31
36
|
});
|
|
@@ -4,7 +4,8 @@
|
|
|
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,
|
|
7
|
+
* > element on the stack of open elements, or if the parser's fragment parser
|
|
8
|
+
* > abort requested is true, then ignore the token. […]
|
|
8
9
|
* > Otherwise: Set the frameset-ok flag to "not ok" […]
|
|
9
10
|
*
|
|
10
11
|
* "in body" insertion mode, "Any other end tag":
|
|
@@ -19,7 +19,8 @@ const DUPLICATE_BODY_START_TAG = /<body(?=[\s/>])/i;
|
|
|
19
19
|
* > A start tag whose tag name is "body": Parse error. If the stack of open
|
|
20
20
|
* > elements has only one node on it, or if the second element on the stack
|
|
21
21
|
* > of open elements is not a body element, or if there is a template
|
|
22
|
-
* > element on the stack of open elements,
|
|
22
|
+
* > element on the stack of open elements, or if the parser's fragment parser
|
|
23
|
+
* > abort requested is true, then ignore the token. […]
|
|
23
24
|
* > Otherwise: Set the frameset-ok flag to "not ok" […]
|
|
24
25
|
*
|
|
25
26
|
* "in body" insertion mode, "Any other end tag":
|
|
@@ -14,7 +14,7 @@ import meta from './meta.js';
|
|
|
14
14
|
export default createRule({
|
|
15
15
|
meta,
|
|
16
16
|
verify({ document, report, t }) {
|
|
17
|
-
const tables = document.
|
|
17
|
+
const tables = document.deepQuerySelectorAll('table');
|
|
18
18
|
for (const table of tables) {
|
|
19
19
|
const grid = new Grid(table);
|
|
20
20
|
if (grid.hasOverlapped()) {
|
|
@@ -14,7 +14,7 @@ import meta from './meta.js';
|
|
|
14
14
|
export default createRule({
|
|
15
15
|
meta,
|
|
16
16
|
verify({ document, report, t }) {
|
|
17
|
-
const tables = document.
|
|
17
|
+
const tables = document.deepQuerySelectorAll('table');
|
|
18
18
|
for (const table of tables) {
|
|
19
19
|
const grid = new Grid(table);
|
|
20
20
|
if (grid.hasOverlapped()) {
|
|
@@ -49,7 +49,7 @@ export default createRule({
|
|
|
49
49
|
const ms = severity === 'error' ? 'must' : 'should';
|
|
50
50
|
const message = t(`{0} ${ms} {1}`, 'Illegal characters', 'escape in character reference');
|
|
51
51
|
for (const attr of node.attributes) {
|
|
52
|
-
if (attr.
|
|
52
|
+
if (attr.isExpressionValue || attr.isDirective) {
|
|
53
53
|
continue;
|
|
54
54
|
}
|
|
55
55
|
const valueNode = attr.valueNode;
|
|
@@ -121,7 +121,8 @@ export default createRule({
|
|
|
121
121
|
/**
|
|
122
122
|
* Generates a localized, human-readable name for a child node based on its type.
|
|
123
123
|
* Used in error messages to describe the offending node (e.g., 'the "div" element',
|
|
124
|
-
* 'the text node', 'the
|
|
124
|
+
* 'the text node', 'the doctype', 'the code block'). A comment is not named: it is
|
|
125
|
+
* ignored when a content model is evaluated.
|
|
125
126
|
*
|
|
126
127
|
* In `'origin'` mode, uses {@link MLElement.rawName} so that the diagnostic refers
|
|
127
128
|
* to the original AST identity (e.g. the component name `"Breadcrumbs"`) rather
|
|
@@ -143,9 +144,6 @@ scope, t, mode) {
|
|
|
143
144
|
if (scope.is(scope.TEXT_NODE)) {
|
|
144
145
|
return t('the {0}', 'text node');
|
|
145
146
|
}
|
|
146
|
-
if (scope.is(scope.CDATA_SECTION_NODE)) {
|
|
147
|
-
return t('the {0}', 'comment');
|
|
148
|
-
}
|
|
149
147
|
if (scope.is(scope.DOCUMENT_TYPE_NODE)) {
|
|
150
148
|
return t('the {0}', 'doctype');
|
|
151
149
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { contentModelCategoryToTagNames } from '@markuplint/ml-spec';
|
|
2
|
+
import { contentModelFirstChildBase, isContentModelFirstChild, isInterElementWhitespace } from '@markuplint/selector';
|
|
2
3
|
import { depthLog } from './debug.js';
|
|
3
4
|
import { cleanObject, matches } from './utils.js';
|
|
4
5
|
/**
|
|
@@ -20,7 +21,7 @@ export function matchesSelector(query,
|
|
|
20
21
|
childNode, specs, depth, mode) {
|
|
21
22
|
const nodeLog = depthLog('node', depth);
|
|
22
23
|
const nodeEnabled = nodeLog.enabled;
|
|
23
|
-
const { selector, hasText, hasNonEmptyText, hasCustom } = optCondition(query, specs);
|
|
24
|
+
const { selector, selectorAsFirstChild, hasText, hasNonEmptyText, hasCustom } = optCondition(query, specs);
|
|
24
25
|
if (childNode == null) {
|
|
25
26
|
if (hasText) {
|
|
26
27
|
if (nodeEnabled) {
|
|
@@ -47,7 +48,7 @@ childNode, specs, depth, mode) {
|
|
|
47
48
|
}
|
|
48
49
|
if (childNode.is(childNode.TEXT_NODE)) {
|
|
49
50
|
if (hasNonEmptyText) {
|
|
50
|
-
if (childNode.
|
|
51
|
+
if (isInterElementWhitespace(childNode.raw)) {
|
|
51
52
|
if (nodeEnabled) {
|
|
52
53
|
nodeLog('<#nonEmptyText>.matches(%s) => WHITESPACE (rejected)', query);
|
|
53
54
|
}
|
|
@@ -85,7 +86,7 @@ childNode, specs, depth, mode) {
|
|
|
85
86
|
hint: {},
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
|
-
if (childNode.
|
|
89
|
+
if (isInterElementWhitespace(childNode.raw)) {
|
|
89
90
|
if (nodeEnabled) {
|
|
90
91
|
nodeLog('<#text>.matches(%s) => WHITESPACE', query);
|
|
91
92
|
}
|
|
@@ -135,7 +136,14 @@ childNode, specs, depth, mode) {
|
|
|
135
136
|
hint: {},
|
|
136
137
|
};
|
|
137
138
|
}
|
|
138
|
-
|
|
139
|
+
let result = matches(selector, childNode, specs, mode);
|
|
140
|
+
// The siblings are looked at only for an element that the rest does not take.
|
|
141
|
+
// The first child is that of the element's own parent, also inside a transparent
|
|
142
|
+
// element, whose content model is derived from its parent's: `style` in
|
|
143
|
+
// `<div><p></p><noscript><style>` is the first child of the noscript.
|
|
144
|
+
if (!result.matched && selectorAsFirstChild != null && isContentModelFirstChild(childNode)) {
|
|
145
|
+
result = matches(selectorAsFirstChild, childNode, specs, mode);
|
|
146
|
+
}
|
|
139
147
|
if (nodeEnabled) {
|
|
140
148
|
nodeLog('%s.matches(%s) => %s', childNode.raw, query, result.matched);
|
|
141
149
|
}
|
|
@@ -220,18 +228,21 @@ element, mode) {
|
|
|
220
228
|
const conditionWithoutSpecs = {
|
|
221
229
|
'#custom': {
|
|
222
230
|
selector: '#custom',
|
|
231
|
+
selectorAsFirstChild: null,
|
|
223
232
|
hasCustom: true,
|
|
224
233
|
hasText: false,
|
|
225
234
|
hasNonEmptyText: false,
|
|
226
235
|
},
|
|
227
236
|
'#text': {
|
|
228
237
|
selector: '#text',
|
|
238
|
+
selectorAsFirstChild: null,
|
|
229
239
|
hasCustom: false,
|
|
230
240
|
hasText: true,
|
|
231
241
|
hasNonEmptyText: false,
|
|
232
242
|
},
|
|
233
243
|
'#nonEmptyText': {
|
|
234
244
|
selector: '#nonEmptyText',
|
|
245
|
+
selectorAsFirstChild: null,
|
|
235
246
|
hasCustom: false,
|
|
236
247
|
hasText: false,
|
|
237
248
|
hasNonEmptyText: true,
|
|
@@ -271,7 +282,9 @@ function optCondition(query, specs) {
|
|
|
271
282
|
let hasCustom = false;
|
|
272
283
|
let hasText = false;
|
|
273
284
|
let hasNonEmptyText = false;
|
|
274
|
-
|
|
285
|
+
let asFirstChild = null;
|
|
286
|
+
const categoryPattern = /^:model\(([^)]+)\)|^#([a-z-]+)/;
|
|
287
|
+
const selector = query.replace(categoryPattern, (_, $model, _model) => {
|
|
275
288
|
const tag = `#${$model ?? _model}`;
|
|
276
289
|
if (tag === '#nonEmptyText') {
|
|
277
290
|
hasNonEmptyText = true;
|
|
@@ -282,6 +295,7 @@ function optCondition(query, specs) {
|
|
|
282
295
|
throw new Error(`${$model ?? _model} is empty`);
|
|
283
296
|
}
|
|
284
297
|
const selectors = [];
|
|
298
|
+
const firstChildBases = [];
|
|
285
299
|
for (const selector of _selectors) {
|
|
286
300
|
if (selector === '#custom') {
|
|
287
301
|
hasCustom = true;
|
|
@@ -291,12 +305,21 @@ function optCondition(query, specs) {
|
|
|
291
305
|
hasText = true;
|
|
292
306
|
continue;
|
|
293
307
|
}
|
|
308
|
+
const firstChildBase = contentModelFirstChildBase(selector);
|
|
309
|
+
if (firstChildBase != null) {
|
|
310
|
+
firstChildBases.push(firstChildBase);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
294
313
|
selectors.push(selector);
|
|
295
314
|
}
|
|
315
|
+
if (firstChildBases.length > 0) {
|
|
316
|
+
asFirstChild = `:is(${firstChildBases.join(',')})`;
|
|
317
|
+
}
|
|
296
318
|
return `:is(${selectors.join(',')})`;
|
|
297
319
|
});
|
|
298
320
|
const result = {
|
|
299
321
|
selector,
|
|
322
|
+
selectorAsFirstChild: asFirstChild == null ? null : query.replace(categoryPattern, () => asFirstChild ?? ''),
|
|
300
323
|
hasCustom,
|
|
301
324
|
hasText,
|
|
302
325
|
hasNonEmptyText,
|