@markuplint/ml-core 4.13.2 → 5.0.0-alpha.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 (92) hide show
  1. package/ARCHITECTURE.ja.md +524 -0
  2. package/ARCHITECTURE.md +524 -0
  3. package/CHANGELOG.md +52 -2
  4. package/README.md +5 -0
  5. package/SKILL.md +61 -0
  6. package/docs/linting-pipeline.ja.md +307 -0
  7. package/docs/linting-pipeline.md +307 -0
  8. package/docs/maintenance.ja.md +210 -0
  9. package/docs/maintenance.md +210 -0
  10. package/docs/ml-dom/attr.ja.md +103 -0
  11. package/docs/ml-dom/attr.md +103 -0
  12. package/docs/ml-dom/block.ja.md +272 -0
  13. package/docs/ml-dom/block.md +272 -0
  14. package/docs/ml-dom/document.ja.md +141 -0
  15. package/docs/ml-dom/document.md +141 -0
  16. package/docs/ml-dom/element.ja.md +176 -0
  17. package/docs/ml-dom/element.md +176 -0
  18. package/docs/ml-dom/helpers.ja.md +203 -0
  19. package/docs/ml-dom/helpers.md +203 -0
  20. package/docs/ml-dom/node.ja.md +199 -0
  21. package/docs/ml-dom/node.md +199 -0
  22. package/docs/ml-dom/others.ja.md +120 -0
  23. package/docs/ml-dom/others.md +120 -0
  24. package/docs/ml-dom/overview.ja.md +102 -0
  25. package/docs/ml-dom/overview.md +102 -0
  26. package/docs/ml-dom/pretender.ja.md +269 -0
  27. package/docs/ml-dom/pretender.md +269 -0
  28. package/docs/ml-dom/rule-mapping.ja.md +371 -0
  29. package/docs/ml-dom/rule-mapping.md +371 -0
  30. package/docs/ml-dom.ja.md +18 -0
  31. package/docs/ml-dom.md +18 -0
  32. package/docs/rule-system.ja.md +287 -0
  33. package/docs/rule-system.md +287 -0
  34. package/lib/convert-ruleset.d.ts +7 -0
  35. package/lib/convert-ruleset.js +7 -0
  36. package/lib/debug.d.ts +4 -0
  37. package/lib/debug.js +4 -0
  38. package/lib/index.d.ts +4 -3
  39. package/lib/index.js +1 -1
  40. package/lib/ml-core.d.ts +37 -1
  41. package/lib/ml-core.js +171 -82
  42. package/lib/ml-dom/helper/accname.d.ts +8 -0
  43. package/lib/ml-dom/helper/accname.js +71 -55
  44. package/lib/ml-dom/helper/create-node.js +1 -0
  45. package/lib/ml-dom/helper/get-indent.d.ts +4 -1
  46. package/lib/ml-dom/helper/get-indent.js +21 -30
  47. package/lib/ml-dom/node/attr.d.ts +65 -4
  48. package/lib/ml-dom/node/attr.js +151 -53
  49. package/lib/ml-dom/node/block.d.ts +23 -2
  50. package/lib/ml-dom/node/block.js +24 -1
  51. package/lib/ml-dom/node/child-node.d.ts +9 -0
  52. package/lib/ml-dom/node/child-node.js +9 -0
  53. package/lib/ml-dom/node/comment.d.ts +7 -0
  54. package/lib/ml-dom/node/comment.js +7 -0
  55. package/lib/ml-dom/node/document-fragment.d.ts +8 -0
  56. package/lib/ml-dom/node/document-fragment.js +8 -0
  57. package/lib/ml-dom/node/document-type.d.ts +22 -0
  58. package/lib/ml-dom/node/document-type.js +25 -0
  59. package/lib/ml-dom/node/document.d.ts +88 -7
  60. package/lib/ml-dom/node/document.js +128 -32
  61. package/lib/ml-dom/node/dom-token-list.js +17 -30
  62. package/lib/ml-dom/node/element-close-tag.js +1 -0
  63. package/lib/ml-dom/node/element.d.ts +151 -5
  64. package/lib/ml-dom/node/element.js +242 -50
  65. package/lib/ml-dom/node/node-store.js +6 -15
  66. package/lib/ml-dom/node/node.d.ts +19 -1
  67. package/lib/ml-dom/node/node.js +175 -166
  68. package/lib/ml-dom/node/parent-node.js +14 -30
  69. package/lib/ml-dom/node/rule-mapper.js +7 -20
  70. package/lib/ml-dom/node/text.d.ts +19 -0
  71. package/lib/ml-dom/node/text.js +21 -0
  72. package/lib/ml-dom/node/types.d.ts +68 -0
  73. package/lib/ml-dom/token/token.d.ts +42 -0
  74. package/lib/ml-dom/token/token.js +59 -39
  75. package/lib/ml-rule/create-rule.d.ts +17 -1
  76. package/lib/ml-rule/ml-rule-context.js +7 -11
  77. package/lib/ml-rule/ml-rule.d.ts +66 -1
  78. package/lib/ml-rule/ml-rule.js +95 -25
  79. package/lib/ml-rule/types.d.ts +41 -0
  80. package/lib/plugin/plugin.d.ts +8 -0
  81. package/lib/plugin/plugin.js +8 -0
  82. package/lib/plugin/types.d.ts +21 -0
  83. package/lib/ruleset/index.d.ts +10 -0
  84. package/lib/ruleset/index.js +13 -0
  85. package/lib/test/index.d.ts +42 -1
  86. package/lib/test/index.js +39 -2
  87. package/lib/types.d.ts +10 -1
  88. package/lib/violation-collector.d.ts +33 -0
  89. package/lib/violation-collector.js +48 -28
  90. package/lib/virtual-rule.d.ts +72 -0
  91. package/lib/virtual-rule.js +233 -0
  92. package/package.json +16 -13
