@markuplint/rules 5.4.0 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/CLAUDE.md +4 -3
  3. package/lib/aria-prop-requires-role/index.js +2 -2
  4. package/lib/attr-eligibility.d.ts +21 -0
  5. package/lib/attr-eligibility.js +86 -6
  6. package/lib/attr-order/index.js +2 -1
  7. package/lib/attr-value-quotes/index.js +2 -2
  8. package/lib/consistent-table-row-length/index.js +1 -1
  9. package/lib/create-message.js +6 -5
  10. package/lib/element-supports-aria-prop/index.js +2 -2
  11. package/lib/form-attr-references-form/index.js +3 -3
  12. package/lib/helpers.d.ts +96 -5
  13. package/lib/helpers.js +187 -29
  14. package/lib/input-list-references-datalist/index.js +3 -3
  15. package/lib/itemprop-requires-itemscope/index.js +28 -11
  16. package/lib/label-for-references-labelable/index.js +3 -3
  17. package/lib/label-has-control/index.d.ts +17 -0
  18. package/lib/label-has-control/index.js +28 -1
  19. package/lib/label-no-multiple-controls/index.d.ts +4 -5
  20. package/lib/label-no-multiple-controls/index.js +8 -8
  21. package/lib/landmark-roles/roles.d.ts +13 -1
  22. package/lib/landmark-roles/roles.js +22 -0
  23. package/lib/link-types/index.js +8 -8
  24. package/lib/map-id-name-match/index.js +1 -1
  25. package/lib/meter-value-bounds/index.js +3 -3
  26. package/lib/no-ambiguous-navigable-target-names/index.js +5 -1
  27. package/lib/no-aria-hidden-on-hidden-until-found/index.js +1 -1
  28. package/lib/no-aria-on-presentational-children/index.js +4 -0
  29. package/lib/no-aria-on-unsupported-element/index.js +2 -2
  30. package/lib/no-boolean-attr-value/index.js +2 -1
  31. package/lib/no-broken-fragment-link/index.d.ts +5 -0
  32. package/lib/no-broken-fragment-link/index.js +8 -2
  33. package/lib/no-consecutive-br/index.js +1 -1
  34. package/lib/no-content-after-body/index.js +2 -2
  35. package/lib/no-contradictory-aria-prop/index.js +2 -2
  36. package/lib/no-default-aria-value/index.js +2 -2
  37. package/lib/no-default-value/index.js +4 -0
  38. package/lib/no-deprecated-aria-prop/index.js +2 -2
  39. package/lib/no-disallowed-ancestor/index.d.ts +16 -1
  40. package/lib/no-disallowed-ancestor/index.js +68 -5
  41. package/lib/no-disallowed-attr/index.js +5 -1
  42. package/lib/no-duplicate-autofocus/index.js +1 -1
  43. package/lib/no-duplicate-dt/index.js +1 -1
  44. package/lib/no-duplicate-h1/index.js +18 -14
  45. package/lib/no-duplicate-id/index.d.ts +9 -0
  46. package/lib/no-duplicate-id/index.js +23 -4
  47. package/lib/no-duplicate-sibling-attr/index.js +1 -1
  48. package/lib/no-duplicate-visible-main/index.js +1 -1
  49. package/lib/no-empty-table-track/index.js +1 -1
  50. package/lib/no-extra-selected-options/index.js +1 -1
  51. package/lib/no-ineffective-attr/index.js +4 -0
  52. package/lib/no-input-file-value/index.js +1 -1
  53. package/lib/no-invalid-aria-prop-value/index.js +3 -3
  54. package/lib/no-invalid-attr-value/index.js +12 -0
  55. package/lib/no-malformed-character-reference/index.js +1 -1
  56. package/lib/no-nested-top-level-landmark/index.js +20 -17
  57. package/lib/no-prohibited-naming/index.js +2 -2
  58. package/lib/no-redundant-accessible-name/index.js +15 -8
  59. package/lib/no-redundant-aria-prop/index.js +2 -2
  60. package/lib/no-refer-to-non-existent-id/index.d.ts +15 -0
  61. package/lib/no-refer-to-non-existent-id/index.js +47 -16
  62. package/lib/no-restricted-attr/index.js +2 -1
  63. package/lib/no-restricted-element/index.js +4 -2
  64. package/lib/no-skipped-heading-level/index.js +9 -4
  65. package/lib/no-stray-head-or-body-tag/index.d.ts +2 -1
  66. package/lib/no-stray-head-or-body-tag/index.js +4 -3
  67. package/lib/no-table-cell-overlap/index.js +3 -2
  68. package/lib/no-table-span-overflow/index.js +1 -1
  69. package/lib/no-translate.d.ts +17 -0
  70. package/lib/no-translate.js +29 -0
  71. package/lib/no-unclosed-element-at-eof/index.js +1 -1
  72. package/lib/no-unescaped-char/index.js +1 -1
  73. package/lib/no-unsupported-browser-features/index.d.ts +9 -0
  74. package/lib/no-unsupported-browser-features/index.js +10 -7
  75. package/lib/permitted-contents/content-model.d.ts +14 -0
  76. package/lib/permitted-contents/content-model.js +21 -1
  77. package/lib/permitted-contents/index.js +6 -8
  78. package/lib/permitted-contents/matches-selector.js +32 -5
  79. package/lib/permitted-contents/represent-transparent-nodes.js +100 -68
  80. package/lib/permitted-contents/slot-content.d.ts +18 -1
  81. package/lib/permitted-contents/slot-content.js +49 -2
  82. package/lib/permitted-contents/start.d.ts +8 -1
  83. package/lib/permitted-contents/start.js +39 -6
  84. package/lib/permitted-contents/utils.d.ts +11 -0
  85. package/lib/permitted-contents/utils.js +15 -0
  86. package/lib/placeholder-label-option/index.js +4 -26
  87. package/lib/progress-value-bounds/index.js +2 -2
  88. package/lib/require-accessible-name/index.js +2 -2
  89. package/lib/require-adjacent-popover/index.js +4 -2
  90. package/lib/require-ancestor/index.d.ts +37 -1
  91. package/lib/require-ancestor/index.js +82 -9
  92. package/lib/require-ancestor/meta.d.ts +5 -0
  93. package/lib/require-ancestor/meta.js +5 -0
  94. package/lib/require-attr/index.js +4 -1
  95. package/lib/require-datetime/index.js +3 -2
  96. package/lib/require-dialog-autofocus/index.js +8 -6
  97. package/lib/require-element/index.js +8 -2
  98. package/lib/require-h1/index.js +4 -8
  99. package/lib/require-landmark-label/index.js +17 -15
  100. package/lib/role-supports-aria-prop/index.js +2 -2
  101. package/lib/sizes-auto-requires-lazy-loading/index.d.ts +4 -2
  102. package/lib/sizes-auto-requires-lazy-loading/index.js +4 -2
  103. package/lib/usemap-references-map/index.js +12 -3
  104. package/lib/valid-speculation-rules/index.js +1 -1
  105. package/lib/wai-aria/checkings/contradictory-aria-prop.js +2 -2
  106. package/lib/wai-aria/checkings/interaction-in-hidden.js +6 -2
  107. package/lib/wai-aria/checkings/presentational-children.js +38 -17
  108. package/lib/wai-aria/checkings/redundant-aria-prop.js +1 -1
  109. package/lib/wai-aria/checkings/required-accessibility-parent-role.js +4 -2
  110. package/lib/wai-aria/checkings/required-owned-elements.d.ts +2 -0
  111. package/lib/wai-aria/checkings/required-owned-elements.js +175 -21
  112. package/lib/wai-aria/checkings/required-prop.js +2 -1
  113. package/lib/wai-aria/checkings/tab-requires-tabpanel.js +10 -6
  114. package/lib/wai-aria/checkings/value.d.ts +1 -4
  115. package/lib/wai-aria/checkings/value.js +8 -19
  116. package/package.json +14 -11
