@markuplint/ml-core 5.0.0-rc.2 → 5.0.0-rc.5

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 (67) hide show
  1. package/CHANGELOG.md +37 -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 +18 -2
  6. package/lib/ml-core.js +223 -61
  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/virtual-rule.d.ts +10 -0
  35. package/lib/virtual-rule.js +1 -24
  36. package/package.json +14 -14
  37. package/ARCHITECTURE.ja.md +0 -676
  38. package/ARCHITECTURE.md +0 -726
  39. package/SKILL.md +0 -61
  40. package/docs/linting-pipeline.ja.md +0 -307
  41. package/docs/linting-pipeline.md +0 -307
  42. package/docs/maintenance.ja.md +0 -210
  43. package/docs/maintenance.md +0 -210
  44. package/docs/ml-dom/attr.ja.md +0 -103
  45. package/docs/ml-dom/attr.md +0 -103
  46. package/docs/ml-dom/block.ja.md +0 -272
  47. package/docs/ml-dom/block.md +0 -272
  48. package/docs/ml-dom/document.ja.md +0 -134
  49. package/docs/ml-dom/document.md +0 -134
  50. package/docs/ml-dom/element.ja.md +0 -161
  51. package/docs/ml-dom/element.md +0 -161
  52. package/docs/ml-dom/helpers.ja.md +0 -203
  53. package/docs/ml-dom/helpers.md +0 -203
  54. package/docs/ml-dom/node.ja.md +0 -199
  55. package/docs/ml-dom/node.md +0 -199
  56. package/docs/ml-dom/others.ja.md +0 -120
  57. package/docs/ml-dom/others.md +0 -120
  58. package/docs/ml-dom/overview.ja.md +0 -102
  59. package/docs/ml-dom/overview.md +0 -102
  60. package/docs/ml-dom/pretender.ja.md +0 -269
  61. package/docs/ml-dom/pretender.md +0 -269
  62. package/docs/ml-dom/rule-mapping.ja.md +0 -371
  63. package/docs/ml-dom/rule-mapping.md +0 -371
  64. package/docs/ml-dom.ja.md +0 -18
  65. package/docs/ml-dom.md +0 -18
  66. package/docs/rule-system.ja.md +0 -287
  67. package/docs/rule-system.md +0 -287
@@ -1,14 +1,6 @@
1
1
  import { computeAccessibleName, escapeCSS, getComputedRole, EMBEDDED_CONTROL_ROLES, isNativeEmbeddedControl, } from '@markuplint/ml-spec';
2
2
  import { log } from '../../debug.js';
3
3
  const accnameLog = log.extend('accname');
4
- /**
5
- * Computes the accessible name for an MLElement using the HTML-AAM algorithm.
6
- * Creates an MLCore-specific resolver that bridges MLElement to the AccnameResolver interface.
7
- *
8
- * @param el - The MLElement to compute the accessible name for
9
- * @param version - The ARIA specification version to use for role resolution
10
- * @returns The computed accessible name string, or an empty string on error
11
- */
12
4
  export function getAccname(
13
5
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
14
6
  el, version) {
@@ -18,8 +10,13 @@ el, version) {
18
10
  // environment differences (e.g., Deno lacking certain DOM APIs).
19
11
  // A single element's failure must not abort the entire linting process,
20
12
  // so we catch all errors and return an empty name (= "unnamed").
21
- // This is an intentional exception to the Tier 1 re-throw policy see
22
- // docs/architectures/ERROR-HANDLING.md § "accname computation errors".
13
+ // This is an intentional exception to the Tier 1 re-throw policy (see
14
+ // `isFatalError()` in `@markuplint/shared`): Tier-1-shaped errors here
15
+ // can be caused by the runtime environment, not only implementation bugs.
16
+ // The empty-string result is not converted to a violation either — a
17
+ // single element's accname failure must not create noise across
18
+ // unrelated rules, and "unnamed" still drives meaningful a11y rule
19
+ // violations downstream.
23
20
  try {
24
21
  const resolver = createMLCoreResolver(el, version);
25
22
  const result = computeAccessibleName(el, resolver);
@@ -131,7 +131,9 @@ export class MLAttr extends MLNode {
131
131
  this.isDirective = this._astToken.isDirective;
132
132
  this.isDuplicatable = this._astToken.isDuplicatable;
133
133
  }
134
- // IDL attribute resolution (after directivePatterns)
134
+ // IDL attribute resolution (after directivePatterns).
135
+ // Performed in the core, not in each parser: any spec opting in via
136
+ // `acceptedAttrNames` shares the same IDL-to-content-attribute mapping.
135
137
  if (ownElement.ownerMLDocument.specs.acceptedAttrNames && !this.isDirective) {
136
138
  const { contentAttrName, idlPropName } = searchIDLAttribute(this.#potentialName);
137
139
  if (contentAttrName && contentAttrName !== this.#potentialName) {
@@ -9,6 +9,12 @@ import { MLNode } from './node.js';
9
9
  * These nodes correspond to template engine constructs such as conditionals (`if`/`else`),
10
10
  * loops (`each`), and other preprocessor directives that are not part of standard HTML.
11
11
  *
12
+ * Serves as the bridge between template syntax and HTML content model
13
+ * validation: transparency keeps the wrapper invisible to DOM traversal so
14
+ * rules such as `permitted-contents` see the effective HTML children, while
15
+ * `blockBehavior` lets `conditionalChildNodes()` enumerate every possible
16
+ * rendering branch.
17
+ *
12
18
  * @template T - The rule configuration value type
13
19
  * @template O - The rule options type
14
20
  */
@@ -5,6 +5,12 @@ import { MLNode } from './node.js';
5
5
  * These nodes correspond to template engine constructs such as conditionals (`if`/`else`),
6
6
  * loops (`each`), and other preprocessor directives that are not part of standard HTML.
7
7
  *
8
+ * Serves as the bridge between template syntax and HTML content model
9
+ * validation: transparency keeps the wrapper invisible to DOM traversal so
10
+ * rules such as `permitted-contents` see the effective HTML children, while
11
+ * `blockBehavior` lets `conditionalChildNodes()` enumerate every possible
12
+ * rendering branch.
13
+ *
8
14
  * @template T - The rule configuration value type
9
15
  * @template O - The rule options type
10
16
  */
@@ -15,13 +15,4 @@ import type { PlainData, RuleConfigValue } from '@markuplint/ml-config';
15
15
  * @see https://dom.spec.whatwg.org/#idl-index
16
16
  */
17
17
  export type MLChildNode<T extends RuleConfigValue, O extends PlainData = undefined> = MLDocumentType<T, O> | MLCharacterData<T, O> | MLElement<T, O> | MLBlock<T, O>;
18
- /**
19
- * Determines whether the given node is a child node type
20
- * (DocumentType, CDATA, Comment, Text, Element, or preprocessor block).
21
- *
22
- * @template T - The rule configuration value type
23
- * @template O - The rule options type
24
- * @param node - The node to check
25
- * @returns True if the node is one of the child node types
26
- */
27
18
  export declare function isChildNode<T extends RuleConfigValue, O extends PlainData = undefined>(node: MLNode<T, O>): node is MLChildNode<T, O>;
@@ -1,12 +1,3 @@
1
- /**
2
- * Determines whether the given node is a child node type
3
- * (DocumentType, CDATA, Comment, Text, Element, or preprocessor block).
4
- *
5
- * @template T - The rule configuration value type
6
- * @template O - The rule options type
7
- * @param node - The node to check
8
- * @returns True if the node is one of the child node types
9
- */
10
1
  export function isChildNode(
11
2
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
12
3
  node) {
@@ -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();