@markuplint/rules 5.4.0 → 5.6.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 +34 -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 +2 -2
- package/lib/consistent-table-row-length/index.js +1 -1
- package/lib/create-message.js +6 -5
- package/lib/element-supports-aria-prop/index.js +2 -2
- package/lib/form-attr-references-form/index.js +3 -3
- package/lib/helpers.d.ts +96 -5
- package/lib/helpers.js +187 -29
- package/lib/input-list-references-datalist/index.js +3 -3
- package/lib/itemprop-requires-itemscope/index.js +28 -11
- package/lib/label-for-references-labelable/index.js +3 -3
- 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 +8 -8
- package/lib/landmark-roles/roles.d.ts +13 -1
- package/lib/landmark-roles/roles.js +22 -0
- package/lib/link-types/index.js +8 -8
- package/lib/map-id-name-match/index.js +1 -1
- package/lib/meter-value-bounds/index.js +3 -3
- package/lib/no-ambiguous-navigable-target-names/index.js +5 -1
- package/lib/no-aria-hidden-on-hidden-until-found/index.js +1 -1
- package/lib/no-aria-on-presentational-children/index.js +4 -0
- 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-content-after-body/index.js +2 -2
- 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 +68 -5
- package/lib/no-disallowed-attr/index.js +5 -1
- package/lib/no-duplicate-autofocus/index.js +1 -1
- 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-duplicate-sibling-attr/index.js +1 -1
- package/lib/no-duplicate-visible-main/index.js +1 -1
- package/lib/no-empty-table-track/index.js +1 -1
- package/lib/no-extra-selected-options/index.js +1 -1
- package/lib/no-ineffective-attr/index.js +4 -0
- package/lib/no-input-file-value/index.js +1 -1
- package/lib/no-invalid-aria-prop-value/index.js +3 -3
- package/lib/no-invalid-attr-value/index.js +12 -0
- package/lib/no-malformed-character-reference/index.js +1 -1
- 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 +15 -8
- 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-attr/index.js +2 -1
- 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 +4 -3
- package/lib/no-table-cell-overlap/index.js +3 -2
- package/lib/no-table-span-overflow/index.js +1 -1
- package/lib/no-translate.d.ts +17 -0
- package/lib/no-translate.js +29 -0
- package/lib/no-unclosed-element-at-eof/index.js +1 -1
- package/lib/no-unescaped-char/index.js +1 -1
- package/lib/no-unsupported-browser-features/index.d.ts +9 -0
- package/lib/no-unsupported-browser-features/index.js +10 -7
- package/lib/permitted-contents/content-model.d.ts +14 -0
- package/lib/permitted-contents/content-model.js +21 -1
- package/lib/permitted-contents/index.js +6 -8
- package/lib/permitted-contents/matches-selector.js +32 -5
- package/lib/permitted-contents/represent-transparent-nodes.js +100 -68
- package/lib/permitted-contents/slot-content.d.ts +18 -1
- package/lib/permitted-contents/slot-content.js +49 -2
- package/lib/permitted-contents/start.d.ts +8 -1
- package/lib/permitted-contents/start.js +39 -6
- 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/progress-value-bounds/index.js +2 -2
- 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 +37 -1
- package/lib/require-ancestor/index.js +82 -9
- package/lib/require-ancestor/meta.d.ts +5 -0
- package/lib/require-ancestor/meta.js +5 -0
- package/lib/require-attr/index.js +4 -1
- package/lib/require-datetime/index.js +3 -2
- package/lib/require-dialog-autofocus/index.js +8 -6
- package/lib/require-element/index.js +8 -2
- 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 +12 -3
- package/lib/valid-speculation-rules/index.js +1 -1
- package/lib/wai-aria/checkings/contradictory-aria-prop.js +2 -2
- package/lib/wai-aria/checkings/interaction-in-hidden.js +6 -2
- package/lib/wai-aria/checkings/presentational-children.js +38 -17
- package/lib/wai-aria/checkings/redundant-aria-prop.js +1 -1
- package/lib/wai-aria/checkings/required-accessibility-parent-role.js +4 -2
- package/lib/wai-aria/checkings/required-owned-elements.d.ts +2 -0
- package/lib/wai-aria/checkings/required-owned-elements.js +175 -21
- 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 +8 -19
- package/package.json +14 -11
|
@@ -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,7 +1,13 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* Labels are names (shown as written, `{n*}`) except `content`, which is a word and is translated.
|
|
7
|
+
*/
|
|
8
|
+
function showLabel(t, label) {
|
|
9
|
+
return label === 'content' ? t('content') : label;
|
|
10
|
+
}
|
|
5
11
|
export default createRule({
|
|
6
12
|
meta,
|
|
7
13
|
defaultSeverity: 'warning',
|
|
@@ -13,7 +19,7 @@ export default createRule({
|
|
|
13
19
|
await document.walkOn('Element', el => {
|
|
14
20
|
const ariaVersion = ARIA_RECOMMENDED_VERSION;
|
|
15
21
|
// Skip mutable elements (template expressions etc.)
|
|
16
|
-
if (accnameMayBeMutable(el
|
|
22
|
+
if (accnameMayBeMutable(el)) {
|
|
17
23
|
return;
|
|
18
24
|
}
|
|
19
25
|
// Skip hidden elements
|
|
@@ -32,7 +38,7 @@ export default createRule({
|
|
|
32
38
|
for (const loser of overridden) {
|
|
33
39
|
report({
|
|
34
40
|
scope: el,
|
|
35
|
-
message: t('The accessible name from "{0*}" overrides "{1*}"', winner.label, loser.label),
|
|
41
|
+
message: t('The accessible name from "{0*}" overrides "{1*}"', showLabel(t, winner.label), showLabel(t, loser.label)),
|
|
36
42
|
});
|
|
37
43
|
}
|
|
38
44
|
}
|
|
@@ -61,13 +67,14 @@ computed) {
|
|
|
61
67
|
//
|
|
62
68
|
// Known limitation: step 2C (Embedded Control) is not modeled, so a self-referencing
|
|
63
69
|
// textbox, combobox, listbox or range is treated like any other element.
|
|
64
|
-
const
|
|
65
|
-
|
|
70
|
+
const ariaLabelledbyAttr = el.getAttributeNode('aria-labelledby');
|
|
71
|
+
const ariaLabelledby = ariaLabelledbyAttr?.value;
|
|
72
|
+
if (ariaLabelledbyAttr && ariaLabelledby && ariaLabelledby.trim()) {
|
|
66
73
|
const ids = ariaLabelledby.trim().split(/\s+/);
|
|
67
74
|
// An IDREF resolves to the first element with that id, so with duplicate ids a
|
|
68
75
|
// 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 =>
|
|
76
|
+
const isSelfReferencing = ids.some(id => getElementByIdInTree(el, id, ariaLabelledbyAttr.isExpressionValue) === el);
|
|
77
|
+
const hasResolvableId = ids.some(id => getElementByIdInTree(el, id, ariaLabelledbyAttr.isExpressionValue));
|
|
71
78
|
if (hasResolvableId && !isSelfReferencing) {
|
|
72
79
|
sources.push({ label: 'aria-labelledby' });
|
|
73
80
|
}
|
|
@@ -78,7 +85,7 @@ computed) {
|
|
|
78
85
|
sources.push({ label: 'aria-label' });
|
|
79
86
|
}
|
|
80
87
|
// 3. label (explicit or implicit) — only for labelable elements
|
|
81
|
-
const ownedLabel = getOwnedLabel(el
|
|
88
|
+
const ownedLabel = getOwnedLabel(el);
|
|
82
89
|
if (ownedLabel) {
|
|
83
90
|
sources.push({ label: 'label' });
|
|
84
91
|
}
|
|
@@ -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({
|
|
@@ -2,6 +2,7 @@ import { createRule } from '@markuplint/ml-core';
|
|
|
2
2
|
import { check, isEnum, isPattern } from '@markuplint/types';
|
|
3
3
|
import { attrCheck } from '../attr-check.js';
|
|
4
4
|
import { createNameOptionLookup } from '../helpers.js';
|
|
5
|
+
import { noTranslate } from '../no-translate.js';
|
|
5
6
|
import meta from './meta.js';
|
|
6
7
|
export default createRule({
|
|
7
8
|
meta: meta,
|
|
@@ -73,7 +74,7 @@ type) {
|
|
|
73
74
|
return t('{0} is matched with the below disallowed patterns: {1}', t('the "{0*}" {1}', name, 'attribute'), type.pattern);
|
|
74
75
|
}
|
|
75
76
|
if (typeof type !== 'string' && isEnum(type)) {
|
|
76
|
-
return t('{0} is disallowed to accept the following values: {1}', t('the "{0*}" {1}', name, 'attribute'), t(type.enum));
|
|
77
|
+
return t('{0} is disallowed to accept the following values: {1}', t('the "{0*}" {1}', name, 'attribute'), t(type.enum.map(noTranslate)));
|
|
77
78
|
}
|
|
78
79
|
return t('{0} is disallowed', t('{0} of {1}', t('the {0}', 'type'), t('the "{0*}" {1}', name, 'attribute')));
|
|
79
80
|
}
|
|
@@ -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":
|
|
@@ -52,13 +53,13 @@ export default createRule({
|
|
|
52
53
|
if (STRAY_HEAD_END_TAG.test(text.raw)) {
|
|
53
54
|
report({
|
|
54
55
|
scope: text,
|
|
55
|
-
message: t('{0} detected', t('a stray "{0}" end tag', 'head')),
|
|
56
|
+
message: t('{0} detected', t('a stray "{0*}" end tag', 'head')),
|
|
56
57
|
});
|
|
57
58
|
}
|
|
58
59
|
if (DUPLICATE_BODY_START_TAG.test(text.raw)) {
|
|
59
60
|
report({
|
|
60
61
|
scope: text,
|
|
61
|
-
message: t('{0} detected', t('a duplicate "{0}" start tag', 'body')),
|
|
62
|
+
message: t('{0} detected', t('a duplicate "{0*}" start tag', 'body')),
|
|
62
63
|
});
|
|
63
64
|
}
|
|
64
65
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
2
|
import { Grid } from '../table-row-column-alignment/grid.js';
|
|
3
|
+
import { noTranslate } from '../no-translate.js';
|
|
3
4
|
import meta from './meta.js';
|
|
4
5
|
/**
|
|
5
6
|
* HTML LS [§4.9.12.1 *Forming a table*](https://html.spec.whatwg.org/multipage/tables.html#forming-a-table)
|
|
@@ -14,7 +15,7 @@ import meta from './meta.js';
|
|
|
14
15
|
export default createRule({
|
|
15
16
|
meta,
|
|
16
17
|
verify({ document, report, t }) {
|
|
17
|
-
const tables = document.
|
|
18
|
+
const tables = document.deepQuerySelectorAll('table');
|
|
18
19
|
for (const table of tables) {
|
|
19
20
|
const grid = new Grid(table);
|
|
20
21
|
if (grid.hasOverlapped()) {
|
|
@@ -22,7 +23,7 @@ export default createRule({
|
|
|
22
23
|
scope: table,
|
|
23
24
|
message: t(
|
|
24
25
|
//
|
|
25
|
-
'{0} are causing {1}', t(['rowspan', 'colspan'], true), 'cell overlap'),
|
|
26
|
+
'{0} are causing {1}', t(['rowspan', 'colspan'].map(noTranslate), true), 'cell overlap'),
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
29
|
}
|
|
@@ -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()) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marks a value so that the translator of `@markuplint/i18n` shows it as written.
|
|
3
|
+
*
|
|
4
|
+
* The translator looks a placeholder's value up as a keyword (`list` is `リスト` with `locale: 'ja'`),
|
|
5
|
+
* and so it does each item of a list, `t(['row', 'list'])`. A placeholder written `{n*}` is not
|
|
6
|
+
* translated, but a list has no such mark: an item that is data (a role name, an attribute value, a
|
|
7
|
+
* token the author wrote) is wrapped in `%…%`, the translator's own form of "not a keyword".
|
|
8
|
+
*
|
|
9
|
+
* The translator reads `%…%` around a whole value as that mark and `%%` at an end as one `%`, so a
|
|
10
|
+
* value that has a `%` at an end is written with it doubled: `%list%` is shown as `%list%`, not
|
|
11
|
+
* `list`. A `%` inside a value needs nothing, as no keyword has one. An empty value is returned as
|
|
12
|
+
* it is: wrapped, `%%` would be read back as a stray `%`.
|
|
13
|
+
*
|
|
14
|
+
* @param value - The text to show as written
|
|
15
|
+
* @returns The text for an item of a list, or for a placeholder, given to `t()`
|
|
16
|
+
*/
|
|
17
|
+
export declare function noTranslate(value: string): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marks a value so that the translator of `@markuplint/i18n` shows it as written.
|
|
3
|
+
*
|
|
4
|
+
* The translator looks a placeholder's value up as a keyword (`list` is `リスト` with `locale: 'ja'`),
|
|
5
|
+
* and so it does each item of a list, `t(['row', 'list'])`. A placeholder written `{n*}` is not
|
|
6
|
+
* translated, but a list has no such mark: an item that is data (a role name, an attribute value, a
|
|
7
|
+
* token the author wrote) is wrapped in `%…%`, the translator's own form of "not a keyword".
|
|
8
|
+
*
|
|
9
|
+
* The translator reads `%…%` around a whole value as that mark and `%%` at an end as one `%`, so a
|
|
10
|
+
* value that has a `%` at an end is written with it doubled: `%list%` is shown as `%list%`, not
|
|
11
|
+
* `list`. A `%` inside a value needs nothing, as no keyword has one. An empty value is returned as
|
|
12
|
+
* it is: wrapped, `%%` would be read back as a stray `%`.
|
|
13
|
+
*
|
|
14
|
+
* @param value - The text to show as written
|
|
15
|
+
* @returns The text for an item of a list, or for a placeholder, given to `t()`
|
|
16
|
+
*/
|
|
17
|
+
export function noTranslate(value) {
|
|
18
|
+
if (value === '') {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
if (!value.includes('%')) {
|
|
22
|
+
return `%${value}%`;
|
|
23
|
+
}
|
|
24
|
+
// `%%` is the escaped form of one `%` at an end, so a lone `%` is a single escaped one
|
|
25
|
+
if (value === '%') {
|
|
26
|
+
return '%%';
|
|
27
|
+
}
|
|
28
|
+
return value.replace(/^%/, '%%').replace(/%$/, '%%');
|
|
29
|
+
}
|
|
@@ -89,7 +89,7 @@ export default createRule({
|
|
|
89
89
|
return;
|
|
90
90
|
report({
|
|
91
91
|
scope: el,
|
|
92
|
-
message: t('{0} is not closed at the end of the file', t('the "{0}" {1}', el.localName, 'element')),
|
|
92
|
+
message: t('{0} is not closed at the end of the file', t('the "{0*}" {1}', el.localName, 'element')),
|
|
93
93
|
});
|
|
94
94
|
});
|
|
95
95
|
},
|
|
@@ -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;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { UnsupportedResult } from './compat-data.js';
|
|
2
|
+
import { createRule } from '@markuplint/ml-core';
|
|
1
3
|
/**
|
|
2
4
|
* Options for the `no-unsupported-browser-features` rule.
|
|
3
5
|
*/
|
|
@@ -17,3 +19,10 @@ type Options = {
|
|
|
17
19
|
};
|
|
18
20
|
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<boolean, Options>>;
|
|
19
21
|
export default _default;
|
|
22
|
+
/**
|
|
23
|
+
* Format an unsupported feature message listing affected browsers.
|
|
24
|
+
*
|
|
25
|
+
* The double-underscore prefix marks this as exposed only for testing: the browsers a feature
|
|
26
|
+
* was removed from are not reachable through the data of the dependencies.
|
|
27
|
+
*/
|
|
28
|
+
export declare function __formatUnsupportedMessage(t: Parameters<Parameters<typeof createRule>[0]['verify']>[0]['t'], featureName: string, featureType: string, unsupported: readonly UnsupportedResult[]): string;
|
|
@@ -34,7 +34,7 @@ export default createRule({
|
|
|
34
34
|
if (unsupported.length > 0) {
|
|
35
35
|
report({
|
|
36
36
|
scope: el,
|
|
37
|
-
message:
|
|
37
|
+
message: __formatUnsupportedMessage(t, elName, 'element', unsupported),
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -53,7 +53,7 @@ export default createRule({
|
|
|
53
53
|
line: attr.nameNode?.startLine,
|
|
54
54
|
col: attr.nameNode?.startCol,
|
|
55
55
|
raw: attr.nameNode?.raw,
|
|
56
|
-
message:
|
|
56
|
+
message: __formatUnsupportedMessage(t, attrName, 'attribute', unsupported),
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -63,18 +63,21 @@ export default createRule({
|
|
|
63
63
|
});
|
|
64
64
|
/**
|
|
65
65
|
* Format an unsupported feature message listing affected browsers.
|
|
66
|
+
*
|
|
67
|
+
* The double-underscore prefix marks this as exposed only for testing: the browsers a feature
|
|
68
|
+
* was removed from are not reachable through the data of the dependencies.
|
|
66
69
|
*/
|
|
67
|
-
function
|
|
70
|
+
export function __formatUnsupportedMessage(t, featureName, featureType, unsupported) {
|
|
68
71
|
const browserDetails = unsupported
|
|
69
72
|
.map(u => {
|
|
70
73
|
if (u.addedVersion === false) {
|
|
71
74
|
if (u.removedVersion) {
|
|
72
|
-
return
|
|
75
|
+
return t('{0*} (removed in {1*})', u.displayName, u.removedVersion);
|
|
73
76
|
}
|
|
74
|
-
return
|
|
77
|
+
return t('{0*} (not supported)', u.displayName);
|
|
75
78
|
}
|
|
76
|
-
return
|
|
79
|
+
return t('{0*} (added in {1*}, target: {2*})', u.displayName, u.addedVersion, u.targetVersion);
|
|
77
80
|
})
|
|
78
81
|
.join(', ');
|
|
79
|
-
return t('{0} is not supported in {1}', t('the "{0*}" {1}', featureName, featureType), browserDetails);
|
|
82
|
+
return t('{0} is not supported in {1*}', t('the "{0*}" {1}', featureName, featureType), browserDetails);
|
|
80
83
|
}
|
|
@@ -40,6 +40,20 @@ export declare function resolveContentModel(el: Element, rules: readonly TagRule
|
|
|
40
40
|
* @returns Whether the rule is written for the element
|
|
41
41
|
*/
|
|
42
42
|
export declare function hasTagRule(el: Element, rule: TagRule): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Whether the user's tag rules include one written for the element whose content model governs
|
|
45
|
+
* the children of `el` (see {@link getContentOwner}).
|
|
46
|
+
*
|
|
47
|
+
* Such a rule lists the components that the element takes by the name that they are written with,
|
|
48
|
+
* and the components are evaluated by it. Without one, a component that no pretender resolved renders
|
|
49
|
+
* something that is not known, and what holds whatever it renders is the only thing to report.
|
|
50
|
+
*
|
|
51
|
+
* @param el - The element whose children are validated
|
|
52
|
+
* @param rules - The user's tag rules
|
|
53
|
+
* @param mode - Which identity to resolve against
|
|
54
|
+
* @returns Whether a tag rule is written for the element
|
|
55
|
+
*/
|
|
56
|
+
export declare function hasUserTagRule(el: Element, rules: readonly TagRule[], mode: Mode): boolean;
|
|
43
57
|
/**
|
|
44
58
|
* Returns a Specs object that merges the base ML spec with user-defined
|
|
45
59
|
* tag rules. Results are cached by the specs of the document and the serialized rules to avoid redundant
|
|
@@ -41,7 +41,8 @@ el, rules, options, mode) {
|
|
|
41
41
|
},
|
|
42
42
|
];
|
|
43
43
|
}
|
|
44
|
-
const
|
|
44
|
+
const ignoreUnresolvedComponents = options.ignoreHasMutableChildren && !hasUserTagRule(el, rules, mode);
|
|
45
|
+
const result = start(model, el, rules, specs, options, mode, ignoreUnresolvedComponents);
|
|
45
46
|
return result;
|
|
46
47
|
}
|
|
47
48
|
/**
|
|
@@ -124,6 +125,25 @@ export function hasTagRule(
|
|
|
124
125
|
el, rule) {
|
|
125
126
|
return rule.tag === el.rawName || el.matchesTagNameVariant(rule.tag);
|
|
126
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Whether the user's tag rules include one written for the element whose content model governs
|
|
130
|
+
* the children of `el` (see {@link getContentOwner}).
|
|
131
|
+
*
|
|
132
|
+
* Such a rule lists the components that the element takes by the name that they are written with,
|
|
133
|
+
* and the components are evaluated by it. Without one, a component that no pretender resolved renders
|
|
134
|
+
* something that is not known, and what holds whatever it renders is the only thing to report.
|
|
135
|
+
*
|
|
136
|
+
* @param el - The element whose children are validated
|
|
137
|
+
* @param rules - The user's tag rules
|
|
138
|
+
* @param mode - Which identity to resolve against
|
|
139
|
+
* @returns Whether a tag rule is written for the element
|
|
140
|
+
*/
|
|
141
|
+
export function hasUserTagRule(
|
|
142
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
143
|
+
el, rules, mode) {
|
|
144
|
+
const owner = getContentOwner(el, mode);
|
|
145
|
+
return rules.some(rule => hasTagRule(owner, rule));
|
|
146
|
+
}
|
|
127
147
|
/**
|
|
128
148
|
* Cache for merged specs: for each base spec of a document, keyed by the JSON-serialized tag rules.
|
|
129
149
|
* Avoids re-merging the same set of user-defined tag rules on every element check.
|