@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
package/CHANGELOG.md CHANGED
@@ -3,6 +3,19 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [5.6.0](https://github.com/markuplint/markuplint/compare/v5.5.0...v5.6.0) (2026-11-12)
7
+
8
+ ### Bug Fixes
9
+
10
+ * do not report an area element that has no parent node in the file ([916a166](https://github.com/markuplint/markuplint/commit/916a166916e4efd5292402abcd293927605ff799)), closes [#4528](https://github.com/markuplint/markuplint/issues/4528)
11
+ * **rules:** do not report an area that a component of unknown output may enclose in a map ([91f796a](https://github.com/markuplint/markuplint/commit/91f796acd865c4815c66bf733a80c838b6f43be3)), closes [#4575](https://github.com/markuplint/markuplint/issues/4575)
12
+ * **rules:** do not report what a component that no pretender resolved may render ([ecf810c](https://github.com/markuplint/markuplint/commit/ecf810c021b8fde9a6053b43293c5261f3911a46)), closes [#4572](https://github.com/markuplint/markuplint/issues/4572) [#4064](https://github.com/markuplint/markuplint/issues/4064)
13
+ * **rules:** read the declarative shadow root as owned by its host ([2324117](https://github.com/markuplint/markuplint/commit/2324117dc83627db98f1f2f2b2f6d26d6886708f)), closes [#4527](https://github.com/markuplint/markuplint/issues/4527)
14
+ * **rules:** read what a slot of a shadow root renders ([d3a5c76](https://github.com/markuplint/markuplint/commit/d3a5c76f5293322986ad0a122f13884f971735c5))
15
+ * **rules:** show every message in Japanese and the names in it as written ([9145904](https://github.com/markuplint/markuplint/commit/9145904e5b43fbeb51b5d5efa6e4735eddef7921))
16
+ * **rules:** write "an ambiguous" in the message of no-ambiguous-navigable-target-names ([9be22b3](https://github.com/markuplint/markuplint/commit/9be22b3949e114f08308d068c238666206384a45)), closes [#4585](https://github.com/markuplint/markuplint/issues/4585)
17
+
18
+
6
19
  # [5.5.0](https://github.com/markuplint/markuplint/compare/v5.4.0...v5.5.0) (2026-11-10)
7
20
 
8
21
  ### Bug Fixes
@@ -13,7 +13,7 @@ export default createRule({
13
13
  defaultValue: 'double',
14
14
  async verify({ document, report, t }) {
15
15
  await document.walkOn('Attr', attr => {
16
- const message = t('{0} is must {1} on {2}', 'Attribute value', 'quote', `${attr.rule.value} quotation mark`);
16
+ const message = t('{0} must be quoted with {1}', 'Attribute value', attr.rule.value === 'double' ? 'double quotation marks' : 'single quotation marks');
17
17
  if (attr.isExpressionValue || attr.isDirective || !attr.equal || attr.equal?.raw === '') {
18
18
  return;
19
19
  }
@@ -1,4 +1,5 @@
1
1
  import { isList, isKeyword, isEnum, isNumber, isDirective, isPattern } from '@markuplint/types';
2
+ import { noTranslate } from './no-translate.js';
2
3
  export function createMessageValueExpected(t, baseTarget, type, matches) {
3
4
  let target = baseTarget;
4
5
  let listDescriptionPart;
@@ -53,7 +54,7 @@ export function __createMessageValueExpected(t, baseTarget, expected, matches) {
53
54
  }
54
55
  case 'extra-token': {
55
56
  const token = matches.partName ?? 'token';
56
- reasonPart = t('Found {0}', t('extra {0}', token)) + ' ' + t([matches.raw]);
57
+ reasonPart = t('Found {0}', t('extra {0}', token)) + ' ' + t([noTranslate(matches.raw)]);
57
58
  if (matches.extra) {
58
59
  const extra = expectValueToWord(t, matches.extra, 'Any');
59
60
  unnecessaryPart = t("It doesn't need {0}", extra);
@@ -212,11 +213,11 @@ function createExpectedObject(type, matches, t) {
212
213
  }
213
214
  else if (isKeyword(type)) {
214
215
  if (type[0] === '<') {
215
- expectedObject.push(t('the CSS Syntax "{0}"', type));
216
+ expectedObject.push(t('the CSS Syntax "{0*}"', type));
216
217
  }
217
218
  }
218
219
  else if (isEnum(type)) {
219
- expectedObject.push(...type.enum);
220
+ expectedObject.push(...type.enum.map(noTranslate));
220
221
  }
221
222
  else if (isNumber(type)) {
222
223
  expectedObject.push(createExpectedNumber(t, type));
@@ -240,7 +241,7 @@ function expectValueToWord(t, expect, type) {
240
241
  return expect.value;
241
242
  }
242
243
  case 'const': {
243
- return expect.value ? `%${expect.value}%` : '';
244
+ return noTranslate(expect.value);
244
245
  }
245
246
  case 'format': {
246
247
  return t('the {0} format', expect.value);
@@ -250,7 +251,7 @@ function expectValueToWord(t, expect, type) {
250
251
  }
251
252
  case 'syntax': {
252
253
  if (isKeyword(type) && type[0] === '<') {
253
- return t('the CSS Syntax "{0}"', expect.value);
254
+ return t('the CSS Syntax "{0*}"', expect.value);
254
255
  }
255
256
  return t('{0} syntax', expect.value);
256
257
  }
@@ -59,7 +59,7 @@ export default createRule({
59
59
  line: formAttr.valueNode?.startLine,
60
60
  col: formAttr.valueNode?.startCol,
61
61
  raw: formAttr.valueNode?.raw,
62
- message: t('The "{0}" attribute on a form-associated element must reference a "{1}" element', 'form', 'form'),
62
+ message: t('The "{0*}" attribute on a form-associated element must reference a "{1*}" element', 'form', 'form'),
63
63
  });
64
64
  }
65
65
  });
package/lib/helpers.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { FixToken, IRuleFixer, PlainData, TextEdit } from '@markuplint/ml-config';
2
- import type { Attr, Element, RuleConfigValue } from '@markuplint/ml-core';
2
+ import type { Attr, ChildNode, Element, RuleConfigValue } from '@markuplint/ml-core';
3
3
  import type { Attribute } from '@markuplint/ml-spec';
4
4
  import type { WritableDeep } from 'type-fest';
5
5
  import { getDOMValue } from '@markuplint/ml-core';
@@ -21,6 +21,38 @@ export { getDOMValue };
21
21
  * @see https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute
22
22
  */
23
23
  export declare function getElementByIdInTree<T extends RuleConfigValue, O extends PlainData>(el: Element<T, O>, reference: string, isExpression?: boolean): Element<T, O> | null;
24
+ /**
25
+ * Whether the element is not an element of the output, so that what is written in it is rendered in
26
+ * place of it:
27
+ *
28
+ * - a fragment (`<>` of JSX), which `childNodes` already flattens;
29
+ * - a `template` element with a directive attribute (Vue's `<template v-if>`, `v-for`, `v-slot`).
30
+ * It is not an HTML `template` element: the template engine renders its children in place of it
31
+ * and drops it. A spread attribute (`{...props}`) is not such a directive: it sets attributes of
32
+ * the element, which stays.
33
+ *
34
+ * `isTransparent` of `@markuplint/ml-core` (`src/ml-dom/helper/tree-scope.ts`), which is not exported,
35
+ * decides the same of an element for the tree boundaries (`getRootNode()`, the shadow host): a kind
36
+ * added here belongs to it too.
37
+ *
38
+ * @template T - The rule configuration value type
39
+ * @template O - The rule options type
40
+ * @param el - An element
41
+ * @returns `true` if the output has no element for it
42
+ */
43
+ export declare function isRenderedAway<T extends RuleConfigValue, O extends PlainData>(el: Element<T, O>): boolean;
44
+ /**
45
+ * Whether the element is a component whose output is not known, so that it may render an element that
46
+ * a rule looks for among the ancestors (`MLElement#isUnresolvedComponent`). What the parser sees as a
47
+ * component but renders its children in place is not one: what {@link isRenderedAway} says (a
48
+ * fragment such as `React.Fragment` or Astro's `Fragment`, a `template` with a directive).
49
+ *
50
+ * @template T - The rule configuration value type
51
+ * @template O - The rule options type
52
+ * @param el - An ancestor of the element
53
+ * @returns `true` if the element may render what is not known
54
+ */
55
+ export declare function isUnknownOutput<T extends RuleConfigValue, O extends PlainData>(el: Element<T, O>): boolean;
24
56
  /**
25
57
  * The elements that match the selector in the tree `el` is in (see {@link getElementByIdInTree}).
26
58
  *
@@ -156,6 +188,27 @@ export declare function match(needle: string, pattern: string): boolean;
156
188
  */
157
189
  export declare const rePCENChar: string;
158
190
  export declare function toNormalizedValue(value: string, spec: Attribute): string;
191
+ /**
192
+ * Whether a child of an element may stand for children that are not known: a block of a preprocessor
193
+ * that has no `blockBehavior` (an output such as `{value}` or an `include`), a `slot`, or a component
194
+ * whose output is not known (`MLElement#isUnresolvedComponent`).
195
+ *
196
+ * It is for a rule that asks which children an element has (the required children, the elements that
197
+ * satisfy a query). It looks at the child alone: a descendant of the child is not a child of the
198
+ * element, so what it renders cannot become the child that the rule asks for, and the attributes of
199
+ * a child do not decide whether the child exists. `MLElement#hasMutableChildren` looks at every
200
+ * descendant, which is the question for a rule that asks what the descendants are or have.
201
+ * `require-datetime` asks about the text, which any descendant may change, and looks at every
202
+ * descendant for an unresolved component itself.
203
+ *
204
+ * @template T - The rule configuration value type
205
+ * @template O - The rule options type
206
+ * @param node - A child node of the element
207
+ * @param components - Whether a component whose output is not known counts. It does not when the
208
+ * rule evaluates the components by the name that they are written with
209
+ * @returns True if the node may stand for children that are not known
210
+ */
211
+ export declare function mayRenderUnknownChildren<T extends RuleConfigValue, O extends PlainData>(node: ChildNode<T, O>, components?: boolean): boolean;
159
212
  export declare function accnameMayBeMutable(el: Element<any, any>): boolean;
160
213
  /**
161
214
  * Whether the accessible name of `el` may be rendered by content the pretender
package/lib/helpers.js CHANGED
@@ -26,6 +26,57 @@ el, reference, isExpression = false) {
26
26
  }
27
27
  return null;
28
28
  }
29
+ /**
30
+ * Whether the element is not an element of the output, so that what is written in it is rendered in
31
+ * place of it:
32
+ *
33
+ * - a fragment (`<>` of JSX), which `childNodes` already flattens;
34
+ * - a `template` element with a directive attribute (Vue's `<template v-if>`, `v-for`, `v-slot`).
35
+ * It is not an HTML `template` element: the template engine renders its children in place of it
36
+ * and drops it. A spread attribute (`{...props}`) is not such a directive: it sets attributes of
37
+ * the element, which stays.
38
+ *
39
+ * `isTransparent` of `@markuplint/ml-core` (`src/ml-dom/helper/tree-scope.ts`), which is not exported,
40
+ * decides the same of an element for the tree boundaries (`getRootNode()`, the shadow host): a kind
41
+ * added here belongs to it too.
42
+ *
43
+ * @template T - The rule configuration value type
44
+ * @template O - The rule options type
45
+ * @param el - An element
46
+ * @returns `true` if the output has no element for it
47
+ */
48
+ export function isRenderedAway(
49
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
50
+ el) {
51
+ if (el.isFragment) {
52
+ return true;
53
+ }
54
+ if (el.localName !== 'template') {
55
+ return false;
56
+ }
57
+ for (const attr of el.attributes) {
58
+ if (attr.isDirective && attr.localName !== '#spread') {
59
+ return true;
60
+ }
61
+ }
62
+ return false;
63
+ }
64
+ /**
65
+ * Whether the element is a component whose output is not known, so that it may render an element that
66
+ * a rule looks for among the ancestors (`MLElement#isUnresolvedComponent`). What the parser sees as a
67
+ * component but renders its children in place is not one: what {@link isRenderedAway} says (a
68
+ * fragment such as `React.Fragment` or Astro's `Fragment`, a `template` with a directive).
69
+ *
70
+ * @template T - The rule configuration value type
71
+ * @template O - The rule options type
72
+ * @param el - An ancestor of the element
73
+ * @returns `true` if the element may render what is not known
74
+ */
75
+ export function isUnknownOutput(
76
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
77
+ el) {
78
+ return !isRenderedAway(el) && el.isUnresolvedComponent();
79
+ }
29
80
  /**
30
81
  * The elements that match the selector in the tree `el` is in (see {@link getElementByIdInTree}).
31
82
  *
@@ -319,18 +370,60 @@ export function toNormalizedValue(value, spec) {
319
370
  }
320
371
  return normalized;
321
372
  }
373
+ /**
374
+ * Whether a child of an element may stand for children that are not known: a block of a preprocessor
375
+ * that has no `blockBehavior` (an output such as `{value}` or an `include`), a `slot`, or a component
376
+ * whose output is not known (`MLElement#isUnresolvedComponent`).
377
+ *
378
+ * It is for a rule that asks which children an element has (the required children, the elements that
379
+ * satisfy a query). It looks at the child alone: a descendant of the child is not a child of the
380
+ * element, so what it renders cannot become the child that the rule asks for, and the attributes of
381
+ * a child do not decide whether the child exists. `MLElement#hasMutableChildren` looks at every
382
+ * descendant, which is the question for a rule that asks what the descendants are or have.
383
+ * `require-datetime` asks about the text, which any descendant may change, and looks at every
384
+ * descendant for an unresolved component itself.
385
+ *
386
+ * @template T - The rule configuration value type
387
+ * @template O - The rule options type
388
+ * @param node - A child node of the element
389
+ * @param components - Whether a component whose output is not known counts. It does not when the
390
+ * rule evaluates the components by the name that they are written with
391
+ * @returns True if the node may stand for children that are not known
392
+ */
393
+ export function mayRenderUnknownChildren(
394
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
395
+ node, components = true) {
396
+ if (node.is(node.MARKUPLINT_PREPROCESSOR_BLOCK)) {
397
+ return !node.blockBehavior;
398
+ }
399
+ return node.is(node.ELEMENT_NODE) && (node.localName === 'slot' || (components && node.isUnresolvedComponent()));
400
+ }
322
401
  export function accnameMayBeMutable(
323
402
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
324
403
  el) {
325
- if (el.hasMutableAttributes() || el.hasMutableChildren(true)) {
404
+ if (el.hasMutableAttributes() || el.hasMutableChildren(true, isSlotOfUnknownAssignment)) {
326
405
  return true;
327
406
  }
328
407
  const ownedLabel = getOwnedLabel(el);
329
- if (ownedLabel && (ownedLabel.hasMutableAttributes() || ownedLabel.hasMutableChildren(true))) {
408
+ if (ownedLabel &&
409
+ (ownedLabel.hasMutableAttributes() || ownedLabel.hasMutableChildren(true, isSlotOfUnknownAssignment))) {
330
410
  return true;
331
411
  }
332
412
  return false;
333
413
  }
414
+ /**
415
+ * Whether a `slot` renders what the markup does not tell: the nodes assigned to it, or its children, are
416
+ * not known (`MLElement#getAssignedNodes` is `null`). A slot whose assignment is known renders them, which
417
+ * the rule reads itself.
418
+ *
419
+ * @param slot - A `slot` element
420
+ * @returns `true` if what the slot renders is not known
421
+ */
422
+ function isSlotOfUnknownAssignment(
423
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
424
+ slot) {
425
+ return slot.getAssignedNodes() === null;
426
+ }
334
427
  /**
335
428
  * Whether the accessible name of `el` may be rendered by content the pretender
336
429
  * does not know: an unknown entry (`{ dynamic: true }`) in the `contents` of
@@ -371,12 +464,15 @@ el) {
371
464
  * whose computed name is empty is not reported: the rule calls this only after
372
465
  * the computed name is empty. Light DOM text that remains in the computed name
373
466
  * is still a name, and the rule does not reach this function.
467
+ *
468
+ * Unknown `contents` of a pretended component count only when no declarative
469
+ * shadow root is attached to it: the shadow root is rendered in their place.
374
470
  */
375
471
  function hasUnknownContent(
376
472
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
377
473
  el) {
378
474
  const context = el.pretenderContext;
379
- if (context?.type === 'pretender' && context.slotContent?.mutable) {
475
+ if (context?.type === 'pretender' && context.slotContent?.mutable && !el.declarativeShadowRoot) {
380
476
  return true;
381
477
  }
382
478
  if (context?.type !== 'pretender' && el.isHTMLTemplateElement && el.declarativeShadowRootAttachment === 'unknown') {
@@ -40,7 +40,7 @@ export default createRule({
40
40
  line: listAttr.valueNode?.startLine,
41
41
  col: listAttr.valueNode?.startCol,
42
42
  raw: listAttr.valueNode?.raw,
43
- message: t('The "{0}" attribute of the "{1}" element must reference a "{2}" element', 'list', 'input', 'datalist'),
43
+ message: t('The "{0*}" attribute of the "{1*}" element must reference a "{2*}" element', 'list', 'input', 'datalist'),
44
44
  });
45
45
  }
46
46
  });
@@ -62,7 +62,7 @@ export default createRule({
62
62
  if (!hasAncestorScope && !isItemrefTarget && !treesWithDynamicItemref.has(el.getRootNode())) {
63
63
  report({
64
64
  scope: el,
65
- message: t('{0} must {1}', t('the {0}', 'element'), 'belong to an item via an ancestor `itemscope` or be referenced by an `itemref`'),
65
+ message: t('{0} must belong to an item via an ancestor `itemscope` or be referenced by an `itemref`', t('the {0}', 'element')),
66
66
  });
67
67
  }
68
68
  }
@@ -63,7 +63,7 @@ export default createRule({
63
63
  line: forAttr.valueNode?.startLine,
64
64
  col: forAttr.valueNode?.startCol,
65
65
  raw: forAttr.valueNode?.raw,
66
- message: t('The "{0}" attribute of the "{1}" element must reference a labelable element', 'for', 'label'),
66
+ message: t('The "{0*}" attribute of the "{1*}" element must reference a labelable element', 'for', 'label'),
67
67
  });
68
68
  }
69
69
  });
@@ -86,7 +86,7 @@ export default createRule({
86
86
  for (const control of controls) {
87
87
  report({
88
88
  scope: control,
89
- message: t('The "{0}" element must not contain a form-control descendant when the "{1}" attribute references an external labelable element', 'label', 'for'),
89
+ message: t('The "{0*}" element must not contain a form-control descendant when the "{1*}" attribute references an external labelable element', 'label', 'for'),
90
90
  });
91
91
  }
92
92
  return;
@@ -96,7 +96,7 @@ export default createRule({
96
96
  for (const control of controls.slice(1)) {
97
97
  report({
98
98
  scope: control,
99
- message: t('The "{0}" element may contain at most one form-control descendant ({1})', 'label', 'button, input, meter, output, progress, select, or textarea'),
99
+ message: t('The "{0*}" element may contain at most one form-control descendant ({1})', 'label', 'button, input, meter, output, progress, select, or textarea'),
100
100
  });
101
101
  }
102
102
  });
@@ -122,28 +122,28 @@ function validateWhatwgKeyword(def, context, t) {
122
122
  switch (context) {
123
123
  case 'link': {
124
124
  if (def.link === 'not allowed') {
125
- return t('The "{0*}" {1} is not allowed on the {2}', def.keyword, 'keyword', '"link" element');
125
+ return t('The "{0*}" {1} is not allowed on {2}', def.keyword, 'keyword', t('the "{0*}" {1}', 'link', 'element'));
126
126
  }
127
127
  return null;
128
128
  }
129
129
  case 'body-link': {
130
130
  if (def.link === 'not allowed') {
131
- return t('The "{0*}" {1} is not allowed on the {2}', def.keyword, 'keyword', '"link" element');
131
+ return t('The "{0*}" {1} is not allowed on {2}', def.keyword, 'keyword', t('the "{0*}" {1}', 'link', 'element'));
132
132
  }
133
133
  if (def.bodyOk !== 'Yes') {
134
- return t('The "{0*}" {1} is not allowed on the {2} inside the {3}', def.keyword, 'keyword', '"link" element', '"body" element');
134
+ return t('The "{0*}" {1} is not allowed on {2} inside {3}', def.keyword, 'keyword', t('the "{0*}" {1}', 'link', 'element'), t('the "{0*}" {1}', 'body', 'element'));
135
135
  }
136
136
  return null;
137
137
  }
138
138
  case 'a-area': {
139
139
  if (def.a === 'not allowed') {
140
- return t('The "{0*}" {1} is not allowed on the {2}', def.keyword, 'keyword', '"a" element');
140
+ return t('The "{0*}" {1} is not allowed on {2}', def.keyword, 'keyword', t('the "{0*}" {1}', 'a', 'element'));
141
141
  }
142
142
  return null;
143
143
  }
144
144
  case 'form': {
145
145
  if (def.form === 'not allowed') {
146
- return t('The "{0*}" {1} is not allowed on the {2}', def.keyword, 'keyword', '"form" element');
146
+ return t('The "{0*}" {1} is not allowed on {2}', def.keyword, 'keyword', t('the "{0*}" {1}', 'form', 'element'));
147
147
  }
148
148
  return null;
149
149
  }
@@ -161,19 +161,19 @@ function validateMicroformatContext(def, context, t) {
161
161
  case 'link':
162
162
  case 'body-link': {
163
163
  if (!def.link) {
164
- return t('The "{0*}" {1} is not allowed on the {2}', def.keyword, 'keyword', '"link" element');
164
+ return t('The "{0*}" {1} is not allowed on {2}', def.keyword, 'keyword', t('the "{0*}" {1}', 'link', 'element'));
165
165
  }
166
166
  return null;
167
167
  }
168
168
  case 'a-area': {
169
169
  if (!def.a) {
170
- return t('The "{0*}" {1} is not allowed on the {2}', def.keyword, 'keyword', '"a" element');
170
+ return t('The "{0*}" {1} is not allowed on {2}', def.keyword, 'keyword', t('the "{0*}" {1}', 'a', 'element'));
171
171
  }
172
172
  return null;
173
173
  }
174
174
  case 'form': {
175
175
  // Microformats don't define form context; reject
176
- return t('The "{0*}" {1} is not allowed on the {2}', def.keyword, 'keyword', '"form" element');
176
+ return t('The "{0*}" {1} is not allowed on {2}', def.keyword, 'keyword', t('the "{0*}" {1}', 'form', 'element'));
177
177
  }
178
178
  }
179
179
  }
@@ -32,7 +32,7 @@ export default createRule({
32
32
  line: idAttr.valueNode?.startLine,
33
33
  col: idAttr.valueNode?.startCol,
34
34
  raw: idAttr.valueNode?.raw,
35
- message: t('The "{0}" attribute on a "{1}" element must have the same value as the "{2}" attribute', 'id', 'map', 'name'),
35
+ message: t('The "{0*}" attribute on a "{1*}" element must have the same value as the "{2*}" attribute', 'id', 'map', 'name'),
36
36
  });
37
37
  });
38
38
  },
@@ -65,9 +65,9 @@ export default createRule({
65
65
  const valueValue = valueNum ?? minValue;
66
66
  const reportLE = (lowerName, lowerAttr, upperName, upperAttr, upperDefault) => {
67
67
  const upperDesc = upperAttr
68
- ? t('the value of the "{0}" attribute', upperName)
69
- : t('{0} when the "{1}" attribute is absent', upperDefault === 1 ? 'one' : String(upperDefault), upperName);
70
- const message = t('The value of the "{0}" attribute must be less than or equal to {1}', lowerName, upperDesc);
68
+ ? t('the value of the "{0*}" attribute', upperName)
69
+ : t('{0} when the "{1*}" attribute is absent', upperDefault === 1 ? 'one' : String(upperDefault), upperName);
70
+ const message = t('The value of the "{0*}" attribute must be less than or equal to {1}', lowerName, upperDesc);
71
71
  const scope = lowerAttr ?? upperAttr;
72
72
  if (scope) {
73
73
  report({
@@ -39,7 +39,7 @@ export default createRule({
39
39
  raw: attr.valueNode.raw,
40
40
  message:
41
41
  //
42
- t("don't use {0}", t('a {0}', t('ambiguous {0}', 'navigable target name'))) +
42
+ t("don't use {0}", t('an {0}', t('ambiguous {0}', 'navigable target name'))) +
43
43
  (attrSpec.type === 'NavigableTargetNameOrKeyword'
44
44
  ? t('. ') + t('Did you mean "{0}"?', valueWithUnderscore)
45
45
  : ''),
@@ -31,7 +31,7 @@ export default createRule({
31
31
  }
32
32
  report({
33
33
  scope: ariaHidden,
34
- message: t('"aria-hidden" must not be "{0}" on an element with "hidden" value "{1}"', 'true', 'until-found'),
34
+ message: t('"aria-hidden" must not be "{0*}" on an element with "hidden" value "{1*}"', 'true', 'until-found'),
35
35
  });
36
36
  });
37
37
  },
@@ -36,14 +36,14 @@ export default createRule({
36
36
  if (child.is(child.ELEMENT_NODE)) {
37
37
  report({
38
38
  scope: child,
39
- message: t('Content after the end tag of the "{0}" element is not allowed', 'body'),
39
+ message: t('Content after the end tag of the "{0*}" element is not allowed', 'body'),
40
40
  });
41
41
  continue;
42
42
  }
43
43
  if (child.is(child.TEXT_NODE) && child.raw.trim() !== '') {
44
44
  report({
45
45
  scope: child,
46
- message: t('Content after the end tag of the "{0}" element is not allowed', 'body'),
46
+ message: t('Content after the end tag of the "{0*}" element is not allowed', 'body'),
47
47
  });
48
48
  }
49
49
  }
@@ -1,5 +1,6 @@
1
1
  import { createRule } from '@markuplint/ml-core';
2
2
  import { getSpecByTagName } from '@markuplint/ml-spec';
3
+ import { isRenderedAway } from '../helpers.js';
3
4
  import meta from './meta.js';
4
5
  /**
5
6
  * Checks the ancestors of an element against its content model, which holds them in two forms
@@ -40,7 +41,7 @@ export default createRule({
40
41
  (permittedAncestors && !matchesPermitted(current, permittedAncestors))) {
41
42
  report({
42
43
  scope: el,
43
- message: t('{0} must not appear as a descendant of {1}', t('the "{0}" {1}', el.localName, 'element'), t('the "{0}" {1}', ancestor.localName, 'element')),
44
+ message: t('{0} must not appear as a descendant of {1}', t('the "{0*}" {1}', el.localName, 'element'), t('the "{0*}" {1}', ancestor.localName, 'element')),
44
45
  });
45
46
  break;
46
47
  }
@@ -58,12 +59,8 @@ export default createRule({
58
59
  * markuplint holds the content as children of the `template` element, so the walk stops here.
59
60
  * A `template` of a framework that puts the children in the element itself (React, Vue: the
60
61
  * `templateContent` of its spec) is an `element` below: the children are in the document.
61
- * - `transparent`: a node that is not an element of the document, which the walk skips as if it
62
- * were not there and does not judge:
63
- * - a fragment (`<>` of JSX), which `childNodes` already flattens;
64
- * - a `template` element with a directive attribute (Vue's `<template v-if>`, `v-for`, `v-slot`).
65
- * It is not an HTML `template` element: the template engine renders its children in place of
66
- * it and drops it.
62
+ * - `transparent`: a node that is not an element of the document ({@link isRenderedAway}), which
63
+ * the walk skips as if it were not there and does not judge.
67
64
  * - `element`: any other element.
68
65
  *
69
66
  * @see https://html.spec.whatwg.org/multipage/scripting.html#the-template-element
@@ -71,17 +68,9 @@ export default createRule({
71
68
  function getAncestorKind(
72
69
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
73
70
  ancestor) {
74
- if (ancestor.isFragment) {
71
+ if (isRenderedAway(ancestor)) {
75
72
  return 'transparent';
76
73
  }
77
- if (ancestor.localName !== 'template') {
78
- return 'element';
79
- }
80
- for (const attr of ancestor.attributes) {
81
- if (attr.isDirective) {
82
- return 'transparent';
83
- }
84
- }
85
74
  return ancestor.isHTMLTemplateElement ? 'template-content' : 'element';
86
75
  }
87
76
  /**
@@ -29,7 +29,7 @@ export default createRule({
29
29
  line: attrName?.startLine,
30
30
  col: attrName?.startCol,
31
31
  raw: attrName?.raw,
32
- message: t('The "{0}" attribute must not be specified on an autonomous custom element', 'is'),
32
+ message: t('The "{0*}" attribute must not be specified on an autonomous custom element', 'is'),
33
33
  });
34
34
  return;
35
35
  }
@@ -31,7 +31,7 @@ export default createRule({
31
31
  if (seenScopingRoots.has(scopingRoot)) {
32
32
  report({
33
33
  scope: el,
34
- message: t('The "{0}" attribute must be unique in the document', 'autofocus'),
34
+ message: t('The "{0*}" attribute must be unique in the document', 'autofocus'),
35
35
  });
36
36
  continue;
37
37
  }
@@ -26,7 +26,7 @@ export default createRule({
26
26
  if (hasPrecedingDuplicate) {
27
27
  report({
28
28
  scope: el,
29
- message: t('The "{0}" attribute must not appear on more than one "{1}" element within the same parent', attrName, el.localName),
29
+ message: t('The "{0*}" attribute must not appear on more than one "{1*}" element within the same parent', attrName, el.localName),
30
30
  });
31
31
  }
32
32
  }
@@ -20,7 +20,7 @@ export default createRule({
20
20
  for (const el of visibleMains.slice(1)) {
21
21
  report({
22
22
  scope: el,
23
- message: t('There must not be more than one visible "{0}" element in a document', 'main'),
23
+ message: t('There must not be more than one visible "{0*}" element in a document', 'main'),
24
24
  });
25
25
  }
26
26
  },
@@ -29,7 +29,7 @@ export default createRule({
29
29
  for (const opt of selected.slice(1)) {
30
30
  report({
31
31
  scope: opt,
32
- message: t('The "{0}" element cannot have more than one selected "{1}" descendant unless the "{2}" attribute is specified', 'select', 'option', 'multiple'),
32
+ message: t('The "{0*}" element cannot have more than one selected "{1*}" descendant unless the "{2*}" attribute is specified', 'select', 'option', 'multiple'),
33
33
  });
34
34
  }
35
35
  });
@@ -40,7 +40,7 @@ export default createRule({
40
40
  line: valueAttr.valueNode?.startLine,
41
41
  col: valueAttr.valueNode?.startCol,
42
42
  raw: valueAttr.valueNode?.raw,
43
- message: t('The "{0}" attribute on a "{1}" element with "{2}={3}" must be the empty string', 'value', 'input', 'type', 'file'),
43
+ message: t('The "{0*}" attribute on a "{1*}" element with "{2*}={3*}" must be the empty string', 'value', 'input', 'type', 'file'),
44
44
  });
45
45
  });
46
46
  },
@@ -31,7 +31,7 @@ export default createRule({
31
31
  line: pe.startLine,
32
32
  col: pe.startCol,
33
33
  raw: pe.raw,
34
- message: t('{0} is {1:c}', t('the {0}', 'character reference'), pe.code),
34
+ message: t('{0} is {1*}', t('the {0}', 'character reference'), pe.code),
35
35
  });
36
36
  }
37
37
  },
@@ -2,6 +2,12 @@ import { createRule, getComputedRole } from '@markuplint/ml-core';
2
2
  import { ARIA_RECOMMENDED_VERSION, isExposed } from '@markuplint/ml-spec';
3
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',
@@ -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
  }
@@ -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
  }