@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/lib/attr-value-quotes/index.js +1 -1
  3. package/lib/create-message.js +6 -5
  4. package/lib/form-attr-references-form/index.js +1 -1
  5. package/lib/helpers.d.ts +54 -1
  6. package/lib/helpers.js +99 -3
  7. package/lib/input-list-references-datalist/index.js +1 -1
  8. package/lib/itemprop-requires-itemscope/index.js +1 -1
  9. package/lib/label-for-references-labelable/index.js +1 -1
  10. package/lib/label-no-multiple-controls/index.js +2 -2
  11. package/lib/link-types/index.js +8 -8
  12. package/lib/map-id-name-match/index.js +1 -1
  13. package/lib/meter-value-bounds/index.js +3 -3
  14. package/lib/no-ambiguous-navigable-target-names/index.js +1 -1
  15. package/lib/no-aria-hidden-on-hidden-until-found/index.js +1 -1
  16. package/lib/no-content-after-body/index.js +2 -2
  17. package/lib/no-disallowed-ancestor/index.js +5 -16
  18. package/lib/no-disallowed-attr/index.js +1 -1
  19. package/lib/no-duplicate-autofocus/index.js +1 -1
  20. package/lib/no-duplicate-sibling-attr/index.js +1 -1
  21. package/lib/no-duplicate-visible-main/index.js +1 -1
  22. package/lib/no-extra-selected-options/index.js +1 -1
  23. package/lib/no-input-file-value/index.js +1 -1
  24. package/lib/no-malformed-character-reference/index.js +1 -1
  25. package/lib/no-redundant-accessible-name/index.js +7 -1
  26. package/lib/no-restricted-attr/index.js +2 -1
  27. package/lib/no-stray-head-or-body-tag/index.js +2 -2
  28. package/lib/no-table-cell-overlap/index.js +2 -1
  29. package/lib/no-translate.d.ts +17 -0
  30. package/lib/no-translate.js +29 -0
  31. package/lib/no-unclosed-element-at-eof/index.js +1 -1
  32. package/lib/no-unsupported-browser-features/index.d.ts +9 -0
  33. package/lib/no-unsupported-browser-features/index.js +10 -7
  34. package/lib/permitted-contents/content-model.d.ts +14 -0
  35. package/lib/permitted-contents/content-model.js +21 -1
  36. package/lib/permitted-contents/index.js +4 -4
  37. package/lib/permitted-contents/matches-selector.js +4 -0
  38. package/lib/permitted-contents/slot-content.d.ts +16 -0
  39. package/lib/permitted-contents/slot-content.js +21 -0
  40. package/lib/permitted-contents/start.d.ts +8 -1
  41. package/lib/permitted-contents/start.js +36 -5
  42. package/lib/progress-value-bounds/index.js +2 -2
  43. package/lib/require-ancestor/index.d.ts +17 -1
  44. package/lib/require-ancestor/index.js +52 -5
  45. package/lib/require-ancestor/meta.d.ts +5 -0
  46. package/lib/require-ancestor/meta.js +5 -0
  47. package/lib/require-attr/index.js +4 -1
  48. package/lib/require-datetime/index.js +2 -1
  49. package/lib/require-element/index.js +8 -2
  50. package/lib/usemap-references-map/index.js +1 -1
  51. package/lib/wai-aria/checkings/contradictory-aria-prop.js +2 -2
  52. package/lib/wai-aria/checkings/redundant-aria-prop.js +1 -1
  53. package/lib/wai-aria/checkings/required-accessibility-parent-role.js +2 -1
  54. package/lib/wai-aria/checkings/required-owned-elements.d.ts +2 -0
  55. package/lib/wai-aria/checkings/required-owned-elements.js +175 -21
  56. package/lib/wai-aria/checkings/value.js +3 -2
  57. 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: formatUnsupportedMessage(t, elName, 'element', unsupported),
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: formatUnsupportedMessage(t, attrName, 'attribute', unsupported),
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 formatUnsupportedMessage(t, featureName, featureType, unsupported) {
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 `${u.displayName} (removed in ${u.removedVersion})`;
75
+ return t('{0*} (removed in {1*})', u.displayName, u.removedVersion);
73
76
  }
74
- return `${u.displayName} (not supported)`;
77
+ return t('{0*} (not supported)', u.displayName);
75
78
  }
76
- return `${u.displayName} (added in ${u.addedVersion}, target: ${u.targetVersion})`;
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 result = start(model, el, rules, specs, options, mode);
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.hasMutableChildren() ||
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.hasMutableChildren() ||
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
- const childNodesPatterns = givenPatterns.map(given => {
28
- const children = expandChildren(el, given, mode).filter(child => !isComment(child));
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<import("@markuplint/ml-core").RuleConfigValue, undefined>>;
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 elSpec = document.specs.specs.find(s => s.name === el.localName);
29
- const descendantOf = elSpec?.contentModel?.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,3 +1,8 @@
1
1
  export default {
2
2
  category: 'structure',
3
+ specConformance: {
4
+ sources: ['html'],
5
+ level: 'must',
6
+ cites: ['https://html.spec.whatwg.org/multipage/image-maps.html#the-area-element'],
7
+ },
3
8
  };
@@ -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 ? t(spec.values) : t('either {0}', t(spec.values));
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
- if (time.hasMutableChildren()) {
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
- const hasMutableContent = document.nodeList.some(n => n.is(n.ELEMENT_NODE) && n.hasMutableChildren());
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
- if (el.rule.options.ignoreHasMutableContents && el.hasMutableChildren()) {
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
  });