@@ -0,0 +1,524 @@
1
+ # @markuplint/ml-core
2
+
3
+ ## Overview
4
+
5
+ `@markuplint/ml-core` is the core linting engine of markuplint. It converts a parsed AST (`MLASTDocument`) into a DOM tree (`MLDOM`), applies configured rules against nodes, and collects violations. The package comprises three subsystems: **MLDOM** (DOM abstraction layer), **MLRule** (rule execution framework), and **MLCore** (orchestration engine).
6
+
7
+ ## Directory Structure
8
+
9
+ ```
10
+ src/
11
+ ├── index.ts — Public API re-exports
12
+ ├── ml-core.ts — MLCore engine class
13
+ ├── types.ts — MLFabric, MLSchema type definitions
14
+ ├── convert-ruleset.ts — Config → Ruleset converter
15
+ ├── debug.ts — Debug logging utilities
16
+ ├── violation-collector.ts — Multi-file violation aggregator
17
+ ├── ml-dom/
18
+ │ ├── index.ts — MLDOM public exports
19
+ │ ├── node/
20
+ │ │ ├── document.ts — MLDocument (root node, rule mapping, pretender init)
21
+ │ │ ├── element.ts — MLElement (attributes, selectors, namespaces)
22
+ │ │ ├── node.ts — MLNode (abstract base for all nodes)
23
+ │ │ ├── parent-node.ts — MLParentNode (querySelector, children)
24
+ │ │ ├── character-data.ts — MLCharacterData (abstract text base)
25
+ │ │ ├── text.ts — MLText
26
+ │ │ ├── comment.ts — MLComment
27
+ │ │ ├── attr.ts — MLAttr (attribute tokens)
28
+ │ │ ├── block.ts — MLBlock (preprocessor blocks)
29
+ │ │ ├── document-fragment.ts — MLDocumentFragment
30
+ │ │ ├── document-type.ts — MLDocumentType
31
+ │ │ ├── element-close-tag.ts — MLElementCloseTag
32
+ │ │ ├── rule-mapper.ts — RuleMapper (ruleset → node mapping)
33
+ │ │ ├── types.ts — Node type constants, AccessibilityProperties
34
+ │ │ ├── node-list.ts — NodeList/HTMLCollection utilities
35
+ │ │ └── unexpected-call-error.ts — Error for unsupported DOM methods
36
+ │ ├── token/
37
+ │ │ └── token.ts — MLToken (base positional token)
38
+ │ ├── helper/
39
+ │ │ ├── accname.ts — Accessible name computation
40
+ │ │ ├── create-node.ts — AST → MLDOM node factory
41
+ │ │ ├── walkers.ts — Tree traversal (sync/async walkers)
42
+ │ │ ├── get-indent.ts — Indentation analysis
43
+ │ │ └── debug.ts — Debug map generation
44
+ │ └── manipulations/
45
+ │ ├── child-node-methods.ts — ChildNode interface stubs
46
+ │ └── get-children.ts — Element children extraction
47
+ ├── virtual-rule.ts — Named nodeRule expansion (expandNamedNodeRules)
48
+ ├── virtual-rule.spec.ts — Virtual rule unit tests
49
+ ├── ml-rule/
50
+ │ ├── ml-rule.ts — MLRule class (rule execution)
51
+ │ ├── ml-rule-context.ts — MLRuleContext (report collection)
52
+ │ ├── create-rule.ts — createRule factory
53
+ │ ├── create-test-rule.ts — Test rule factory
54
+ │ └── types.ts — RuleSeed, Checker types
55
+ ├── ruleset/
56
+ │ └── index.ts — Ruleset class (rules + nodeRules + childNodeRules)
57
+ ├── plugin/
58
+ │ ├── plugin.ts — createPlugin factory
59
+ │ ├── types.ts — Plugin, PluginCreator types
60
+ │ └── index.ts — Plugin exports
61
+ ├── test/
62
+ │ └── index.ts — createTestDocument, createTestElement, dummySchemas
63
+ └── utils/
64
+ ├── index.ts — Utility exports
65
+ ├── get-location-from-chars.ts — Character location resolver
66
+ └── string-splice.ts — String splice helper
67
+ ```
68
+
69
+ ## Architecture Diagram
70
+
71
+ ```mermaid
72
+ flowchart TD
73
+ subgraph upstream ["Upstream Dependencies"]
74
+ mlAst["@markuplint/ml-ast\n(AST types)"]
75
+ mlConfig["@markuplint/ml-config\n(Config, RuleConfigValue)"]
76
+ mlSpec["@markuplint/ml-spec\n(HTML/ARIA specs)"]
77
+ htmlSpec["@markuplint/html-spec\n(Default spec data)"]
78
+ htmlParser["@markuplint/html-parser\n(Default parser)"]
79
+ parserUtils["@markuplint/parser-utils\n(ParserOptions)"]
80
+ selector["@markuplint/selector\n(CSS selector matching)"]
81
+ i18n["@markuplint/i18n\n(Locale, Translator)"]
82
+ shared["@markuplint/shared\n(Utilities)"]
83
+ configPresets["@markuplint/config-presets\n(Built-in presets)"]
84
+ end
85
+
86
+ subgraph pkg ["@markuplint/ml-core"]
87
+ subgraph mldom ["MLDOM"]
88
+ document["MLDocument"]
89
+ element["MLElement"]
90
+ node["MLNode / MLToken"]
91
+ ruleMapper["RuleMapper"]
92
+ end
93
+
94
+ subgraph mlRule ["MLRule"]
95
+ rule["MLRule"]
96
+ ruleContext["MLRuleContext"]
97
+ createRule["createRule()"]
98
+ end
99
+
100
+ subgraph engine ["Engine"]
101
+ core["MLCore"]
102
+ ruleset["Ruleset"]
103
+ convertRuleset["convertRuleset()"]
104
+ end
105
+
106
+ subgraph extras ["Extras"]
107
+ plugin["Plugin / createPlugin()"]
108
+ testUtils["Test Utilities"]
109
+ end
110
+ end
111
+
112
+ subgraph downstream ["Downstream"]
113
+ rules["@markuplint/rules\n(Built-in rules)"]
114
+ markuplint["markuplint\n(CLI & API)"]
115
+ end
116
+
117
+ upstream -->|"types, parsing, specs"| pkg
118
+ core --> document
119
+ core --> rule
120
+ document --> ruleMapper
121
+ rule --> ruleContext
122
+ pkg -->|"MLDOM, MLRule, MLCore"| downstream
123
+ ```
124
+
125
+ ## Linting Pipeline
126
+
127
+ The `MLCore.verify()` method orchestrates the full linting flow:
128
+
129
+ ```mermaid
130
+ flowchart LR
131
+ A["MLCore\nconstructor"]
132
+ B["_parse()\nParser → MLASTDocument"]
133
+ C["_createDocument()\nMLASTDocument → MLDocument"]
134
+ D["verify(fix?)\nFor each rule:"]
135
+ E["document.setRule(rule)\nRuleMapper maps config → nodes"]
136
+ F["rule.verify(document)\nMLRuleContext collects reports"]
137
+ G["Violations[]"]
138
+
139
+ A --> B --> C --> D --> E --> F --> G
140
+ ```
141
+
142
+ ### Step-by-step
143
+
144
+ 1. **Parse**: `MLCore` invokes the configured parser (`MLParser`) to produce an `MLASTDocument`
145
+ 2. **Create Document**: The AST is wrapped in an `MLDocument`, which builds the full MLDOM tree via `createNode()` factory. `RuleMapper` resolves rule configuration for every node
146
+ 3. **Verify**: For each `MLRule`, the engine calls `document.setRule(rule)` then `rule.verify(document)`. The rule walks relevant nodes via `document.walkOn()` and reports violations through `MLRuleContext`
147
+ 4. **Fix** (optional): When `fix=true`, rules may call `node.fix()` to modify token content. `document.toString(true)` produces the fixed source
148
+
149
+ ## MLDOM Class Hierarchy
150
+
151
+ ```
152
+ MLToken<A extends MLASTToken>
153
+ └── MLNode<T, O, A extends MLASTNode>
154
+ ├── MLAttr<T, O>
155
+ ├── MLCharacterData<T, O, A> (abstract)
156
+ │ ├── MLText<T, O>
157
+ │ └── MLComment<T, O>
158
+ ├── MLDocumentType<T, O>
159
+ ├── MLBlock<T, O>
160
+ ├── MLElementCloseTag<T, O>
161
+ └── MLParentNode<T, O, A> (abstract)
162
+ ├── MLElement<T, O>
163
+ ├── MLDocumentFragment<T, O>
164
+ └── MLDocument<T, O>
165
+ ```
166
+
167
+ ### Class Responsibilities
168
+
169
+ | Class | DOM Interface | Key Responsibility |
170
+ | -------------------- | ------------------ | ------------------------------------------------------------------------------------------- |
171
+ | `MLToken` | — | Base token with position tracking (`startLine`, `endCol`, `raw`, `fixed`), `fix()` method |
172
+ | `MLNode` | `Node` | Tree structure (`parentNode`, `childNodes`, `nextSibling`), rule storage, `is()` type guard |
173
+ | `MLAttr` | `Attr` | Attribute name/value tokens, `isDynamicValue`, `isDirective`, `valueType`, `tokenList` |
174
+ | `MLCharacterData` | `CharacterData` | Abstract base for text content nodes (`data`, `nodeValue`) |
175
+ | `MLText` | `Text` | Text nodes, `isWhitespace()`, `isRawTextElementContent()` |
176
+ | `MLComment` | `Comment` | Comment nodes with `textContent` |
177
+ | `MLDocumentType` | `DocumentType` | `<!DOCTYPE>` with `name`, `publicId`, `systemId` |
178
+ | `MLBlock` | — | Preprocessor-specific blocks (if/each/switch), `blockBehavior`, `isTransparent` |
179
+ | `MLElementCloseTag` | — | Close tag paired with its open tag element |
180
+ | `MLParentNode` | `ParentNode` | `querySelector()`, `querySelectorAll()`, `children`, `childElementCount` |
181
+ | `MLElement` | `Element` | Attributes, selectors, namespaces, pretender context, `elementType`, `closeTag` |
182
+ | `MLDocumentFragment` | `DocumentFragment` | Fragment root node |
183
+ | `MLDocument` | `Document` | Root node, `nodeList`, `walkOn()`, `setRule()`, rule mapping, spec access |
184
+
185
+ ## MLDocument
186
+
187
+ `MLDocument` is the root of the MLDOM tree and the primary interface for rule execution.
188
+
189
+ ### Construction
190
+
191
+ The constructor receives an `MLASTDocument`, a `Ruleset`, and an `MLSchema` tuple. It:
192
+
193
+ 1. Builds the flat `nodeList` by traversing the AST and calling `createNode()` for each AST node
194
+ 2. Initializes `RuleMapper` to distribute rule configuration across nodes
195
+ 3. Sets up pretender contexts when pretender definitions are provided
196
+
197
+ ### Key Properties
198
+
199
+ | Property | Type | Description |
200
+ | ------------- | ----------------------- | --------------------------------------------------------- |
201
+ | `nodeList` | `ReadonlyArray<MLNode>` | Flat list of all nodes in document order |
202
+ | `specs` | `MLMLSpec` | HTML/ARIA specification data |
203
+ | `isFragment` | `boolean` | Whether the document is a fragment |
204
+ | `currentRule` | `MLRule \| null` | The rule currently being evaluated |
205
+ | `endTag` | `EndTagType` | End tag handling mode (`'xml'`, `'omittable'`, `'never'`) |
206
+
207
+ ### Key Methods
208
+
209
+ | Method | Description |
210
+ | --------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
211
+ | `walkOn(type, walker)` | Walks nodes of a given type (`'Element'`, `'Text'`, `'Comment'`, `'Attr'`, `'ElementCloseTag'`) |
212
+ | `setRule(rule)` | Sets the current rule, used by `MLCore` during verification |
213
+ | `getTokenList()` | Returns all tokens for source reconstruction |
214
+ | `searchNodeByLocation(line, col)` | Finds the node at a given source position |
215
+ | `getAccessibilityProp(node)` | Computes ARIA accessibility properties (delegates to `MLElement.getAccessibleName()` for cached accessible name) |
216
+ | `toString(fixed?)` | Reconstructs source code, optionally with fixes applied |
217
+
218
+ ## MLElement
219
+
220
+ `MLElement` represents an HTML/SVG/MathML element with full attribute access and selector matching.
221
+
222
+ ### Key Properties
223
+
224
+ | Property | Type | Description |
225
+ | ------------------ | --------------------------- | -------------------------------------------- |
226
+ | `localName` | `string` | Lowercase tag name (for HTML) |
227
+ | `namespaceURI` | `NamespaceURI` | Element namespace (HTML, SVG, MathML) |
228
+ | `attributes` | `MLNamedNodeMap` | Named attribute collection |
229
+ | `elementType` | `ElementType` | `'html'`, `'web-component'`, or `'authored'` |
230
+ | `closeTag` | `MLElementCloseTag \| null` | Paired close tag |
231
+ | `pretenderContext` | `PretenderContext \| null` | Pretender mapping context |
232
+ | `isForeignElement` | `boolean` | `true` for SVG/MathML elements |
233
+ | `isOmitted` | `boolean` | `true` for implicitly inserted elements |
234
+
235
+ ### Key Methods
236
+
237
+ | Method | Description |
238
+ | ---------------------------- | ---------------------------------------------------------------- |
239
+ | `getAttribute(name)` | Returns attribute value or `null` |
240
+ | `getAttributeToken(name)` | Returns `MLAttr[]` for the named attribute |
241
+ | `hasAttribute(name)` | Checks attribute existence |
242
+ | `getAccessibleName(version)` | Cached accessible name computation (memoized per ARIA version) |
243
+ | `matches(selector)` | CSS selector matching |
244
+ | `matchMLSelector(selector)` | Extended markuplint selector matching (supports `RegexSelector`) |
245
+ | `querySelector(selector)` | Finds first matching descendant |
246
+ | `querySelectorAll(selector)` | Finds all matching descendants |
247
+
248
+ ## Rule System
249
+
250
+ ### MLRule
251
+
252
+ `MLRule<T, O>` encapsulates a linting rule with verification and optional fix logic.
253
+
254
+ | Property/Method | Description |
255
+ | --------------------------------- | --------------------------------------------------------------------------- |
256
+ | `name` | Rule identifier (e.g., `"attr-duplication"`) |
257
+ | `defaultSeverity` | Default severity level |
258
+ | `defaultValue` / `defaultOptions` | Default configuration |
259
+ | `baseRuleId` | For virtual rules: the base rule's name (e.g., `"required-attr"`) |
260
+ | `groupName` | For multi-entry virtual rules: group name for batch disable |
261
+ | `specConformance` | For virtual rules: `'normative'` or `'non-normative'` (from named nodeRule) |
262
+ | `verify(document, locale, fix)` | Executes the rule and returns violations |
263
+ | `createAlias(name, options?)` | Creates a virtual rule that reuses this rule's verify/fix logic |
264
+ | `optimizeOption(settings)` | Normalizes raw rule configuration into `RuleInfo` |
265
+
266
+ ### RuleSeed
267
+
268
+ The `RuleSeed<T, O>` type defines the rule implementation:
269
+
270
+ ```typescript
271
+ type RuleSeed<T, O> = {
272
+ meta?: {
273
+ category?: 'validation' | 'style' | 'naming-convention' | 'a11y' | 'maintainability';
274
+ };
275
+ defaultSeverity?: Severity;
276
+ defaultValue?: T;
277
+ defaultOptions?: O;
278
+ verify(context): void | Promise<void>;
279
+ fix?(context): void | Promise<void>;
280
+ };
281
+ ```
282
+
283
+ ### createRule
284
+
285
+ `createRule(seed)` is a factory function for type-safe rule seed creation. It returns the seed as-is, serving primarily as a type helper.
286
+
287
+ ### MLRuleContext
288
+
289
+ `MLRuleContext<T, O>` provides the execution context for rules:
290
+
291
+ - `document` — The current `MLDocument`
292
+ - `translate` / `t` — Locale-aware message translator
293
+ - `report(report)` — Reports a violation with node, message, and optional fix
294
+
295
+ The `provide()` method returns the context object passed to `RuleSeed.verify()` and `RuleSeed.fix()`.
296
+
297
+ ### Rule Configuration Resolution
298
+
299
+ Rules are configured at three levels, resolved by `RuleMapper`:
300
+
301
+ 1. **Global rules** (`rules`) — Apply to all nodes; lowest priority
302
+ 2. **Node rules** (`nodeRules`) — Apply to nodes matching a selector; medium priority
303
+ 3. **Child node rules** (`childNodeRules`) — Apply to children of nodes matching a selector; highest priority
304
+
305
+ When multiple rules match, `RuleMapper` resolves conflicts using CSS selector specificity. The mapping is computed once during `MLDocument` construction and stored on each `MLNode.rules`.
306
+
307
+ ### Rule Execution Flow
308
+
309
+ ```mermaid
310
+ flowchart TD
311
+ A["MLCore.verify()"] --> B["For each MLRule"]
312
+ B --> C["document.setRule(rule)"]
313
+ C --> D["rule.verify(document, locale, fix)"]
314
+ D --> E["rule.getRuleInfo(ruleset)\nResolve global config"]
315
+ E --> F["document.walkOn(type, walker)\nIterate matching nodes"]
316
+ F --> G["context.report()\nCollect violations per node"]
317
+ G --> H["Return Violation[]"]
318
+ ```
319
+
320
+ ### Virtual Rule System
321
+
322
+ Source: `src/virtual-rule.ts`
323
+
324
+ > **Terminology policy**: "Virtual rule" is an **internal implementation term** for contributors only. User-facing documentation (website, migration guides, README) must use **"named rule"** instead. From a config user's perspective, there are only two concepts: a **base rule** (e.g., `required-attr`) and a **named rule** (e.g., `a11y/html-lang`). The internal mechanics of `MLRule` aliasing should not be exposed.
325
+
326
+ Virtual rules are independent `MLRule` instances created from **named nodeRules** — nodeRule entries with a `name` property containing `/` (e.g., `"a11y/html-lang"`). This enables per-check control: each virtual rule can be independently enabled/disabled via `rules["alias/name"]: false`.
327
+
328
+ #### Named NodeRule Expansion
329
+
330
+ `expandNamedNodeRules()` converts named nodeRules (and childNodeRules) into virtual rules during `MLCore` construction:
331
+
332
+ ```
333
+ Named nodeRule (config) Virtual MLRule (runtime)
334
+ ┌─────────────────────────┐ ┌──────────────────────────┐
335
+ │ name: "a11y/html-lang" │ │ name: "a11y/html-lang" │
336
+ │ specConformance: "norm."│ ──────► │ baseRuleId: "required-attr" │
337
+ │ selector: ":where(html)"│ │ specConformance: metadata│
338
+ │ rules: │ │ verify/fix: from base │
339
+ │ required-attr: [lang] │ └──────────────────────────┘
340
+ └─────────────────────────┘
341
+ ```
342
+
343
+ Key behaviors:
344
+
345
+ - **False entry separation**: `false` entries in `rules` are automatically separated into unnamed nodeRules, preserving their semantics as base-rule specificity overrides
346
+ - **Multi-entry support**: Named nodeRules with 2+ non-false entries create derived names (`name/baseRuleName`) with a `groupName` for group disable
347
+ - **Metadata**: `specConformance` is attached to the virtual rule as metadata for downstream tools and reporting
348
+ - **Hot-reload**: Pre-expansion nodeRules are preserved in `#originalNodeRules` / `#originalChildNodeRules` so `update()` can re-expand them
349
+
350
+ #### Why `specConformance` Is Restricted to Named NodeRules
351
+
352
+ `specConformance` is intentionally available **only on named nodeRules** (in presets), not on regular built-in rules. The design rationale:
353
+
354
+ 1. **Built-in rules already have correct default severity.** Rules like `permitted-contents` or `required-attr` are inherently normative (they enforce WHATWG MUST requirements), and their `defaultSeverity` is already set to `'error'`. There is no need for a separate `specConformance` flag — the severity is baked in.
355
+
356
+ 2. **Named nodeRules are preset-authored spec interpretations.** When a preset like `preset.html-standard.jsonc` creates a named nodeRule `"html-standard/head-charset-utf8"`, the preset author is expressing a specific spec requirement as a check. `specConformance` lets the author declare the RFC 2119 keyword strength of that requirement, so downstream tools and reports can identify which violations originate from spec requirements and at what normative level.
357
+
358
+ 3. **Users should not set `specConformance` on their own rules.** A user-defined nodeRule for a custom component (e.g., validating `<MyComponent>` props) is not a spec conformance check — it is a project convention. Allowing `specConformance` on arbitrary user config would blur the distinction between "the HTML spec requires this" and "our team prefers this". The `name` property (which requires `/`) serves as a gatekeeper: only named nodeRules can carry `specConformance`, and named nodeRules are designed for preset authors who understand the spec.
359
+
360
+ In summary: `specConformance` is a **preset-level annotation** that provides metadata about which spec requirements a check enforces. Built-in rules handle their own severity via `defaultSeverity`. User-defined rules express severity directly via the `severity` field in rule config.
361
+
362
+ #### Virtual Rule Disable
363
+
364
+ Virtual rules can be disabled at three levels in the `rules` config:
365
+
366
+ 1. **Exact name**: `rules["a11y/html-lang"]: false`
367
+ 2. **Group disable**: `rules["custom/multi"]: false` (for multi-entry named nodeRules)
368
+ 3. **Namespace wildcard**: `rules["a11y/*"]: false` (disables all virtual rules starting with `a11y/`)
369
+
370
+ ## Pretender System
371
+
372
+ The pretender system allows components to be treated as semantic HTML elements during linting. This enables rules to validate custom components (e.g., `<MyButton>`) as if they were standard elements (e.g., `<button>`).
373
+
374
+ ### Configuration
375
+
376
+ Pretenders are defined in the markuplint config as an array of `Pretender` objects:
377
+
378
+ ```typescript
379
+ type Pretender = {
380
+ selector: string; // CSS selector matching the component
381
+ as: string; // HTML element to pretend as
382
+ aria?: PretenderARIA; // Optional ARIA overrides
383
+ };
384
+ ```
385
+
386
+ ### How It Works
387
+
388
+ 1. During `MLDocument` construction, pretender definitions are processed
389
+ 2. Each `MLElement` matching a pretender selector gets a `pretenderContext` with `type: 'pretender'`
390
+ 3. The target HTML element gets a `pretenderContext` with `type: 'origin'`
391
+ 4. Rules can access `element.pretenderContext` to check the semantic mapping
392
+ 5. Accessibility computations use pretender context for role/name resolution
393
+
394
+ ## Conditional Child Nodes
395
+
396
+ Template engines (Pug, EJS, Nunjucks, etc.) produce preprocessor-specific blocks represented by `MLBlock` nodes. These blocks can wrap child nodes conditionally:
397
+
398
+ | `blockBehavior.type` | Template Construct | Description |
399
+ | -------------------- | ------------------ | -------------------------- |
400
+ | `'if'` | `{% if %}` | Start of conditional block |
401
+ | `'if:else'` | `{% else %}` | Alternative branch |
402
+ | `'end'` | `{% endif %}` | End of conditional block |
403
+ | `'each'` | `{% for %}` | Start of loop |
404
+ | `'end'` | `{% endfor %}` | End of loop |
405
+ | `'switch:case'` | `{% switch %}` | Start of switch |
406
+ | `'switch:default'` | `{% case %}` | Switch case |
407
+ | `'end'` | `{% endswitch %}` | End of switch |
408
+
409
+ `MLNode.conditionalChildNodes()` returns an array of `NodeListOf` arrays — one per conditional branch — so rules can analyze each branch independently. Note that `'each'` blocks do not start a new conditional mode (`'if'` or `'switch'`); they are flattened into `childNodes` and their content is treated as always-present rather than as an alternative branch. Only `'if'`/`'if:elseif'` and `'switch:case'` start new modes that generate null sentinels for the "empty branch" case.
410
+
411
+ ## Plugin System
412
+
413
+ Plugins extend markuplint with custom rules and shared configurations.
414
+
415
+ ### Plugin Type
416
+
417
+ ```typescript
418
+ type Plugin = {
419
+ readonly name: string;
420
+ readonly rules?: Record<string, RuleSeed<any, any>>;
421
+ readonly configs?: Record<string, Config>;
422
+ };
423
+ ```
424
+
425
+ ### PluginCreator
426
+
427
+ For plugins that accept settings:
428
+
429
+ ```typescript
430
+ type PluginCreator<S> = {
431
+ readonly name: string;
432
+ create(setting: S): Omit<Plugin, 'name'>;
433
+ };
434
+ ```
435
+
436
+ `createPlugin(creator)` is a factory function for type-safe plugin creator definitions.
437
+
438
+ ## Test Utilities
439
+
440
+ The `test/` module provides helpers for rule testing:
441
+
442
+ | Function | Description |
443
+ | ------------------------------------------- | ----------------------------------------------- |
444
+ | `createTestDocument(sourceCode, options?)` | Parses source into an `MLDocument` for testing |
445
+ | `createTestElement(sourceCode, options?)` | Parses source and returns the first `MLElement` |
446
+ | `createTestNodeList(sourceCode, options?)` | Returns the flat node list from parsed source |
447
+ | `createTestTokenList(sourceCode, options?)` | Returns the flat token list from parsed source |
448
+ | `dummySchemas()` | Returns the default HTML spec as a schema tuple |
449
+
450
+ `CreateTestOptions` accepts `config`, `parser`, `specs`, and `pretenders` overrides.
451
+
452
+ ## External Dependencies
453
+
454
+ | Dependency | Purpose |
455
+ | ---------------------------- | ----------------------------------------------------------------- |
456
+ | `@markuplint/ml-ast` | AST type definitions (`MLASTDocument`, `MLASTNode`, etc.) |
457
+ | `@markuplint/ml-config` | Configuration types (`Config`, `RuleConfigValue`, `Pretender`) |
458
+ | `@markuplint/ml-spec` | HTML/ARIA specification access (`MLMLSpec`, role/attribute specs) |
459
+ | `@markuplint/html-spec` | Default HTML specification data |
460
+ | `@markuplint/html-parser` | Default HTML parser (used in test utilities) |
461
+ | `@markuplint/parser-utils` | Parser options and types |
462
+ | `@markuplint/selector` | CSS and extended selector matching |
463
+ | `@markuplint/i18n` | Internationalization (`LocaleSet`, `Translator`) |
464
+ | `@markuplint/shared` | Shared utilities |
465
+ | `@markuplint/config-presets` | Built-in configuration presets |
466
+ | `debug` | Debug logging |
467
+ | `is-plain-object` | Plain object type checking |
468
+ | `type-fest` | TypeScript utility types |
469
+
470
+ ## Integration Points
471
+
472
+ ```mermaid
473
+ flowchart TD
474
+ subgraph upstream ["Upstream"]
475
+ mlAst["@markuplint/ml-ast"]
476
+ mlConfig["@markuplint/ml-config"]
477
+ mlSpec["@markuplint/ml-spec"]
478
+ htmlSpec["@markuplint/html-spec"]
479
+ htmlParser["@markuplint/html-parser"]
480
+ parserUtils["@markuplint/parser-utils"]
481
+ selector["@markuplint/selector"]
482
+ i18n["@markuplint/i18n"]
483
+ shared["@markuplint/shared"]
484
+ configPresets["@markuplint/config-presets"]
485
+ end
486
+
487
+ subgraph pkg ["@markuplint/ml-core"]
488
+ core["MLCore Engine"]
489
+ end
490
+
491
+ subgraph downstream ["Downstream"]
492
+ rules["@markuplint/rules\n(Built-in rule implementations)"]
493
+ markuplint["markuplint\n(CLI, API, MLEngine)"]
494
+ end
495
+
496
+ upstream -->|"types, parsing, specs, i18n"| core
497
+ core -->|"MLDOM classes, MLRule,\ncreateRule, test utils"| rules
498
+ core -->|"MLCore, ViolationCollector,\nconvertRuleset, Plugin types"| markuplint
499
+ ```
500
+
501
+ ### Upstream
502
+
503
+ - **`@markuplint/ml-ast`** — AST types used to construct the MLDOM tree
504
+ - **`@markuplint/ml-config`** — Config and rule configuration types
505
+ - **`@markuplint/ml-spec`** — HTML/ARIA specification for element validation, role computation
506
+ - **`@markuplint/html-spec`** — Default spec data bundle
507
+ - **`@markuplint/html-parser`** — Default parser used in test utilities
508
+ - **`@markuplint/parser-utils`** — Parser option types
509
+ - **`@markuplint/selector`** — CSS selector engine for `querySelector`, `matches`, and `RegexSelector`
510
+ - **`@markuplint/i18n`** — Locale sets and translation for rule messages
511
+ - **`@markuplint/shared`** — Shared utility functions
512
+ - **`@markuplint/config-presets`** — Built-in configuration presets
513
+
514
+ ### Downstream
515
+
516
+ - **`@markuplint/rules`** — Imports MLDOM classes, `createRule`, `MLRuleContext`, and test utilities to implement built-in rules
517
+ - **`markuplint`** — Imports `MLCore`, `ViolationCollector`, `convertRuleset`, and plugin types to provide the CLI and API
518
+
519
+ ## Documentation Map
520
+
521
+ - [MLDOM Reference](docs/ml-dom.md) ([日本語](docs/ml-dom.ja.md)) — Class hierarchy, node properties, tree traversal
522
+ - [Rule System](docs/rule-system.md) ([日本語](docs/rule-system.ja.md)) — MLRule, RuleSeed, MLRuleContext, configuration resolution
523
+ - [Linting Pipeline](docs/linting-pipeline.md) ([日本語](docs/linting-pipeline.ja.md)) — MLCore engine, verify flow, pretender, plugin system
524
+ - [Maintenance Guide](docs/maintenance.md) ([日本語](docs/maintenance.ja.md)) — Commands, recipes, and troubleshooting
package/CHANGELOG.md CHANGED
@@ -3,13 +3,63 @@
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
- ## [4.13.2](https://github.com/markuplint/markuplint/compare/@markuplint/ml-core@4.13.1...@markuplint/ml-core@4.13.2) (2025-11-05)
6
+ # [5.0.0-alpha.0](https://github.com/markuplint/markuplint/compare/v4.14.1...v5.0.0-alpha.0) (2026-02-20)
7
7
 
