@markuplint/rules 5.5.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 +13 -0
- package/lib/attr-value-quotes/index.js +1 -1
- package/lib/create-message.js +6 -5
- package/lib/form-attr-references-form/index.js +1 -1
- package/lib/helpers.d.ts +54 -1
- package/lib/helpers.js +99 -3
- package/lib/input-list-references-datalist/index.js +1 -1
- package/lib/itemprop-requires-itemscope/index.js +1 -1
- package/lib/label-for-references-labelable/index.js +1 -1
- package/lib/label-no-multiple-controls/index.js +2 -2
- 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 +1 -1
- package/lib/no-aria-hidden-on-hidden-until-found/index.js +1 -1
- package/lib/no-content-after-body/index.js +2 -2
- package/lib/no-disallowed-ancestor/index.js +5 -16
- package/lib/no-disallowed-attr/index.js +1 -1
- package/lib/no-duplicate-autofocus/index.js +1 -1
- package/lib/no-duplicate-sibling-attr/index.js +1 -1
- package/lib/no-duplicate-visible-main/index.js +1 -1
- package/lib/no-extra-selected-options/index.js +1 -1
- package/lib/no-input-file-value/index.js +1 -1
- package/lib/no-malformed-character-reference/index.js +1 -1
- package/lib/no-redundant-accessible-name/index.js +7 -1
- package/lib/no-restricted-attr/index.js +2 -1
- package/lib/no-stray-head-or-body-tag/index.js +2 -2
- package/lib/no-table-cell-overlap/index.js +2 -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-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 +4 -4
- package/lib/permitted-contents/matches-selector.js +4 -0
- package/lib/permitted-contents/slot-content.d.ts +16 -0
- package/lib/permitted-contents/slot-content.js +21 -0
- package/lib/permitted-contents/start.d.ts +8 -1
- package/lib/permitted-contents/start.js +36 -5
- package/lib/progress-value-bounds/index.js +2 -2
- package/lib/require-ancestor/index.d.ts +17 -1
- package/lib/require-ancestor/index.js +52 -5
- 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 +2 -1
- package/lib/require-element/index.js +8 -2
- package/lib/usemap-references-map/index.js +1 -1
- package/lib/wai-aria/checkings/contradictory-aria-prop.js +2 -2
- package/lib/wai-aria/checkings/redundant-aria-prop.js +1 -1
- package/lib/wai-aria/checkings/required-accessibility-parent-role.js +2 -1
- 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/value.js +3 -2
- package/package.json +12 -9
|
@@ -53,13 +53,13 @@ export default createRule({
|
|
|
53
53
|
if (STRAY_HEAD_END_TAG.test(text.raw)) {
|
|
54
54
|
report({
|
|
55
55
|
scope: text,
|
|
56
|
-
message: t('{0} detected', t('a stray "{0}" end tag', 'head')),
|
|
56
|
+
message: t('{0} detected', t('a stray "{0*}" end tag', 'head')),
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
if (DUPLICATE_BODY_START_TAG.test(text.raw)) {
|
|
60
60
|
report({
|
|
61
61
|
scope: text,
|
|
62
|
-
message: t('{0} detected', t('a duplicate "{0}" start tag', 'body')),
|
|
62
|
+
message: t('{0} detected', t('a duplicate "{0*}" start tag', 'body')),
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
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)
|
|
@@ -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
|
}
|
|
@@ -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
|
},
|
|
@@ -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.
|
|
@@ -2,7 +2,7 @@ import { createRule } from '@markuplint/ml-core';
|
|
|
2
2
|
import { contentModel, hasTagRule } from './content-model.js';
|
|
3
3
|
import meta from './meta.js';
|
|
4
4
|
import { transparentMode } from './represent-transparent-nodes.js';
|
|
5
|
-
import { getContentOwner, getReportScope, isContentMutable } from './slot-content.js';
|
|
5
|
+
import { getContentOwner, getReportScope, hasMutableChildNodes, isContentMutable } from './slot-content.js';
|
|
6
6
|
export default createRule({
|
|
7
7
|
meta: meta,
|
|
8
8
|
defaultValue: [],
|
|
@@ -39,7 +39,7 @@ export default createRule({
|
|
|
39
39
|
case 'MISSING_NODE_ONE_OR_MORE': {
|
|
40
40
|
if (scope.rule.options.ignoreHasMutableChildren &&
|
|
41
41
|
(!scope.is(scope.ELEMENT_NODE) ||
|
|
42
|
-
scope
|
|
42
|
+
hasMutableChildNodes(scope, mode) ||
|
|
43
43
|
isContentMutable(el, mode))) {
|
|
44
44
|
break;
|
|
45
45
|
}
|
|
@@ -59,7 +59,7 @@ export default createRule({
|
|
|
59
59
|
case 'MISSING_NODE_REQUIRED': {
|
|
60
60
|
if (scope.rule.options.ignoreHasMutableChildren &&
|
|
61
61
|
(!scope.is(scope.ELEMENT_NODE) ||
|
|
62
|
-
scope
|
|
62
|
+
hasMutableChildNodes(scope, mode) ||
|
|
63
63
|
isContentMutable(el, mode))) {
|
|
64
64
|
break;
|
|
65
65
|
}
|
|
@@ -139,7 +139,7 @@ function name(
|
|
|
139
139
|
scope, t, mode) {
|
|
140
140
|
if (scope.is(scope.ELEMENT_NODE)) {
|
|
141
141
|
const displayName = mode === 'origin' ? scope.rawName : scope.localName;
|
|
142
|
-
return t('the "{0}" {1}', displayName, 'element');
|
|
142
|
+
return t('the "{0*}" {1}', displayName, 'element');
|
|
143
143
|
}
|
|
144
144
|
if (scope.is(scope.TEXT_NODE)) {
|
|
145
145
|
return t('the {0}', 'text node');
|
|
@@ -211,6 +211,10 @@ childNode, specs, depth, mode) {
|
|
|
211
211
|
*
|
|
212
212
|
* The `'pretended'` criterion is the one `MLElement.isCustomElement` gives a
|
|
213
213
|
* `:model()` selector; change them together.
|
|
214
|
+
*
|
|
215
|
+
* What a component renders is not known only when no pretender resolved it, and
|
|
216
|
+
* a web component renders as itself: `MLElement#isUnresolvedComponent` is the narrower
|
|
217
|
+
* criterion, which `start` uses to leave the components out of the evaluation.
|
|
214
218
|
*/
|
|
215
219
|
function isTreatedAsCustom(
|
|
216
220
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -34,6 +34,22 @@ export declare function expandChildren<T extends RuleConfigValue, O extends Plai
|
|
|
34
34
|
* a fragment pretender among its children (which stands in the parent).
|
|
35
35
|
*/
|
|
36
36
|
export declare function isContentMutable<T extends RuleConfigValue, O extends PlainData>(el: Element<T, O>, mode: Mode): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Whether a child of `el` that the content model reads is a block of a preprocessor or a `slot`
|
|
39
|
+
* (`mayRenderUnknownChildren`): a required child that is missing may be what it renders.
|
|
40
|
+
* Only the children are looked at (the children of a fragment pretender included, as the content
|
|
41
|
+
* model reads them): what a descendant renders is not a child of `el`.
|
|
42
|
+
*
|
|
43
|
+
* A component that no pretender resolved is not looked at here: `start` leaves it out of each
|
|
44
|
+
* conditional branch that has one, and drops the missing children of that branch only.
|
|
45
|
+
*
|
|
46
|
+
* @template T - The rule configuration value type
|
|
47
|
+
* @template O - The rule options type
|
|
48
|
+
* @param el - The element whose children are looked at
|
|
49
|
+
* @param mode - Which identity of `el` the content model is read for
|
|
50
|
+
* @returns True if a child may stand for children that are not known
|
|
51
|
+
*/
|
|
52
|
+
export declare function hasMutableChildNodes<T extends RuleConfigValue, O extends PlainData>(el: Element<T, O>, mode: Mode): boolean;
|
|
37
53
|
/**
|
|
38
54
|
* A violation on an element that only the pretender renders (`contents`) is reported
|
|
39
55
|
* on the component that renders it: the element does not exist in the source.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mayRenderUnknownChildren } from '../helpers.js';
|
|
1
2
|
/**
|
|
2
3
|
* Returns the slot-content description of a pretended component, which exists
|
|
3
4
|
* only in `'pretended'` mode: the `'origin'` mode evaluates user tag rules keyed
|
|
@@ -86,6 +87,26 @@ el, mode) {
|
|
|
86
87
|
}
|
|
87
88
|
return [...el.childNodes].some(child => isFragmentPretender(child, mode) && isContentMutable(child, mode));
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Whether a child of `el` that the content model reads is a block of a preprocessor or a `slot`
|
|
92
|
+
* (`mayRenderUnknownChildren`): a required child that is missing may be what it renders.
|
|
93
|
+
* Only the children are looked at (the children of a fragment pretender included, as the content
|
|
94
|
+
* model reads them): what a descendant renders is not a child of `el`.
|
|
95
|
+
*
|
|
96
|
+
* A component that no pretender resolved is not looked at here: `start` leaves it out of each
|
|
97
|
+
* conditional branch that has one, and drops the missing children of that branch only.
|
|
98
|
+
*
|
|
99
|
+
* @template T - The rule configuration value type
|
|
100
|
+
* @template O - The rule options type
|
|
101
|
+
* @param el - The element whose children are looked at
|
|
102
|
+
* @param mode - Which identity of `el` the content model is read for
|
|
103
|
+
* @returns True if a child may stand for children that are not known
|
|
104
|
+
*/
|
|
105
|
+
export function hasMutableChildNodes(
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
107
|
+
el, mode) {
|
|
108
|
+
return expandChildren(el, [...el.getPureChildNodes()], mode).some(child => mayRenderUnknownChildren(child, false));
|
|
109
|
+
}
|
|
89
110
|
function isFragmentPretender(
|
|
90
111
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
91
112
|
node, mode) {
|
|
@@ -10,10 +10,17 @@ import type { ReadonlyDeep } from 'type-fest';
|
|
|
10
10
|
* When `options.evaluateConditionalChildNodes` is enabled, all conditional branches
|
|
11
11
|
* (e.g., from template directives) are evaluated independently.
|
|
12
12
|
*
|
|
13
|
+
* A component that no pretender resolved (`MLElement#isUnresolvedComponent`) renders something that is
|
|
14
|
+
* not known: it may be the element that the content model asks for, or stand for several. With
|
|
15
|
+
* `ignoreUnresolvedComponents` the children are evaluated without such components, and only what holds
|
|
16
|
+
* whatever they render is kept ({@link holdsWhateverIsRendered}). Without it they are evaluated as
|
|
17
|
+
* written, by the name of the component (`isTreatedAsCustom` of `matches-selector.ts`).
|
|
18
|
+
*
|
|
13
19
|
* @param contents - The content model definition: `false` for empty, `true` for any, or an array of patterns.
|
|
14
20
|
* @param el - The parent element whose children are being validated.
|
|
15
21
|
* @param specs - The resolved spec data for content model lookups.
|
|
16
22
|
* @param options - Validation behavior options.
|
|
23
|
+
* @param ignoreUnresolvedComponents - Whether to evaluate the children without the components that no pretender resolved.
|
|
17
24
|
* @returns An array of content model results describing any violations found.
|
|
18
25
|
*/
|
|
19
|
-
export declare function start(contents: ReadonlyDeep<ContentModel['contents']>, el: Element, rules: readonly TagRule[], specs: Specs, options: Options, mode: Mode): ContentModelResult[];
|
|
26
|
+
export declare function start(contents: ReadonlyDeep<ContentModel['contents']>, el: Element, rules: readonly TagRule[], specs: Specs, options: Options, mode: Mode, ignoreUnresolvedComponents?: boolean): ContentModelResult[];
|
|
@@ -12,25 +12,42 @@ import { isComment } from './utils.js';
|
|
|
12
12
|
* When `options.evaluateConditionalChildNodes` is enabled, all conditional branches
|
|
13
13
|
* (e.g., from template directives) are evaluated independently.
|
|
14
14
|
*
|
|
15
|
+
* A component that no pretender resolved (`MLElement#isUnresolvedComponent`) renders something that is
|
|
16
|
+
* not known: it may be the element that the content model asks for, or stand for several. With
|
|
17
|
+
* `ignoreUnresolvedComponents` the children are evaluated without such components, and only what holds
|
|
18
|
+
* whatever they render is kept ({@link holdsWhateverIsRendered}). Without it they are evaluated as
|
|
19
|
+
* written, by the name of the component (`isTreatedAsCustom` of `matches-selector.ts`).
|
|
20
|
+
*
|
|
15
21
|
* @param contents - The content model definition: `false` for empty, `true` for any, or an array of patterns.
|
|
16
22
|
* @param el - The parent element whose children are being validated.
|
|
17
23
|
* @param specs - The resolved spec data for content model lookups.
|
|
18
24
|
* @param options - Validation behavior options.
|
|
25
|
+
* @param ignoreUnresolvedComponents - Whether to evaluate the children without the components that no pretender resolved.
|
|
19
26
|
* @returns An array of content model results describing any violations found.
|
|
20
27
|
*/
|
|
21
28
|
export function start(contents,
|
|
22
29
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
23
|
-
el, rules, specs, options, mode) {
|
|
30
|
+
el, rules, specs, options, mode, ignoreUnresolvedComponents = false) {
|
|
24
31
|
const givenPatterns = options.evaluateConditionalChildNodes
|
|
25
32
|
? el.conditionalChildNodes().map(childNodes => [...childNodes])
|
|
26
33
|
: [[...el.childNodes]];
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
// A model that is `false` does not allow a component either: what it renders may be anything
|
|
35
|
+
const ignoresComponents = ignoreUnresolvedComponents && contents !== false && contents !== true;
|
|
36
|
+
const isIgnoredComponent = (child) => ignoresComponents && child.is(child.ELEMENT_NODE) && child.isUnresolvedComponent();
|
|
37
|
+
const expandedPatterns = givenPatterns.map(given => expandChildren(el, given, mode));
|
|
38
|
+
// Per pattern: a conditional branch without a component is evaluated as it is written
|
|
39
|
+
const hasIgnoredComponents = expandedPatterns.map(expanded => expanded.some(child => isIgnoredComponent(child)));
|
|
40
|
+
const childNodesPatterns = expandedPatterns.map(expanded => {
|
|
41
|
+
const children = expanded.filter(child => !isComment(child) && !isIgnoredComponent(child));
|
|
29
42
|
return options.evaluateConditionalChildNodes
|
|
30
43
|
? [...children]
|
|
31
44
|
: children.filter(child => !(child.is(child.TEXT_NODE) && isInterElementWhitespace(child.raw)));
|
|
32
45
|
});
|
|
33
|
-
return childNodesPatterns.flatMap(childNodes => {
|
|
46
|
+
return childNodesPatterns.flatMap((childNodes, index) => {
|
|
47
|
+
const results = evaluate(childNodes);
|
|
48
|
+
return hasIgnoredComponents[index] ? results.filter(result => holdsWhateverIsRendered(result)) : results;
|
|
49
|
+
});
|
|
50
|
+
function evaluate(childNodes) {
|
|
34
51
|
if (contents === false) {
|
|
35
52
|
if (childNodes.length > 0) {
|
|
36
53
|
return [
|
|
@@ -82,5 +99,19 @@ el, rules, specs, options, mode) {
|
|
|
82
99
|
})),
|
|
83
100
|
];
|
|
84
101
|
});
|
|
85
|
-
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Whether a violation found without the components that no pretender resolved holds whatever
|
|
106
|
+
* they render.
|
|
107
|
+
*
|
|
108
|
+
* A component may render the child that is missing, so a missing child is not one. An element
|
|
109
|
+
* that the content model does not take is (`<div>` in `<ul><Item /><div></div></ul>`, the second
|
|
110
|
+
* `<title>` in `<head><Meta /><title /><title /></head>`): `order` reports it only after it has
|
|
111
|
+
* consumed every entry that it can, and a component adds nodes without moving or removing the ones
|
|
112
|
+
* that are written. The order is not reported either: when the elements are out of order the
|
|
113
|
+
* entry that comes first is missing, which a component may render (`<dl><Term /><dd></dd></dl>`).
|
|
114
|
+
*/
|
|
115
|
+
function holdsWhateverIsRendered(result) {
|
|
116
|
+
return result.type !== 'MISSING_NODE_REQUIRED' && result.type !== 'MISSING_NODE_ONE_OR_MORE';
|
|
86
117
|
}
|
|
@@ -45,8 +45,8 @@ export default createRule({
|
|
|
45
45
|
if (valueNum <= maxValue)
|
|
46
46
|
return;
|
|
47
47
|
const message = maxAttr
|
|
48
|
-
? t('The value of the "{0}" attribute must be less than or equal to {1}', 'value', t('the value of the "{0}" attribute', 'max'))
|
|
49
|
-
: t('The value of the "{0}" attribute must be less than or equal to {1}', 'value', t('{0} when the "{1}" attribute is absent', 'one', 'max'));
|
|
48
|
+
? t('The value of the "{0*}" attribute must be less than or equal to {1}', 'value', t('the value of the "{0*}" attribute', 'max'))
|
|
49
|
+
: t('The value of the "{0*}" attribute must be less than or equal to {1}', 'value', t('{0} when the "{1*}" attribute is absent', 'one', 'max'));
|
|
50
50
|
report({
|
|
51
51
|
scope: valueAttr,
|
|
52
52
|
line: valueAttr.valueNode?.startLine,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RuleConfigValue } from '@markuplint/ml-core';
|
|
1
2
|
/**
|
|
2
3
|
* Looks for the ancestor that the content model asks for (`descendantOf`) in the tree of the
|
|
3
4
|
* element, as the DOM does: the climb stops at the top of the content of a `template` element, and
|
|
@@ -15,8 +16,23 @@
|
|
|
15
16
|
* or the `template` of a framework whose spec puts the children in the element itself) is not an HTML
|
|
16
17
|
* `template` element, and the climb goes through it as through any other element.
|
|
17
18
|
*
|
|
19
|
+
* A component whose output is not known (`MLElement#isUnresolvedComponent`: no pretender stands for
|
|
20
|
+
* it) may render the ancestor, so an element with such a component above it, up to the top of its
|
|
21
|
+
* tree, is not reported: the rule does not decide what the file does not tell. A pretender that
|
|
22
|
+
* says what the component renders is read as that element, and a web component is the element
|
|
23
|
+
* that it is written as. What renders its children in place is not one even where the parser sees it
|
|
24
|
+
* as a component: a fragment (Vue's `slot` is one to its parser) and a `template` with a directive
|
|
25
|
+
* ({@link isUnknownOutput}).
|
|
26
|
+
*
|
|
27
|
+
* When the content model says the ancestor is required only of an element with a parent node
|
|
28
|
+
* (`descendantOfWithParentNode`, as the `area` element's is), an element that has no parent node in
|
|
29
|
+
* the file ({@link hasParentNode}) is not reported:
|
|
30
|
+
*
|
|
31
|
+
* > An area element with a parent node must have a map element ancestor.
|
|
32
|
+
*
|
|
18
33
|
* @see https://dom.spec.whatwg.org/#parent-element
|
|
19
34
|
* @see https://html.spec.whatwg.org/multipage/scripting.html#the-template-element
|
|
35
|
+
* @see https://html.spec.whatwg.org/multipage/image-maps.html#the-area-element
|
|
20
36
|
*/
|
|
21
|
-
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<
|
|
37
|
+
declare const _default: Readonly<import("@markuplint/ml-core").RuleSeed<RuleConfigValue, undefined>>;
|
|
22
38
|
export default _default;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
-
import { isShadowRoot } from '@markuplint/ml-spec';
|
|
2
|
+
import { getSpecByTagName, isShadowRoot } from '@markuplint/ml-spec';
|
|
3
|
+
import { isRenderedAway, isUnknownOutput } from '../helpers.js';
|
|
3
4
|
import meta from './meta.js';
|
|
4
5
|
/**
|
|
5
6
|
* Looks for the ancestor that the content model asks for (`descendantOf`) in the tree of the
|
|
@@ -18,15 +19,30 @@ import meta from './meta.js';
|
|
|
18
19
|
* or the `template` of a framework whose spec puts the children in the element itself) is not an HTML
|
|
19
20
|
* `template` element, and the climb goes through it as through any other element.
|
|
20
21
|
*
|
|
22
|
+
* A component whose output is not known (`MLElement#isUnresolvedComponent`: no pretender stands for
|
|
23
|
+
* it) may render the ancestor, so an element with such a component above it, up to the top of its
|
|
24
|
+
* tree, is not reported: the rule does not decide what the file does not tell. A pretender that
|
|
25
|
+
* says what the component renders is read as that element, and a web component is the element
|
|
26
|
+
* that it is written as. What renders its children in place is not one even where the parser sees it
|
|
27
|
+
* as a component: a fragment (Vue's `slot` is one to its parser) and a `template` with a directive
|
|
28
|
+
* ({@link isUnknownOutput}).
|
|
29
|
+
*
|
|
30
|
+
* When the content model says the ancestor is required only of an element with a parent node
|
|
31
|
+
* (`descendantOfWithParentNode`, as the `area` element's is), an element that has no parent node in
|
|
32
|
+
* the file ({@link hasParentNode}) is not reported:
|
|
33
|
+
*
|
|
34
|
+
* > An area element with a parent node must have a map element ancestor.
|
|
35
|
+
*
|
|
21
36
|
* @see https://dom.spec.whatwg.org/#parent-element
|
|
22
37
|
* @see https://html.spec.whatwg.org/multipage/scripting.html#the-template-element
|
|
38
|
+
* @see https://html.spec.whatwg.org/multipage/image-maps.html#the-area-element
|
|
23
39
|
*/
|
|
24
40
|
export default createRule({
|
|
25
41
|
meta: meta,
|
|
26
42
|
async verify({ document, report, t }) {
|
|
27
43
|
await document.walkOn('Element', el => {
|
|
28
|
-
const
|
|
29
|
-
const descendantOf =
|
|
44
|
+
const contentModel = getSpecByTagName(document.specs.specs, el.localName, el.namespaceURI)?.contentModel;
|
|
45
|
+
const descendantOf = contentModel?.descendantOf;
|
|
30
46
|
if (!descendantOf) {
|
|
31
47
|
return;
|
|
32
48
|
}
|
|
@@ -37,9 +53,15 @@ export default createRule({
|
|
|
37
53
|
found = true;
|
|
38
54
|
break;
|
|
39
55
|
}
|
|
56
|
+
// What the component renders is not known, so the ancestor may be among it. The climb does not
|
|
57
|
+
// leave the tree of the element (`treeParentElement`), so a component outside a declarative shadow
|
|
58
|
+
// root is not reached
|
|
59
|
+
if (isUnknownOutput(ancestor)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
40
62
|
ancestor = ancestor.treeParentElement;
|
|
41
63
|
}
|
|
42
|
-
if (found) {
|
|
64
|
+
if (found || (contentModel.descendantOfWithParentNode && !hasParentNode(el))) {
|
|
43
65
|
return;
|
|
44
66
|
}
|
|
45
67
|
const root = el.getRootNode();
|
|
@@ -48,8 +70,33 @@ export default createRule({
|
|
|
48
70
|
}
|
|
49
71
|
report({
|
|
50
72
|
scope: el,
|
|
51
|
-
message: t('{0} must appear as a descendant of {1}', t('the "{0}" {1}', el.localName, 'element'), t('the "{0}" {1}', descendantOf, 'element')),
|
|
73
|
+
message: t('{0} must appear as a descendant of {1}', t('the "{0*}" {1}', el.localName, 'element'), t('the "{0*}" {1}', descendantOf, 'element')),
|
|
52
74
|
});
|
|
53
75
|
});
|
|
54
76
|
},
|
|
55
77
|
});
|
|
78
|
+
/**
|
|
79
|
+
* Whether the element has a parent node, as far as the file tells. In a whole document every element
|
|
80
|
+
* has one. An element at the top of a file that is a fragment of a document (a partial, a component)
|
|
81
|
+
* has none: where it is inserted is not known from the markup. What is rendered away between the
|
|
82
|
+
* element and the top of the file is not a parent node: a block of a template language, which
|
|
83
|
+
* `parentNode` (and so `parentElement`) walks through, and a fragment of JSX or a `template` with
|
|
84
|
+
* a directive ({@link isRenderedAway}).
|
|
85
|
+
*
|
|
86
|
+
* @template T - The rule configuration value type
|
|
87
|
+
* @template O - The rule options type
|
|
88
|
+
* @param el - An element
|
|
89
|
+
* @returns `false` if the element is at the top of a fragment document
|
|
90
|
+
*/
|
|
91
|
+
function hasParentNode(
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
93
|
+
el) {
|
|
94
|
+
if (!el.isInFragmentDocument()) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
let parent = el.parentElement;
|
|
98
|
+
while (parent && isRenderedAway(parent)) {
|
|
99
|
+
parent = parent.parentElement;
|
|
100
|
+
}
|
|
101
|
+
return parent !== null;
|
|
102
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
declare const _default: {
|
|
2
2
|
readonly category: "structure";
|
|
3
|
+
readonly specConformance: {
|
|
4
|
+
readonly sources: readonly ["html"];
|
|
5
|
+
readonly level: "must";
|
|
6
|
+
readonly cites: readonly ["https://html.spec.whatwg.org/multipage/image-maps.html#the-area-element"];
|
|
7
|
+
};
|
|
3
8
|
};
|
|
4
9
|
export default _default;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRule, getAttrSpecs } from '@markuplint/ml-core';
|
|
2
2
|
import { attrMatches, match } from '../helpers.js';
|
|
3
|
+
import { noTranslate } from '../no-translate.js';
|
|
3
4
|
import meta from './meta.js';
|
|
4
5
|
export default createRule({
|
|
5
6
|
meta: meta,
|
|
@@ -107,7 +108,9 @@ export default createRule({
|
|
|
107
108
|
if (spec.values.length > 0 && value) {
|
|
108
109
|
const matched = spec.values.some(pattern => match(value, pattern));
|
|
109
110
|
if (!matched) {
|
|
110
|
-
const expects = spec.values.length === 1
|
|
111
|
+
const expects = spec.values.length === 1
|
|
112
|
+
? t(spec.values.map(noTranslate))
|
|
113
|
+
: t('either {0}', t(spec.values.map(noTranslate)));
|
|
111
114
|
const message = t('{0} expects {1}', t('the "{0*}" {1}', spec.name, 'attribute'), expects);
|
|
112
115
|
const attrToken = el.getAttributeToken(spec.name);
|
|
113
116
|
const valueToken = attrToken[0]?.valueNode;
|
|
@@ -9,7 +9,8 @@ export default createRule({
|
|
|
9
9
|
},
|
|
10
10
|
verify({ document, report, t }) {
|
|
11
11
|
for (const time of document.deepQuerySelectorAll('time:not([datetime])')) {
|
|
12
|
-
|
|
12
|
+
// The text of the element is the text of its descendants, which a component may render
|
|
13
|
+
if (time.hasMutableChildren() || [...time.querySelectorAll('*')].some(el => el.isUnresolvedComponent())) {
|
|
13
14
|
continue;
|
|
14
15
|
}
|
|
15
16
|
const content = time.textContent.trim();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRule } from '@markuplint/ml-core';
|
|
2
|
+
import { mayRenderUnknownChildren } from '../helpers.js';
|
|
2
3
|
import meta from './meta.js';
|
|
3
4
|
export default createRule({
|
|
4
5
|
meta: meta,
|
|
@@ -8,7 +9,10 @@ export default createRule({
|
|
|
8
9
|
ignoreOmittedElements: true,
|
|
9
10
|
},
|
|
10
11
|
async verify({ document, report, t }) {
|
|
11
|
-
|
|
12
|
+
// The element that a global rule asks for may be anywhere, so what any component, block or
|
|
13
|
+
// descendant renders may be it
|
|
14
|
+
const hasMutableContent = document.rule.options.ignoreHasMutableContents &&
|
|
15
|
+
document.nodeList.some(n => n.is(n.ELEMENT_NODE) && (n.hasMutableChildren() || n.isUnresolvedComponent()));
|
|
12
16
|
if (!hasMutableContent) {
|
|
13
17
|
for (const query of document.rule.value) {
|
|
14
18
|
const matched = document.querySelectorAll(query);
|
|
@@ -30,7 +34,9 @@ export default createRule({
|
|
|
30
34
|
if (el.rule.value === document.rule.value) {
|
|
31
35
|
return;
|
|
32
36
|
}
|
|
33
|
-
|
|
37
|
+
// What a descendant renders is not a child of the element that has to have the required one
|
|
38
|
+
if (el.rule.options.ignoreHasMutableContents &&
|
|
39
|
+
[...el.getPureChildNodes()].some(child => mayRenderUnknownChildren(child))) {
|
|
34
40
|
return;
|
|
35
41
|
}
|
|
36
42
|
for (const query of el.rule.value) {
|
|
@@ -47,7 +47,7 @@ export default createRule({
|
|
|
47
47
|
line: usemapAttr.valueNode?.startLine,
|
|
48
48
|
col: usemapAttr.valueNode?.startCol,
|
|
49
49
|
raw: usemapAttr.valueNode?.raw,
|
|
50
|
-
message: t('The "{0}" attribute of the "{1}" element must be a valid hash-name reference to a "{2}" element', 'usemap', 'img', 'map'),
|
|
50
|
+
message: t('The "{0*}" attribute of the "{1*}" element must be a valid hash-name reference to a "{2*}" element', 'usemap', 'img', 'map'),
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
53
|
});
|