@markuplint/ml-core 5.0.0-rc.4 → 5.0.0-rc.6

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 (68) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +0 -5
  3. package/lib/cursor-offset.js +0 -3
  4. package/lib/fix-applier.js +3 -9
  5. package/lib/ml-core.d.ts +33 -3
  6. package/lib/ml-core.js +276 -62
  7. package/lib/ml-dom/helper/accname.d.ts +0 -8
  8. package/lib/ml-dom/helper/accname.js +7 -10
  9. package/lib/ml-dom/node/attr.js +3 -1
  10. package/lib/ml-dom/node/block.d.ts +6 -0
  11. package/lib/ml-dom/node/block.js +6 -0
  12. package/lib/ml-dom/node/child-node.d.ts +0 -9
  13. package/lib/ml-dom/node/child-node.js +0 -9
  14. package/lib/ml-dom/node/document.d.ts +20 -1
  15. package/lib/ml-dom/node/document.js +24 -13
  16. package/lib/ml-dom/node/element-close-tag.d.ts +12 -0
  17. package/lib/ml-dom/node/element-close-tag.js +12 -0
  18. package/lib/ml-dom/node/element.d.ts +22 -0
  19. package/lib/ml-dom/node/element.js +37 -11
  20. package/lib/ml-dom/node/node-store.d.ts +0 -3
  21. package/lib/ml-dom/node/node-store.js +0 -3
  22. package/lib/ml-dom/node/node.d.ts +34 -1
  23. package/lib/ml-dom/node/node.js +34 -16
  24. package/lib/ml-dom/node/parent-node.js +0 -6
  25. package/lib/ml-dom/node/rule-mapper.d.ts +8 -0
  26. package/lib/ml-dom/node/rule-mapper.js +8 -0
  27. package/lib/ml-rule/ml-rule.d.ts +19 -0
  28. package/lib/ml-rule/ml-rule.js +38 -7
  29. package/lib/ml-rule/types.d.ts +110 -1
  30. package/lib/ml-rule/types.js +28 -1
  31. package/lib/ruleset/index.d.ts +2 -1
  32. package/lib/ruleset/index.js +2 -1
  33. package/lib/test/index.js +1 -1
  34. package/lib/types.d.ts +8 -1
  35. package/lib/virtual-rule.d.ts +10 -0
  36. package/lib/virtual-rule.js +1 -24
  37. package/package.json +13 -13
  38. package/ARCHITECTURE.ja.md +0 -676
  39. package/ARCHITECTURE.md +0 -726
  40. package/SKILL.md +0 -61
  41. package/docs/linting-pipeline.ja.md +0 -307
  42. package/docs/linting-pipeline.md +0 -307
  43. package/docs/maintenance.ja.md +0 -210
  44. package/docs/maintenance.md +0 -210
  45. package/docs/ml-dom/attr.ja.md +0 -103
  46. package/docs/ml-dom/attr.md +0 -103
  47. package/docs/ml-dom/block.ja.md +0 -272
  48. package/docs/ml-dom/block.md +0 -272
  49. package/docs/ml-dom/document.ja.md +0 -134
  50. package/docs/ml-dom/document.md +0 -134
  51. package/docs/ml-dom/element.ja.md +0 -161
  52. package/docs/ml-dom/element.md +0 -161
  53. package/docs/ml-dom/helpers.ja.md +0 -203
  54. package/docs/ml-dom/helpers.md +0 -203
  55. package/docs/ml-dom/node.ja.md +0 -199
  56. package/docs/ml-dom/node.md +0 -199
  57. package/docs/ml-dom/others.ja.md +0 -120
  58. package/docs/ml-dom/others.md +0 -120
  59. package/docs/ml-dom/overview.ja.md +0 -102
  60. package/docs/ml-dom/overview.md +0 -102
  61. package/docs/ml-dom/pretender.ja.md +0 -269
  62. package/docs/ml-dom/pretender.md +0 -269
  63. package/docs/ml-dom/rule-mapping.ja.md +0 -371
  64. package/docs/ml-dom/rule-mapping.md +0 -371
  65. package/docs/ml-dom.ja.md +0 -18
  66. package/docs/ml-dom.md +0 -18
  67. package/docs/rule-system.ja.md +0 -287
  68. package/docs/rule-system.md +0 -287
@@ -10,7 +10,7 @@ import type { Ruleset } from '../../ruleset/index.js';
10
10
  import type { MLSchema } from '../../types.js';