package/CHANGELOG.md CHANGED
@@ -3,6 +3,40 @@
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
+
19
+ # [5.5.0](https://github.com/markuplint/markuplint/compare/v5.4.0...v5.5.0) (2026-11-10)
20
+
21
+ ### Bug Fixes
22
+
23
+ * **html-spec:** allow script-supporting elements in table sections, colgroup and dl ([197176a](https://github.com/markuplint/markuplint/commit/197176a7aae805e2949f9b96c6a27f39d478d261)), closes [#3591](https://github.com/markuplint/markuplint/issues/3591) [#4492](https://github.com/markuplint/markuplint/issues/4492)
24
+ * report a main element that is not hierarchically correct ([b44c936](https://github.com/markuplint/markuplint/commit/b44c936cf2e40c8142f88fe6b503af6f437cd341))
25
+ * **rules:** accept style as the first child in permitted-contents ([0523872](https://github.com/markuplint/markuplint/commit/05238722e9501b7758035d55293a178ab766001a)), closes [#x20](https://github.com/markuplint/markuplint/issues/x20)
26
+ * **rules:** accept the interactive content that canvas allows ([095eb3a](https://github.com/markuplint/markuplint/commit/095eb3aeb16186b0f5cb3f4a3bd342360dae1c2c)), closes [#4491](https://github.com/markuplint/markuplint/issues/4491)
27
+ * **rules:** count an element inside a conditional block as a label's descendant ([6ca1da5](https://github.com/markuplint/markuplint/commit/6ca1da545a903bec024f4418c3595f16d72365bc))
28
+ * **rules:** do not judge an element that the framework moves to the head by its parent ([db28a20](https://github.com/markuplint/markuplint/commit/db28a20bc562233f0c55594077a15276ec9c113a)), closes [#4484](https://github.com/markuplint/markuplint/issues/4484) [#4484](https://github.com/markuplint/markuplint/issues/4484)
29
+ * **rules:** do not look for an ancestor across the boundary of template content ([b660c8b](https://github.com/markuplint/markuplint/commit/b660c8bc5d2b4cbcc16f592ad536db9a2b8bdaaa))
30
+ * **rules:** do not treat template content as an accessible name ([0d004ef](https://github.com/markuplint/markuplint/commit/0d004efd84e6cd201cdcdc66163b89c4b0a714d2))
31
+ * **rules:** ignore comment nodes in permitted-contents ([38881eb](https://github.com/markuplint/markuplint/commit/38881eb71e80bfef45832a8f2378d336e5bfbe09)), closes [#4488](https://github.com/markuplint/markuplint/issues/4488)
32
+ * **rules:** judge an id and a structure in the tree that it is in ([#4489](https://github.com/markuplint/markuplint/issues/4489)) ([b17db6d](https://github.com/markuplint/markuplint/commit/b17db6d813012e127beb2c2b9e3fc21a393e2d4d))
33
+ * **rules:** require as to satisfy every matched link destination ([b4dbea2](https://github.com/markuplint/markuplint/commit/b4dbea2a08d761c90c98098102d7af40cf485849))
34
+
35
+ ### Features
36
+
37
+ * **rules:** judge an attribute by what the framework renders ([3beee5a](https://github.com/markuplint/markuplint/commit/3beee5ad51e96fc3033b7930fec2889ea5bf18e6))
38
+
39
+
6
40
  # [5.4.0](https://github.com/markuplint/markuplint/compare/v5.3.0...v5.4.0) (2026-10-29)
7
41
 
8
42
  ### Bug Fixes
package/CLAUDE.md CHANGED
@@ -65,10 +65,11 @@ broken description and detonate in someone else's PR. Run `yarn site:build` befo
65
65
 
66
66
  ## Citing a specification
67
67
 
68
- A README, a source comment, `meta.ts` and `schema.json` cite the passage that justifies a rule. `yarn spec-citations` checks every citation against the live specification (what it checks and why: the module comment of `scripts/spec-citations/cli.ts`), and `scripts/spec-citations/citations-policy.spec.ts` holds the conventions below offline. After editing a citation, run `yarn spec-citations` (read-only) and fix what it reports. Its lock file is refreshed by the scheduled workflow's PR, never by hand.
68
+ A README, a source comment, `meta.ts` and `schema.json` cite the passage that justifies a rule; so do the comments in the `src` of every package (the algorithms of `ml-spec`, the value grammars of `types`, the DOM of `ml-core`, the declarations of how a framework renders a literal in `*-spec` and `*-parser`), the `html-spec` data and the website's pages. The monitor reads the whole `src` of each package, so a citation that moves with its code to another package stays watched. `yarn spec-citations` checks every citation against the live specification, the documentation or the source of a framework (what it checks and why: the module comment of `scripts/spec-citations/cli.ts`), and `scripts/spec-citations/citations-policy.spec.ts` holds the conventions below offline. After editing a citation, run `yarn spec-citations` (read-only) and fix what it reports. Its lock file is refreshed by the scheduled workflow's PR, never by hand.
69
69
 
70
- - Quote a spec sentence verbatim, as a blockquote in a README or as `>` lines in a source comment, and name its source: in a README the `Cite: [..](url)` line after the quote (`[..](url)より引用` in Japanese), in a comment an `@see` URL in the same comment. Never quote inline in double quotes: the monitor cannot check it. `[…]` marks what the quote leaves out.
71
- - Cite a section by its fragment. A `§4.10.7` beside the link is compared with the section the fragment is in; the number of an ancestor section is accepted.
70
+ - Quote a spec sentence verbatim, as a blockquote in a README or as `>` lines in a source comment, and name its source: in a README the `Cite: [..](url)` line after the quote (`[..](url)より引用` in Japanese), in a comment an `@see` URL in the same comment. A `>` is read only at the start of a comment line (not after a list marker), and each quote is a paragraph of its own. Never quote inline in double quotes: the monitor cannot check it. `[…]` marks what the quote leaves out, and a quote is looked up in the sections the comment cites, in the order that it writes it in.
71
+ - Cite a section by its fragment. A `§4.10.7` beside the link is compared with the section the fragment is in (one before the link belongs to the link that follows it, one after a link on the same line, as in `@see <url> — Name §4.3.2`, to that link); the number of an ancestor section is accepted. Prefer the id of a heading to a text fragment (`#:~:text=`) alone: a text fragment with no `#fragment` is searched in the whole page, and stops matching when the passage is reworded (HTML-AAM's tables were rebuilt into one section per element, and `#:~:text=Comments-,article,-aside` went with them).
72
+ - A source file of a framework is cited by `https://github.com/<owner>/<repo>/blob/<default branch>/<path>#<name>`, where `<name>` is a top-level declaration of the file (a function, a variable, a class, a type, or `exports.<name> =`). It is compared as the declaration alone, so an edit elsewhere in the file reports nothing and a renamed declaration is `anchor-missing`. Only the repositories listed in `scripts/spec-citations/collect.ts` are monitored, and a link without a name or with a line range (`#L11-L52`) is not checked.
72
73
  - `README.ja.md` cites the HTML Living Standard through its translation (`https://momdo.github.io/html/…`, the same page and anchor as the English README) and quotes the translation's own text. That text stays English where the translation is behind the original, and the monitor reports it once the translation catches up. For WCAG, the translation is waic.jp's.
73
74
  - Cite W3C specs at an edition-less URL (`https://w3c.github.io/accname/`, `https://w3c.github.io/aria/`, `https://www.w3.org/TR/WCAG/`, `https://www.w3.org/TR/html-aria/`). WCAG's Techniques and Understanding pages have no edition-less URL; cite the latest edition (`https://www.w3.org/WAI/WCAG22/…`). Not `w3c.github.io/html-aria/`: that is unrendered ReSpec source whose anchors exist only after a browser builds them.
74
75
 
@@ -2,7 +2,7 @@ import { createRule, getComputedRole, ariaSpecs, getSpec } from '@markuplint/ml-
2
2
  import { ARIA_RECOMMENDED_VERSION } from '@markuplint/ml-spec';
3
3
  import { checkingNoGlobalProp } from '../wai-aria/checkings/no-global-prop.js';
4
4
  import { defaultOptions } from '../wai-aria/default-options.js';
5
- import { isAriaAttr } from '../helpers.js';
5
+ import { getAriaAttrs } from '../helpers.js';
6
6
  import meta from './meta.js';
7
7
  export default createRule({
8
8
  meta,
@@ -14,7 +14,7 @@ export default createRule({
14
14
  return;
15
15
  if (!elSpec.globalAttrs['#ARIAAttrs'])
16
16
  return;
17
- const propAttrs = el.attributes.filter(attr => isAriaAttr(attr));
17
+ const propAttrs = getAriaAttrs(el);
18
18
  if (propAttrs.length === 0)
19
19
  return;
20
20
  const ariaVersion = el.rule.options?.version ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
@@ -41,8 +41,29 @@ export type AttrEligibility = {
41
41
  * @param attr - The attribute that is being resolved
42
42
  */
43
43
  export declare function resolveAttrEligibility<T extends RuleConfigValue, O extends PlainData>(name: string, node: Element<T, O>, attrSpecs: readonly Attribute[], attr?: Attr<T, O>): AttrEligibility;
44
+ type PlainEnum = {
45
+ readonly enum: readonly [string, ...string[]];
46
+ readonly caseInsensitive?: boolean;
47
+ readonly disallowToSurroundBySpaces?: boolean;
48
+ readonly invalidValueDefault?: string;
49
+ };
50
+ /**
51
+ * `invalidValueDefault` is not carried over: a fallback of one set may be a
52
+ * value another set forbids. A case-sensitive spelling is kept when an earlier
53
+ * entry only has another case. Case and surrounding spaces follow the stricter
54
+ * side, because the value has to satisfy every entry.
55
+ *
56
+ * An empty intersection cannot be an enum (`enum` has at least one token).
57
+ * A pattern that matches nothing keeps every value invalid. That pattern is
58
+ * not a keyword list an author is meant to satisfy.
59
+ *
60
+ * @param enums - The enum entries that matched, in spec order. At least two
61
+ * @returns The intersection, or a pattern that matches nothing when the sets are disjoint
62
+ */
63
+ export declare function intersectEnums(enums: readonly PlainEnum[]): Attribute['type'];
44
64
  /**
45
65
  * Formats the "did you mean" suffix shared by `no-unknown-attr`'s two typo
46
66
  * paths (spec-driven candidate, case-sensitivity mismatch).
47
67
  */
48
68
  export declare function suggestionSuffix(t: Translator, candidate: string | undefined): string;
69
+ export {};
@@ -1,5 +1,5 @@
1
1
  import { isConditionalAttributeTypeArray } from '@markuplint/ml-spec';
2
- import { getCandidate } from '@markuplint/types';
2
+ import { check, getCandidate } from '@markuplint/types';
3
3
  import { attrMatches, conditionDependsOnDynamicAttr, findAttrSpec, namespaceKeyOf } from './helpers.js';
4
4
  /**
5
5
  * Attribute namespaces that are exempt from spec-based name/eligibility
@@ -43,14 +43,11 @@ attr) {
43
43
  }
44
44
  // Resolve ConditionalAttributeType[] to a concrete type based on element matching (#3598).
45
45
  if (isConditionalAttributeTypeArray(spec.type)) {
46
- const matched = spec.type.find(entry => {
46
+ const matched = spec.type.filter(entry => {
47
47
  const cond = typeof entry.condition === 'string' ? entry.condition : entry.condition.join(',');
48
48
  return node.matches(cond);
49
49
  });
50
- // Fallback to 'Any' when no condition matches: input types without an explicit
51
- // entry (text, search, tel, password, hidden, checkbox, radio, file, submit,
52
- // image, reset, button) have no value constraints per the HTML spec.
53
- spec = { ...spec, type: matched ? matched.type : 'Any' };
50
+ spec = { ...spec, type: combineMatchedConditionalTypes(matched) };
54
51
  }
55
52
  if (spec.condition != null &&
56
53
  !node.hasSpreadAttr &&
@@ -60,6 +57,89 @@ attr) {
60
57
  }
61
58
  return { status: 'ok', spec };
62
59
  }
60
+ /**
61
+ * Enum entries that match together are simultaneous constraints: `link[as]`
62
+ * has to be a preload destination and a module preload destination when `rel`
63
+ * contains both keywords. The message names that intersection and does not
64
+ * offer a keyword another entry forbids. Token order follows the entries:
65
+ * a spelling is kept from the earliest entry whose own token satisfies every
66
+ * entry, which is the first entry's order when that spelling is valid there.
67
+ *
68
+ * A non-enum overlap stays on the first match. Those entries are ordered
69
+ * alternatives: an empty `input[value]` other than `type=range` is
70
+ * unconstrained even though a later type-specific entry also matches, and
71
+ * `email` with `multiple` matches both the address-list type and the
72
+ * single-address type, of which the list is the requirement.
73
+ */
74
+ function combineMatchedConditionalTypes(matched) {
75
+ const first = matched[0];
76
+ // No condition matched. Input types without an explicit entry (text, search,
77
+ // tel, password, hidden, checkbox, radio, file, submit, image, reset, button)
78
+ // have no value constraints per the HTML spec.
79
+ if (!first) {
80
+ return 'Any';
81
+ }
82
+ const enums = [];
83
+ for (const entry of matched) {
84
+ if (!isPlainEnum(entry.type)) {
85
+ return first.type;
86
+ }
87
+ enums.push(entry.type);
88
+ }
89
+ if (enums.length === 1) {
90
+ return first.type;
91
+ }
92
+ return intersectEnums(enums);
93
+ }
94
+ function isPlainEnum(type) {
95
+ if (typeof type === 'string' || Array.isArray(type)) {
96
+ return false;
97
+ }
98
+ return 'enum' in type && Array.isArray(type.enum) && type.enum.length > 0;
99
+ }
100
+ /**
101
+ * `invalidValueDefault` is not carried over: a fallback of one set may be a
102
+ * value another set forbids. A case-sensitive spelling is kept when an earlier
103
+ * entry only has another case. Case and surrounding spaces follow the stricter
104
+ * side, because the value has to satisfy every entry.
105
+ *
106
+ * An empty intersection cannot be an enum (`enum` has at least one token).
107
+ * A pattern that matches nothing keeps every value invalid. That pattern is
108
+ * not a keyword list an author is meant to satisfy.
109
+ *
110
+ * @param enums - The enum entries that matched, in spec order. At least two
111
+ * @returns The intersection, or a pattern that matches nothing when the sets are disjoint
112
+ */
113
+ export function intersectEnums(enums) {
114
+ const caseInsensitive = enums.every(item => item.caseInsensitive !== false);
115
+ const tokens = [];
116
+ for (const item of enums) {
117
+ for (const token of item.enum) {
118
+ if (!enums.every(other => check(token, other).matched)) {
119
+ continue;
120
+ }
121
+ if (tokens.some(existing => sameToken(existing, token, caseInsensitive))) {
122
+ continue;
123
+ }
124
+ tokens.push(token);
125
+ }
126
+ }
127
+ const [head, ...rest] = tokens;
128
+ if (head === undefined) {
129
+ return { pattern: '/(?!)/' };
130
+ }
131
+ const result = {
132
+ enum: [head, ...rest],
133
+ ...(caseInsensitive ? {} : { caseInsensitive: false }),
134
+ ...(enums.every(item => item.disallowToSurroundBySpaces === false)
135
+ ? { disallowToSurroundBySpaces: false }
136
+ : {}),
137
+ };
138
+ return result;
139
+ }
140
+ function sameToken(a, b, caseInsensitive) {
141
+ return caseInsensitive ? a.toLowerCase() === b.toLowerCase() : a === b;
142
+ }
63
143
  /**
64
144
  * Formats the "did you mean" suffix shared by `no-unknown-attr`'s two typo
65
145
  * paths (spec-driven candidate, case-sensitivity mismatch).
@@ -229,7 +229,8 @@ export default createRule({
229
229
  let cachedValue;
230
230
  let cachedCompiled;
231
231
  await document.walkOn('Element', node => {
232
- const attrs = node.getAttributeTokens();
232
+ // An attribute that a pretender made is not in the source, so it has no place in the order of it
233
+ const attrs = node.getAttributeTokens().filter(attr => !attr.isPretended);
233
234
  if (attrs.length <= 1) {
234
235
  return;
235
236
  }
@@ -13,8 +13,8 @@ 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`);
17
- if (attr.isDynamicValue || attr.isDirective || !attr.equal || attr.equal?.raw === '') {
16
+ const message = t('{0} must be quoted with {1}', 'Attribute value', attr.rule.value === 'double' ? 'double quotation marks' : 'single quotation marks');
17
+ if (attr.isExpressionValue || attr.isDirective || !attr.equal || attr.equal?.raw === '') {
18
18
  return;
19
19
  }
20
20
  const quote = attr.startQuote?.raw;
@@ -18,7 +18,7 @@ export default createRule({
18
18
  meta,
19
19
  defaultSeverity: 'warning',
20
20
  verify({ document, report, t }) {
21
- const tables = document.querySelectorAll('table');
21
+ const tables = document.deepQuerySelectorAll('table');
22
22
  for (const table of tables) {
23
23
  const grid = new Grid(table);
24
24
  if (grid.hasOverlapped()) {
@@ -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
  }
@@ -2,7 +2,7 @@ import { createRule, getComputedRole, ariaSpecs, getSpec } from '@markuplint/ml-
2
2
  import { ARIA_RECOMMENDED_VERSION } from '@markuplint/ml-spec';
3
3
  import { checkingElementSupportsAriaProp } from '../wai-aria/checkings/element-supports-aria-prop.js';
4
4
  import { defaultOptions } from '../wai-aria/default-options.js';
5
- import { isAriaAttr } from '../helpers.js';
5
+ import { getAriaAttrs } from '../helpers.js';
6
6
  import meta from './meta.js';
7
7
  /**
8
8
  * Split from the former `wai-aria-disallowed-props` rule (#3989): the ARIA in
@@ -25,7 +25,7 @@ export default createRule({
25
25
  return;
26
26
  if (!elSpec.globalAttrs['#ARIAAttrs'])
27
27
  return;
28
- const propAttrs = el.attributes.filter(attr => isAriaAttr(attr));
28
+ const propAttrs = getAriaAttrs(el);
29
29
  if (propAttrs.length === 0)
30
30
  return;
31
31
  const ariaVersion = el.rule.options?.version ?? document.ruleCommonSettings?.ariaVersion ?? ARIA_RECOMMENDED_VERSION;
@@ -1,4 +1,5 @@
1
1
  import { createRule } from '@markuplint/ml-core';
2
+ import { getElementByIdInTree } from '../helpers.js';
2
3
  import meta from './meta.js';
3
4
  /**
4
5
  * Form-associated elements (HTML LS §4.10.2). Listed elements may carry
@@ -47,8 +48,7 @@ export default createRule({
47
48
  return;
48
49
  if (formAttr.isDynamicValue)
49
50
  return;
50
- const targetId = formAttr.value;
51
- const target = [...document.querySelectorAll('[id]')].find(candidate => candidate.getAttribute('id') === targetId);
51
+ const target = getElementByIdInTree(el, formAttr.value, formAttr.isExpressionValue);
52
52
  // Existence is checked by `no-refer-to-non-existent-id`. If the ID does not resolve
53
53
  // here, leave that diagnostic to the dedicated rule and stay silent.
54
54
  if (!target)
@@ -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,7 +1,68 @@
1
1
  import type { FixToken, IRuleFixer, PlainData, TextEdit } from '@markuplint/ml-config';
2
- import type { Attr, Element, RuleConfigValue, Document } 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
+ import { getDOMValue } from '@markuplint/ml-core';
6
+ export { getDOMValue };
7
+ /**
8
+ * The element that an id reference written in an attribute of `el` resolves to: the first element
9
+ * of the tree `el` is in whose id is the reference. The content of a `template` element and a
10
+ * declarative shadow root are trees of their own, so a reference there does not reach an id of the
11
+ * document, nor the other way around.
12
+ *
13
+ * The reference is compared with the character references in the markup resolved, as the id is.
14
+ *
15
+ * @template T - The rule configuration value type
16
+ * @template O - The rule options type
17
+ * @param el - The element that has the attribute
18
+ * @param reference - The value of the attribute, or one token of it, as written in the markup
19
+ * @param isExpression - Whether the value is written as code, which has no character reference in it
20
+ * @returns The element, or `null` when the tree has none, or `el` is in no tree
21
+ * @see https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute
22
+ */
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;
56
+ /**
57
+ * The elements that match the selector in the tree `el` is in (see {@link getElementByIdInTree}).
58
+ *
59
+ * @template T - The rule configuration value type
60
+ * @template O - The rule options type
61
+ * @param el - An element of the tree
62
+ * @param selectors - A selector
63
+ * @returns The elements in the order of the markup, none when `el` is in no tree
64
+ */
65
+ export declare function querySelectorAllInTree<T extends RuleConfigValue, O extends PlainData>(el: Element<T, O>, selectors: string): readonly Element<T, O>[];
5
66
  /** A `null`/`undefined` condition means the attribute applies unconditionally. */
6
67
  export declare function attrMatches<T extends RuleConfigValue, O extends PlainData>(node: Element<T, O>, condition: Attribute['condition']): boolean;
7
68
  /**
@@ -60,6 +121,16 @@ export declare function namespaceKeyOf(attr: Pick<Attr<RuleConfigValue, PlainDat
60
121
  readonly elementType: string;
61
122
  };
62
123
  }): string;
124
+ /**
125
+ * The `aria-*` attributes that the element has. An attribute that the framework does not output
126
+ * (`aria-label={null}`, `MLAttr#isAbsent`) is not one of them: the DOM does not have it, so what a rule
127
+ * says of an attribute of the element (it is unsupported, deprecated, redundant, or its value is
128
+ * invalid) is not said of it.
129
+ *
130
+ * @param el - The element
131
+ * @returns The `aria-*` attributes that are output
132
+ */
133
+ export declare function getAriaAttrs<T extends RuleConfigValue, O extends PlainData>(el: Element<T, O>): Attr<T, O>[];
63
134
  /**
64
135
  * Whether the attribute is an `aria-*` one, told by {@link namespaceKeyOf}.
65
136
  *
@@ -117,7 +188,28 @@ export declare function match(needle: string, pattern: string): boolean;
117
188
  */
118
189
  export declare const rePCENChar: string;
119
190
  export declare function toNormalizedValue(value: string, spec: Attribute): string;
120
- export declare function accnameMayBeMutable(el: Element<any, any>, document: Document<any, any>): boolean;
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;
212
+ export declare function accnameMayBeMutable(el: Element<any, any>): boolean;
121
213
  /**
122
214
  * Whether the accessible name of `el` may be rendered by content the pretender
123
215
  * does not know: an unknown entry (`{ dynamic: true }`) in the `contents` of
@@ -129,9 +221,9 @@ export declare function accnameMayBeMutable(el: Element<any, any>, document: Doc
129
221
  * must stay checked (and reported as redundant by `no-redundant-accessible-name`).
130
222
  * Call it only once the static name has been computed and found empty.
131
223
  */
132
- export declare function accnameMayComeFromUnknownContent(el: Element<any, any>, document: Document<any, any>): boolean;
224
+ export declare function accnameMayComeFromUnknownContent(el: Element<any, any>): boolean;
133
225
  export declare const labelable: string[];
134
- export declare function getOwnedLabel<V extends RuleConfigValue, O extends PlainData>(el: Element<V, O>, document: Document<V, O>): Element<V, O> | null;
226
+ export declare function getOwnedLabel<V extends RuleConfigValue, O extends PlainData>(el: Element<V, O>): Element<V, O> | null;
135
227
  /** `null`/`undefined` items are silently dropped on add. */
136
228
  export declare class Collection<T> {
137
229
  #private;
@@ -181,4 +273,3 @@ export declare function createIgnoreFeaturesLookup(tagNameCaseSensitive: boolean
181
273
  /** Whether the attribute of the element is ignored. */
182
274
  isAttrIgnored<T extends RuleConfigValue, O extends PlainData>(elementOption: readonly string[] | undefined, elementName: string, attr: Attr<T, O>): boolean;
183
275
  };
184
- export {};