8
- **Note:** Version bump only for package @markuplint/ml-core
8
+ ### Bug Fixes
9
+
10
+ - **ml-core:** improve detection of namespace ([5b507ad](https://github.com/markuplint/markuplint/commit/5b507ad7c19c5015b8ce587845d901e31dfa6518))
11
+ - treat orphaned end tags as bogus instead of plain text ([#1575](https://github.com/markuplint/markuplint/issues/1575)) ([557199a](https://github.com/markuplint/markuplint/commit/557199a6960ab35573a544f9a33c00e98eb9967e))
12
+ - use explicit `export type` for type-only re-exports ([7c77c05](https://github.com/markuplint/markuplint/commit/7c77c05619518c8d18a183132040f5b2cd0ab6ec))
13
+
14
+ - feat(ml-core)!: adapt DOM layer to simplified AST types ([5d92f2b](https://github.com/markuplint/markuplint/commit/5d92f2be75ce0d45823fb26f72588aecee278ba3))
15
+
16
+ ### Features
17
+
18
+ - delete htmx-parser, simplify alpine-parser, add migration guide and tests ([f8dbb09](https://github.com/markuplint/markuplint/commit/f8dbb090707d8cfbf3d859a9b868b2087064f89b))
19
+ - **ml-core:** add directive and IDL resolution to MLAttr constructor ([ba0ad66](https://github.com/markuplint/markuplint/commit/ba0ad66585c022cdb34fda8a8191bcc9af078e07))
20
+ - **ml-core:** add expandNamedRules for named rule groups in rules section ([7eed355](https://github.com/markuplint/markuplint/commit/7eed355075cee90b17a79c0f8a5b18213d1ce54e))
21
+ - **ml-core:** implement VirtualRule system for named nodeRules ([864f51d](https://github.com/markuplint/markuplint/commit/864f51d54dba26c6af2bc45eea3566db5f7d8e26))
22
+ - **ml-core:** require defaultValue for non-boolean rule types in createRule ([6c99908](https://github.com/markuplint/markuplint/commit/6c999087feff4fb8906cf47d564ee08ca8e5f450)), closes [#808](https://github.com/markuplint/markuplint/issues/808)
23
+ - **ml-core:** the each block skips linting in childNodes ([d5ca83d](https://github.com/markuplint/markuplint/commit/d5ca83d5ec6dc9b2f40b5d6599b07cc4746f3dca))
24
+ - **ml-core:** wire ruleCommonSettings through MLCore to Document ([28bb176](https://github.com/markuplint/markuplint/commit/28bb17601b983b3789b2ae200bd77ad887905cda))
25
+ - **ml-spec:** add declarative directivePatterns for parser-less framework support ([ceb9aa6](https://github.com/markuplint/markuplint/commit/ceb9aa67048e3a058b40a9e4d91eb903c8ff1861))
26
+
27
+ ### Performance Improvements
28
+
29
+ - **ml-core:** add memoization cache to MLElement.getAccessibleName() ([cdbe289](https://github.com/markuplint/markuplint/commit/cdbe289755312ee30e3f02171f42bf2c00412eea)), closes [#2179](https://github.com/markuplint/markuplint/issues/2179)
30
+
31
+ ### BREAKING CHANGES
32
+
33
+ - Multiple breaking changes to DOM API:
9
34
 
35
+ MLToken:
10
36
 
37
+ - Compute end positions via getEndCol/getEndLine helpers
38
+ instead of storing them as private fields
39
+ - Use \_astToken.offset/line/col directly
11
40
 
41
+ MLElement:
12
42
 
43
+ - Remove selfClosingSolidus property
44
+ - Add blockBehavior: MLASTBlockBehavior | null
45
+
46
+ MLBlock:
47
+
48
+ - Replace conditionalType with blockBehavior property
49
+
50
+ Node traversal:
51
+
52
+ - Use blockBehavior?.type instead of conditionalType
53
+
54
+ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
55
+
56
+ ## [4.13.3](https://github.com/markuplint/markuplint/compare/@markuplint/ml-core@4.13.2...@markuplint/ml-core@4.13.3) (2026-02-10)
57
+
58
+ **Note:** Version bump only for package @markuplint/ml-core
59
+
60
+ ## [4.13.2](https://github.com/markuplint/markuplint/compare/@markuplint/ml-core@4.13.1...@markuplint/ml-core@4.13.2) (2025-11-05)
61
+
62
+ **Note:** Version bump only for package @markuplint/ml-core
13
63
 
14
64
  ## [4.13.1](https://github.com/markuplint/markuplint/compare/@markuplint/ml-core@4.13.0...@markuplint/ml-core@4.13.1) (2025-08-24)
15
65
 
package/README.md CHANGED
@@ -5,6 +5,11 @@
5
5
  ## Documentation
6
6
 
7
7
  - [API Document](https://markuplint.dev/api-docs)
8
+ - [Architecture](ARCHITECTURE.md) ([日本語](ARCHITECTURE.ja.md)) — Package overview, MLDOM hierarchy, rule system, and linting pipeline
9
+ - [MLDOM Reference](docs/ml-dom.md) ([日本語](docs/ml-dom.ja.md)) — Class hierarchy, node properties, tree traversal
10
+ - [Rule System](docs/rule-system.md) ([日本語](docs/rule-system.ja.md)) — MLRule, RuleSeed, MLRuleContext, configuration resolution
11
+ - [Linting Pipeline](docs/linting-pipeline.md) ([日本語](docs/linting-pipeline.ja.md)) — MLCore engine, verify flow, pretender, plugin system
12
+ - [Maintenance Guide](docs/maintenance.md) ([日本語](docs/maintenance.ja.md)) — Commands, recipes, and troubleshooting
8
13
 
9
14
  ## Install
10
15
 
package/SKILL.md ADDED
@@ -0,0 +1,61 @@
1
+ ---
2
+ description: Maintenance tasks for @markuplint/ml-core — the core linting engine with MLDOM, MLRule, and MLCore subsystems
3
+ globs:
4
+ - packages/@markuplint/ml-core/src/**/*.ts
5
+ - packages/@markuplint/ml-core/tsconfig*.json
6
+ - packages/@markuplint/ml-core/package.json
7
+ alwaysApply: false
8
+ ---
9
+
10
+ # @markuplint/ml-core Maintenance
11
+
12
+ You are maintaining `@markuplint/ml-core`, the core linting engine of markuplint.
13
+
14
+ ## Architecture
15
+
16
+ Read [ARCHITECTURE.md](ARCHITECTURE.md) for the full package overview, MLDOM class hierarchy, rule system, and linting pipeline.
17
+
18
+ ## Tasks
19
+
20
+ ### add-node-property
21
+
22
+ Add a property to an MLDOM node class.
23
+
24
+ 1. Identify the target class in `src/ml-dom/node/` (e.g., `element.ts`, `node.ts`, `document.ts`)
25
+ 2. Add the property as a getter or readonly field
26
+ 3. Use `this.#astNode` for AST-derived data, `this.ownerMLDocument.specs` for spec data
27
+ 4. Update type definitions in `src/ml-dom/node/types.ts` if needed
28
+ 5. Build: `yarn build --scope @markuplint/ml-core`
29
+ 6. Check downstream impact on `@markuplint/rules`
30
+
31
+ ### create-rule
32
+
33
+ Create a new linting rule using the `createRule` API.
34
+
35
+ 1. Rules are implemented in `@markuplint/rules`, not in this package
36
+ 2. Use `createRule()` from `src/ml-rule/create-rule.ts` for the rule seed
37
+ 3. Define `verify()` and optionally `fix()` in the `RuleSeed`
38
+ 4. Use `document.walkOn(type, walker)` to iterate target nodes
39
+ 5. Use `context.report()` to report violations
40
+ 6. Test with `createTestDocument()` and `createTestElement()` from `src/test/index.ts`
41
+
42
+ ### modify-rule-mapping
43
+
44
+ Change how rules are mapped to nodes via RuleMapper.
45
+
46
+ 1. Open `src/ml-dom/node/rule-mapper.ts`
47
+ 2. `apply()` iterates through global rules, nodeRules, and childNodeRules
48
+ 3. `MappingLayer` includes `from`, `specificity`, and the resolved `rule`
49
+ 4. `set()` assigns layers to nodes, resolving conflicts by CSS selector specificity
50
+ 5. Build and verify: `yarn build --scope @markuplint/ml-core`
51
+ 6. Test rule resolution with `createTestDocument` using nodeRules/childNodeRules config
52
+
53
+ ### update-pretender
54
+
55
+ Update the pretender system for component-to-HTML element mapping.
56
+
57
+ 1. Pretender initialization is in `MLDocument` constructor (`src/ml-dom/node/document.ts`)
58
+ 2. `PretenderContext` types are in `src/ml-dom/node/types.ts`
59
+ 3. Elements matching pretender selectors get `pretenderContext` with `type: 'pretender'`
60
+ 4. Accessibility name computation uses pretender context (`src/ml-dom/helper/accname.ts`)
61
+ 5. Test with `createTestDocument(source, { pretenders: [...] })`