11
11
  import type { Walker } from '../helper/walkers.js';
12
12
  import type { MLToken } from '../token/token.js';
13
- import type { EndTagType, MLASTDocument, MLASTNodeTreeItem } from '@markuplint/ml-ast';
13
+ import type { EndTagType, MLASTDocument, MLASTNodeTreeItem, MLASTParseError } from '@markuplint/ml-ast';
14
14
  import type { PlainData, Pretender, RuleCommonSettings, RuleConfigValue } from '@markuplint/ml-config';
15
15
  import type { ARIAVersion, MLMLSpec } from '@markuplint/ml-spec';
16
16
  import { MLParentNode } from './parent-node.js';
@@ -250,6 +250,25 @@ export declare class MLDocument<T extends RuleConfigValue, O extends PlainData =
250
250
  * @implements DOM API: `Document`
251
251
  */
252
252
  get dir(): string;
253
+ /**
254
+ * Non-fatal parser conformance errors collected during tokenisation by
255
+ * the underlying parser (currently `@markuplint/html-parser` and the
256
+ * template-engine parsers that delegate to it). Empty array when the
257
+ * parser does not produce events or the source had none.
258
+ *
259
+ * Rules that have claimed responsibility for parse5 events via
260
+ * `meta.mirrorsParseErrorCodes` typically read this array to surface
261
+ * the corresponding violations themselves. `character-reference` is the
262
+ * canonical example: its self-detection covers unescaped `<`, `>`, `&`,
263
+ * `"` (the "missed escape" direction), and reading `parseErrors` adds
264
+ * coverage for parse5's malformed-reference codes (`&xyz;`, etc.) under
265
+ * the same rule id.
266
+ *
267
+ * ml-core's built-in parse-error channel suppresses the mirrored codes
268
+ * unconditionally — so the rule that reads them here is the only place
269
+ * the user sees the violation.
270
+ */
271
+ get parseErrors(): readonly MLASTParseError[];
253
272
  /**
254
273
  * @implements DOM API: `Document`
255
274
  */
@@ -48,9 +48,6 @@ export class MLDocument extends MLParentNode {
48
48
  * @default "omittable"
49
49
  */
50
50
  endTag;
51
- /**
52
- * The file path of the source document, if available.
53
- */
54
51
  #astNodeMap;
55
52
  #filename;
56
53
  /**
@@ -332,6 +329,27 @@ export class MLDocument extends MLParentNode {
332
329
  get dir() {
333
330
  throw new UnexpectedCallError('Not supported "dir" property');
334
331
  }
332
+ /**
333
+ * Non-fatal parser conformance errors collected during tokenisation by
334
+ * the underlying parser (currently `@markuplint/html-parser` and the
335
+ * template-engine parsers that delegate to it). Empty array when the
336
+ * parser does not produce events or the source had none.
337
+ *
338
+ * Rules that have claimed responsibility for parse5 events via
339
+ * `meta.mirrorsParseErrorCodes` typically read this array to surface
340
+ * the corresponding violations themselves. `character-reference` is the
341
+ * canonical example: its self-detection covers unescaped `<`, `>`, `&`,
342
+ * `"` (the "missed escape" direction), and reading `parseErrors` adds
343
+ * coverage for parse5's malformed-reference codes (`&xyz;`, etc.) under
344
+ * the same rule id.
345
+ *
346
+ * ml-core's built-in parse-error channel suppresses the mirrored codes
347
+ * unconditionally — so the rule that reads them here is the only place
348
+ * the user sees the violation.
349
+ */
350
+ get parseErrors() {
351
+ return this._astToken.parseErrors ?? [];
352
+ }
335
353
  /**
336
354
  * @implements DOM API: `Document`
337
355
  */
@@ -2318,9 +2336,9 @@ export class MLDocument extends MLParentNode {
2318
2336
  throw new UnexpectedCallError('Not supported "writeln" method');
2319
2337
  }
2320
2338
  /**
2321
- * Initializes pretender contexts for all element nodes in the document.
2322
- *
2323
- * @param pretenders - Optional pretender configurations from the document options
2339
+ * Must run before `#ruleMapping`: rule selectors (e.g. a `nodeRules`
2340
+ * entry targeting `button`) are matched against the pretender identity,
2341
+ * so the pretender link has to exist when rules are mapped.
2324
2342
  */
2325
2343
  #pretending(pretenders) {
2326
2344
  if (docLog.enabled) {
@@ -2332,13 +2350,6 @@ export class MLDocument extends MLParentNode {
2332
2350
  }
2333
2351
  }
2334
2352
  }
2335
- /**
2336
- * Maps the ruleset configuration to each node in the document.
2337
- * Applies global rules, node-specific rules (by selector), and
2338
- * child-node rules to build the per-node rule configuration.
2339
- *
2340
- * @param ruleset - The ruleset containing rules, nodeRules, and childNodeRules
2341
- */
2342
2353
  #ruleMapping(ruleset) {
2343
2354
  if (docLog.enabled) {
2344
2355
  docLog('Rule Mapping: %O', Object.keys(ruleset.rules));
@@ -3,6 +3,18 @@ import type { MLElement } from './element.js';
3
3
  import type { MLASTElementCloseTag } from '@markuplint/ml-ast';
4
4
  import type { PlainData, RuleConfigValue } from '@markuplint/ml-config';
5
5
  import { MLNode } from './node.js';
6
+ /**
7
+ * The close tag paired with its opening `MLElement`.
8
+ *
9
+ * Close tags are not part of the document's `nodeList`; each instance exists
10
+ * only as a satellite of its paired element (`MLElement.closeTag`). The class
11
+ * exists for two purposes:
12
+ *
13
+ * 1. Reporting violations at the close tag's own source location instead of
14
+ * the open tag (e.g. the `case-sensitive-tag-name` rule).
15
+ * 2. Detecting close-tag presence: `MLElement.closeTag` is `null` for void,
16
+ * self-closing, or omitted end tags (e.g. the `end-tag` rule).
17
+ */
6
18
  export declare class MLElementCloseTag<T extends RuleConfigValue, O extends PlainData = undefined> extends MLNode<T, O, MLASTElementCloseTag> {
7
19
  readonly pair: MLElement<T, O>;
8
20
  constructor(astNode: MLASTElementCloseTag, document: MLDocument<T, O>, pair: MLElement<T, O>);
@@ -1,4 +1,16 @@
1
1
  import { MLNode } from './node.js';
2
+ /**
3
+ * The close tag paired with its opening `MLElement`.
4
+ *
5
+ * Close tags are not part of the document's `nodeList`; each instance exists
6
+ * only as a satellite of its paired element (`MLElement.closeTag`). The class
7
+ * exists for two purposes:
8
+ *
9
+ * 1. Reporting violations at the close tag's own source location instead of
10
+ * the open tag (e.g. the `case-sensitive-tag-name` rule).
11
+ * 2. Detecting close-tag presence: `MLElement.closeTag` is `null` for void,
12
+ * self-closing, or omitted end tags (e.g. the `end-tag` rule).
13
+ */
2
14
  export class MLElementCloseTag extends MLNode {
3
15
  pair;
4
16
  constructor(astNode,
@@ -53,6 +53,10 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
53
53
  /**
54
54
  * The pretender context if this element is participating in pretender behavior,
55
55
  * or null if it is not a pretender or pretended element.
56
+ *
57
+ * The virtual element created by `pretending()` is not registered in the
58
+ * document's `nodeList`; walkers visit only the original element, which
59
+ * delegates its name and attribute getters to the virtual element.
56
60
  */
57
61
  pretenderContext: PretenderContext<MLElement<T, O>, T, O> | null;
58
62
  /**
@@ -1637,6 +1641,9 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
1637
1641
  /**
1638
1642
  * Returns the rule configuration for this element, respecting the pretender context.
1639
1643
  * If the element is a pretended origin, returns the rule from the pretending element.
1644
+ * Rules are mapped only to nodes in the document's `nodeList`; the virtual
1645
+ * pretender element never appears there, so it must read the resolved
1646
+ * rules from the original element.
1640
1647
  *
1641
1648
  * @implements `@markuplint/ml-core` API: `MLNode`
1642
1649
  */
@@ -1901,6 +1908,10 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
1901
1908
  * Gets the attribute value from the original (non-pretended) attributes list,
1902
1909
  * bypassing any pretender context that might be active.
1903
1910
  *
1911
+ * Exists for the pretender ARIA `{ fromAttr }` accessible-name resolution:
1912
+ * the source attribute (e.g. `label` on `<MyButton label="...">`) lives on
1913
+ * the original component element, not on the virtual pretender element.
1914
+ *
1904
1915
  * @implements `@markuplint/ml-core` API: `MLElement`
1905
1916
  * @param attrName - The attribute name to look up (case-insensitive)
1906
1917
  * @returns The attribute value, or null if the attribute is not found
@@ -2015,6 +2026,12 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
2015
2026
  * such as preprocessor-specific blocks, slot elements, or (optionally) elements
2016
2027
  * with dynamic attributes.
2017
2028
  *
2029
+ * Blocks that carry a `blockBehavior` are not treated as mutable because
2030
+ * their branches are deterministically enumerable via
2031
+ * `conditionalChildNodes()`; blocks without one (e.g. expression output
2032
+ * like `{value}`) can produce arbitrary content, so the children are
2033
+ * considered mutable.
2034
+ *
2018
2035
  * @implements `@markuplint/ml-core` API: `MLElement`
2019
2036
  * @param attr - When true, also considers children with mutable attributes as mutable
2020
2037
  * @returns True if this element has potentially mutable children
@@ -2090,6 +2107,11 @@ export declare class MLElement<T extends RuleConfigValue, O extends PlainData =
2090
2107
  * returning detailed match results. When the element is a pretender,
2091
2108
  * it attempts to match both as the pretender and as the original element.
2092
2109
  *
2110
+ * The two-phase strategy lets both targeting styles work: selectors for
2111
+ * the semantic element (e.g. `button`) match via the pretender identity,
2112
+ * while selectors for the component name (e.g. `MyButton`) still match
2113
+ * the original.
2114
+ *
2093
2115
  * @param selector - The CSS selector string or regex selector to match against
2094
2116
  * @param scope - An optional scope node for scoped selector matching
2095
2117
  * @returns The detailed match result including captured groups from regex selectors
@@ -1,5 +1,5 @@
1
1
  /* global StylePropertyMap, StylePropertyMapReadOnly */
2
- import { resolveNamespace } from '@markuplint/ml-spec';
2
+ import { getSpecByTagName, resolveNamespace } from '@markuplint/ml-spec';
3
3
  import { matchSelector } from '@markuplint/selector';
4
4
  import { getAccname } from '../helper/accname.js';
5
5
  import { after, before, nextElementSibling, previousElementSibling, remove, replaceWith, } from '../manipulations/child-node-methods.js';
@@ -49,8 +49,6 @@ export class MLElement extends MLParentNode {
49
49
  */
50
50
  namespaceURI;
51
51
  /**
52
- * Memoization cache for the accessible name computation, keyed by ARIA version.
53
- *
54
52
  * ## Why this cache exists
55
53
  *
56
54
  * Multiple rules and the `:aria(has name)` selector evaluate the accessible name
@@ -68,14 +66,6 @@ export class MLElement extends MLParentNode {
68
66
  * logic is needed. The cache is garbage-collected together with the
69
67
  * MLElement instance when the document is released.
70
68
  *
71
- * ## Consumers that benefit
72
- *
73
- * - `require-accessible-name` rule (direct call)
74
- * - `wai-aria` rule via `:aria(has name)` selector
75
- * - `neighbor-popovers` rule
76
- * - `landmark-roles` rule
77
- * - `MLDocument.getAccessibilityProp()` (accessibility tree builder)
78
- *
79
69
  * Introduced to resolve {@link https://github.com/markuplint/markuplint/issues/2179 | #2179}
80
70
  * (AccName performance bottleneck).
81
71
  *
@@ -91,6 +81,10 @@ export class MLElement extends MLParentNode {
91
81
  /**
92
82
  * The pretender context if this element is participating in pretender behavior,
93
83
  * or null if it is not a pretender or pretended element.
84
+ *
85
+ * The virtual element created by `pretending()` is not registered in the
86
+ * document's `nodeList`; walkers visit only the original element, which
87
+ * delegates its name and attribute getters to the virtual element.
94
88
  */
95
89
  pretenderContext = null;
96
90
  /**
@@ -2125,6 +2119,9 @@ export class MLElement extends MLParentNode {
2125
2119
  /**
2126
2120
  * Returns the rule configuration for this element, respecting the pretender context.
2127
2121
  * If the element is a pretended origin, returns the rule from the pretending element.
2122
+ * Rules are mapped only to nodes in the document's `nodeList`; the virtual
2123
+ * pretender element never appears there, so it must read the resolved
2124
+ * rules from the original element.
2128
2125
  *
2129
2126
  * @implements `@markuplint/ml-core` API: `MLNode`
2130
2127
  */
@@ -2501,6 +2498,10 @@ export class MLElement extends MLParentNode {
2501
2498
  * Gets the attribute value from the original (non-pretended) attributes list,
2502
2499
  * bypassing any pretender context that might be active.
2503
2500
  *
2501
+ * Exists for the pretender ARIA `{ fromAttr }` accessible-name resolution:
2502
+ * the source attribute (e.g. `label` on `<MyButton label="...">`) lives on
2503
+ * the original component element, not on the virtual pretender element.
2504
+ *
2504
2505
  * @implements `@markuplint/ml-core` API: `MLElement`
2505
2506
  * @param attrName - The attribute name to look up (case-insensitive)
2506
2507
  * @returns The attribute value, or null if the attribute is not found
@@ -2692,6 +2693,12 @@ export class MLElement extends MLParentNode {
2692
2693
  * such as preprocessor-specific blocks, slot elements, or (optionally) elements
2693
2694
  * with dynamic attributes.
2694
2695
  *
2696
+ * Blocks that carry a `blockBehavior` are not treated as mutable because
2697
+ * their branches are deterministically enumerable via
2698
+ * `conditionalChildNodes()`; blocks without one (e.g. expression output
2699
+ * like `{value}`) can produce arbitrary content, so the children are
2700
+ * considered mutable.
2701
+ *
2695
2702
  * @implements `@markuplint/ml-core` API: `MLElement`
2696
2703
  * @param attr - When true, also considers children with mutable attributes as mutable
2697
2704
  * @returns True if this element has potentially mutable children
@@ -2816,6 +2823,11 @@ export class MLElement extends MLParentNode {
2816
2823
  * returning detailed match results. When the element is a pretender,
2817
2824
  * it attempts to match both as the pretender and as the original element.
2818
2825
  *
2826
+ * The two-phase strategy lets both targeting styles work: selectors for
2827
+ * the semantic element (e.g. `button`) match via the pretender identity,
2828
+ * while selectors for the component name (e.g. `MyButton`) still match
2829
+ * the original.
2830
+ *
2819
2831
  * @param selector - The CSS selector string or regex selector to match against
2820
2832
  * @param scope - An optional scope node for scoped selector matching
2821
2833
  * @returns The detailed match result including captured groups from regex selectors
@@ -2850,6 +2862,20 @@ export class MLElement extends MLParentNode {
2850
2862
  * @param pretenders - Optional array of pretender configurations to match against
2851
2863
  */
2852
2864
  pretending(pretenders) {
2865
+ // Pretender must not apply to a recognised standard HTML element (e.g. <marquee>,
2866
+ // <h1>, <button>). Allowing such elements to masquerade as another would silently
2867
+ // mask spec-driven rules — deprecation, ARIA role restrictions, browser support —
2868
+ // keyed on the original tag. See issue #3740.
2869
+ //
2870
+ // Names that the HTML parser cannot distinguish from typos (PascalCase JSX-like
2871
+ // usage in plain HTML such as `<SimpleButton>`) get `elementType === 'html'`
2872
+ // from the parser but have no spec entry; those remain pretender-eligible
2873
+ // (this is what `pretenders.scan` relies on). The legacy "no inline `as=` on
2874
+ // HTML elements" guard is preserved further down for that case.
2875
+ if (this.elementType === 'html' &&
2876
+ getSpecByTagName(this.ownerMLDocument.specs.specs, this.localName, this.namespaceURI) != null) {
2877
+ return;
2878
+ }
2853
2879
  const pretenderConfig = pretenders?.find(option => this.matches(option.selector));
2854
2880
  const asAttrValue = this.getAttribute('as');
2855
2881
  const pretenderElement = pretenderConfig?.as ??
@@ -8,8 +8,5 @@ declare class NodeStore {
8
8
  getNodeByUuid<T extends RuleConfigValue, O extends PlainData = undefined>(uuid: string): MLNode<T, O, any>;
9
9
  setNode<A extends MLASTNode, T extends RuleConfigValue, O extends PlainData = undefined>(astNode: A, node: MLNode<T, O, A>): void;
10
10
  }
11
- /**
12
- * `NodeStore` Singleton
13
- */
14
11
  export declare const nodeStore: NodeStore;
15
12
  export {};
@@ -46,7 +46,4 @@ class NodeStore {
46
46
  this.#store.set(astNode.uuid, node);
47
47
  }
48
48
  }
49
- /**
50
- * `NodeStore` Singleton
51
- */
52
49
  export const nodeStore = new NodeStore();
@@ -13,6 +13,18 @@ import { MLToken } from '../token/token.js';
13
13
  * Extends `MLToken` with DOM `Node` interface compliance, tree traversal,
14
14
  * rule configuration access, and child node management.
15
15
  *
16
+ * The `implements Node` declaration is a maintenance strategy: conforming to
17
+ * the built-in DOM interfaces makes the compiler report errors whenever the
18
+ * TypeScript DOM type definitions gain new members, so the MLDOM API surface
19
+ * never drifts from the DOM Standard unnoticed. Members that are meaningless
20
+ * in static analysis (mutation, events, layout) are stubbed to throw
21
+ * `UnexpectedCallError` instead of being implemented.
22
+ *
23
+ * The `T`/`O` generics exist solely for rule authors using `createRule`:
24
+ * they propagate through the node tree so that `node.rule` is typed as
25
+ * `RuleInfo<T, O>` inside `verify()`/`fix()` callbacks. They are a
26
+ * compile-time-only mechanism — see the `rules` field for the runtime side.
27
+ *
16
28
  * @template T - The rule configuration value type
17
29
  * @template O - The rule options type
18
30
  * @template A - The underlying AST node type
@@ -121,7 +133,9 @@ export declare abstract class MLNode<T extends RuleConfigValue, O extends PlainD
121
133
  */
122
134
  readonly isFragment: boolean;
123
135
  /**
124
- *
136
+ * Rules mapped to this node by `RuleMapper`. Deliberately untyped
137
+ * (`AnyRule`) storage: the `T`/`O` generics are not enforced at runtime —
138
+ * the `rule` getter recovers the typed `RuleInfo<T, O>` via a cast.
125
139
  */
126
140
  readonly rules: Record<string, AnyRule>;
127
141
  protected _astToken: A;
@@ -169,6 +183,9 @@ export declare abstract class MLNode<T extends RuleConfigValue, O extends PlainD
169
183
  */
170
184
  get lastChild(): MLChildNode<T, O> | null;
171
185
  /**
186
+ * The next node in the syntactical sibling list which, unlike
187
+ * `nextSibling`, includes `MLBlock` nodes (AST-level traversal).
188
+ *
172
189
  * @implements `@markuplint/ml-core` API: `MLNode`
173
190
  */
174
191
  get nextNode(): MLNode<T, O> | null;
@@ -276,10 +293,18 @@ export declare abstract class MLNode<T extends RuleConfigValue, O extends PlainD
276
293
  */
277
294
  get parentNode(): MLDocument<any, any> | MLDocumentFragment<any, any> | MLElement<T, O> | null;
278
295
  /**
296
+ * The previous node in the syntactical sibling list which, unlike
297
+ * `previousSibling`, includes `MLBlock` nodes (AST-level traversal).
298
+ *
279
299
  * @implements `@markuplint/ml-core` API: `MLNode`
280
300
  */
281
301
  get prevNode(): MLNode<T, O> | null;
282
302
  /**
303
+ * The previous node in the document-order `nodeList`.
304
+ * Omitted (ghost) elements are skipped because they have no source tokens;
305
+ * including them would break offset chains used for indentation analysis
306
+ * and source reconstruction.
307
+ *
283
308
  * @implements `@markuplint/ml-core` API: `MLNode`
284
309
  */
285
310
  get prevToken(): MLNode<T, O> | null;
@@ -383,6 +408,14 @@ export declare abstract class MLNode<T extends RuleConfigValue, O extends PlainD
383
408
  *
384
409
  * Note: NodeList doesn't include whitespace nodes.
385
410
  *
411
+ * For `if`/`switch` groups, a `null` sentinel is appended to each branch
412
+ * group to represent the case where no branch renders at all, so that
413
+ * content-model rules also validate the "empty branch" pattern
414
+ * (`branchesToPatterns` filters the `null` out of the generated patterns).
415
+ * `each` blocks intentionally do not start a conditional mode: their content
416
+ * is flattened as always-present rather than treated as an alternative
417
+ * branch, even though a loop may render zero times.
418
+ *
386
419
  * @returns An array of NodeLists representing the conditional child nodes.
387
420
  *
388
421
  * @experemental
@@ -8,6 +8,18 @@ import { nodeStore } from './node-store.js';
8
8
  * Extends `MLToken` with DOM `Node` interface compliance, tree traversal,
9
9
  * rule configuration access, and child node management.
10
10
  *
11
+ * The `implements Node` declaration is a maintenance strategy: conforming to
12
+ * the built-in DOM interfaces makes the compiler report errors whenever the
13
+ * TypeScript DOM type definitions gain new members, so the MLDOM API surface
14
+ * never drifts from the DOM Standard unnoticed. Members that are meaningless
15
+ * in static analysis (mutation, events, layout) are stubbed to throw
16
+ * `UnexpectedCallError` instead of being implemented.
17
+ *
18
+ * The `T`/`O` generics exist solely for rule authors using `createRule`:
19
+ * they propagate through the node tree so that `node.rule` is typed as
20
+ * `RuleInfo<T, O>` inside `verify()`/`fix()` callbacks. They are a
21
+ * compile-time-only mechanism — see the `rules` field for the runtime side.
22
+ *
11
23
  * @template T - The rule configuration value type
12
24
  * @template O - The rule options type
13
25
  * @template A - The underlying AST node type
@@ -110,31 +122,18 @@ export class MLNode extends MLToken {
110
122
  * @see https://dom.spec.whatwg.org/#interface-node
111
123
  */
112
124
  TEXT_NODE = 3;
113
- /**
114
- * Cached `childNodes` property
115
- */
116
125
  #pureChildNodesCache;
117
126
  /**
118
127
  * Returns child nodes when parent node access by `childNodes` property.
119
128
  */
120
129
  isFragment;
121
- /**
122
- * Owner `Document`
123
- *
124
- * @implements DOM API: `Node`
125
- * @see https://dom.spec.whatwg.org/#ref-for-dom-node-ownerdocument
126
- */
127
130
  #ownerDocument;
128
- /**
129
- * Cached `prevToken` property
130
- */
131
131
  #prevToken;
132
- /**
133
- * Cached `conditionalChildNodes` method
134
- */
135
132
  #conditionalChildNodes;
136
133
  /**
137
- *
134
+ * Rules mapped to this node by `RuleMapper`. Deliberately untyped
135
+ * (`AnyRule`) storage: the `T`/`O` generics are not enforced at runtime —
136
+ * the `rule` getter recovers the typed `RuleInfo<T, O>` via a cast.
138
137
  */
139
138
  rules = {};
140
139
  _astToken;
@@ -210,6 +209,9 @@ export class MLNode extends MLToken {
210
209
  return this.childNodes[this.childNodes.length - 1] ?? null;
211
210
  }
212
211
  /**
212
+ * The next node in the syntactical sibling list which, unlike
213
+ * `nextSibling`, includes `MLBlock` nodes (AST-level traversal).
214
+ *
213
215
  * @implements `@markuplint/ml-core` API: `MLNode`
214
216
  */
215
217
  get nextNode() {
@@ -369,6 +371,9 @@ export class MLNode extends MLToken {
369
371
  return parentNode;
370
372
  }
371
373
  /**
374
+ * The previous node in the syntactical sibling list which, unlike
375
+ * `previousSibling`, includes `MLBlock` nodes (AST-level traversal).
376
+ *
372
377
  * @implements `@markuplint/ml-core` API: `MLNode`
373
378
  */
374
379
  get prevNode() {
@@ -377,6 +382,11 @@ export class MLNode extends MLToken {
377
382
  return siblings[index - 1] ?? null;
378
383
  }
379
384
  /**
385
+ * The previous node in the document-order `nodeList`.
386
+ * Omitted (ghost) elements are skipped because they have no source tokens;
387
+ * including them would break offset chains used for indentation analysis
388
+ * and source reconstruction.
389
+ *
380
390
  * @implements `@markuplint/ml-core` API: `MLNode`
381
391
  */
382
392
  get prevToken() {
@@ -549,6 +559,14 @@ export class MLNode extends MLToken {
549
559
  *
550
560
  * Note: NodeList doesn't include whitespace nodes.
551
561
  *
562
+ * For `if`/`switch` groups, a `null` sentinel is appended to each branch
563
+ * group to represent the case where no branch renders at all, so that
564
+ * content-model rules also validate the "empty branch" pattern
565
+ * (`branchesToPatterns` filters the `null` out of the generated patterns).
566
+ * `each` blocks intentionally do not start a conditional mode: their content
567
+ * is flattened as always-present rather than treated as an alternative
568
+ * branch, even though a loop may render zero times.
569
+ *
552
570
  * @returns An array of NodeLists representing the conditional child nodes.
553
571
  *
554
572
  * @experemental
@@ -8,13 +8,7 @@ import { UnexpectedCallError } from '@markuplint/shared';
8
8
  * @see https://dom.spec.whatwg.org/#interface-parentnode
9
9
  */
10
10
  export class MLParentNode extends MLNode {
11
- /**
12
- * Cached `children`
13
- */
14
11
  #children = null;
15
- /**
16
- * Cached elements that created from `querySelectorAll`
17
- */
18
12
  #selectedElements = new Map();
19
13
  /**
20
14
  * @implements DOM API: `Element`, `Document`, `DocumentFragment`
@@ -8,6 +8,14 @@ type MappingLayer = {
8
8
  readonly specificity: Specificity;
9
9
  readonly rule: AnyRule;
10
10
  };
11
+ /**
12
+ * Accumulates rule-to-node mappings and applies them to each node's `rules`
13
+ * record. Conflicts for the same rule on the same node are resolved by CSS
14
+ * selector specificity: higher specificity wins regardless of order, and on
15
+ * equal specificity the later `set()` call wins — so declaration order in
16
+ * the config arrays decides. Entries from `childNodeRules` carry the
17
+ * specificity of the parent's selector, not the child's.
18
+ */
11
19
  export declare class RuleMapper {
12
20
  #private;
13
21
  constructor(document: MLDocument<any, any>);
@@ -3,6 +3,14 @@ import { log as coreLog } from '../../debug.js';
3
3
  const ruleMapperLog = coreLog.extend('rule-mapper');
4
4
  const ruleMapperNodeLog = ruleMapperLog.extend('node');
5
5
  const ruleMapperNodeRuleLog = ruleMapperNodeLog.extend('rule');
6
+ /**
7
+ * Accumulates rule-to-node mappings and applies them to each node's `rules`
8
+ * record. Conflicts for the same rule on the same node are resolved by CSS
9
+ * selector specificity: higher specificity wins regardless of order, and on
10
+ * equal specificity the later `set()` call wins — so declaration order in
11
+ * the config arrays decides. Entries from `childNodeRules` carry the
12
+ * specificity of the parent's selector, not the child's.
13
+ */
6
14
  export class RuleMapper {
7
15
  #nodeList;
8
16
  #ruleMap = new Map();
@@ -2,6 +2,7 @@ import type { RuleSeed } from './types.js';
2
2
  import type { MLDocument } from '../ml-dom/node/document.js';
3
3
  import type { Ruleset } from '../ruleset/index.js';
4
4
  import type { LocaleSet } from '@markuplint/i18n';
5
+ import type { MLASTParseErrorCode } from '@markuplint/ml-ast';
5
6
  import type { GlobalRuleInfo, SpecConformance, PlainData, Rule, RuleConfigValue, RuleInfo, Severity, Violation } from '@markuplint/ml-config';
6
7
  /**
7
8
  * Represents a single markuplint rule that can verify documents and report violations.
@@ -28,8 +29,26 @@ export declare class MLRule<T extends RuleConfigValue, O extends PlainData = und
28
29
  /**
29
30
  * The spec conformance classification of this rule, based on RFC 2119 keyword strength.
30
31
  * Set on virtual rules derived from named nodeRules in presets.
32
+ *
33
+ * Intentionally restricted to named (preset-authored) entries: built-in
34
+ * rules already bake normative strength into `defaultSeverity`, and
35
+ * user-defined nodeRules express project conventions rather than spec
36
+ * requirements — allowing it on arbitrary config would blur "the HTML spec
37
+ * requires this" with "our team prefers this". The `/`-containing `name`
38
+ * requirement acts as the gatekeeper. It is reporting metadata only and
39
+ * never influences severity; severity changes require an explicit
40
+ * `defaultSeverity` override on the alias.
31
41
  */
32
42
  readonly specConformance?: SpecConformance;
43
+ /**
44
+ * parse5 `ERR` codes this rule's detection covers. When the rule is active
45
+ * in the ruleset, ml-core's built-in `parse-error` channel skips events
46
+ * whose code is in this list, so users do not see duplicate violations.
47
+ *
48
+ * Declared per-rule in `meta.mirrorsParseErrorCodes`; defaults to an empty
49
+ * array when the rule does not mirror any parse5 event.
50
+ */
51
+ readonly mirrorsParseErrorCodes: readonly MLASTParseErrorCode[];
33
52
  constructor(o: Readonly<RuleSeed<T, O>> & {
34
53
  readonly name: string;
35
54
  readonly baseRuleId?: string;