@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
package/ARCHITECTURE.md DELETED
@@ -1,726 +0,0 @@
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
- │ ├── token/
36
- │ │ └── token.ts — MLToken (base positional token)
37
- │ ├── helper/
38
- │ │ ├── accname.ts — Accessible name computation
39
- │ │ ├── create-node.ts — AST → MLDOM node factory
40
- │ │ ├── walkers.ts — Tree traversal (sync/async walkers)
41
- │ │ ├── get-indent.ts — Indentation analysis
42
- │ │ └── debug.ts — Debug map generation
43
- │ └── manipulations/
44
- │ ├── child-node-methods.ts — ChildNode interface stubs
45
- │ └── get-children.ts — Element children extraction
46
- ├── virtual-rule.ts — Named nodeRule expansion (expandNamedNodeRules)
47
- ├── virtual-rule.spec.ts — Virtual rule unit tests
48
- ├── ml-rule/
49
- │ ├── ml-rule.ts — MLRule class (rule execution)
50
- │ ├── ml-rule-context.ts — MLRuleContext (report collection)
51
- │ ├── rule-fixer.ts — RuleFixer (TextEdit builder for fix callbacks)
52
- │ ├── create-rule.ts — createRule factory
53
- │ ├── create-test-rule.ts — Test rule factory
54
- │ └── types.ts — RuleSeed, Checker types
55
- ├── cursor-offset.ts — computeCursorOffset (cursor remapping after edits)
56
- ├── fix-applier.ts — applyFixes (overlap-aware TextEdit applicator)
57
- ├── ruleset/
58
- │ └── index.ts — Ruleset class (rules + nodeRules + childNodeRules)
59
- ├── plugin/
60
- │ ├── plugin.ts — createPlugin factory
61
- │ ├── types.ts — Plugin, PluginCreator types
62
- │ └── index.ts — Plugin exports
63
- ├── test/
64
- │ └── index.ts — createTestDocument, createTestElement, dummySchemas
65
- └── utils/
66
- ├── index.ts — Utility exports
67
- ├── get-location-from-chars.ts — Character location resolver
68
- └── string-splice.ts — String splice helper
69
- ```
70
-
71
- ## Architecture Diagram
72
-
73
- ```mermaid
74
- flowchart TD
75
- subgraph upstream ["Upstream Dependencies"]
76
- mlAst["@markuplint/ml-ast\n(AST types)"]
77
- mlConfig["@markuplint/ml-config\n(Config, RuleConfigValue)"]
78
- mlSpec["@markuplint/ml-spec\n(HTML/ARIA specs)"]
79
- htmlSpec["@markuplint/html-spec\n(Default spec data)"]
80
- htmlParser["@markuplint/html-parser\n(Default parser)"]
81
- parserUtils["@markuplint/parser-utils\n(ParserOptions)"]
82
- selector["@markuplint/selector\n(CSS selector matching)"]
83
- i18n["@markuplint/i18n\n(Locale, Translator)"]
84
- shared["@markuplint/shared\n(Utilities)"]
85
- configPresets["@markuplint/config-presets\n(Built-in presets)"]
86
- end
87
-
88
- subgraph pkg ["@markuplint/ml-core"]
89
- subgraph mldom ["MLDOM"]
90
- document["MLDocument"]
91
- element["MLElement"]
92
- node["MLNode / MLToken"]
93
- ruleMapper["RuleMapper"]
94
- end
95
-
96
- subgraph mlRule ["MLRule"]
97
- rule["MLRule"]
98
- ruleContext["MLRuleContext"]
99
- createRule["createRule()"]
100
- end
101
-
102
- subgraph engine ["Engine"]
103
- core["MLCore"]
104
- ruleset["Ruleset"]
105
- convertRuleset["convertRuleset()"]
106
- end
107
-
108
- subgraph extras ["Extras"]
109
- plugin["Plugin / createPlugin()"]
110
- testUtils["Test Utilities"]
111
- end
112
- end
113
-
114
- subgraph downstream ["Downstream"]
115
- rules["@markuplint/rules\n(Built-in rules)"]
116
- markuplint["markuplint\n(CLI & API)"]
117
- end
118
-
119
- upstream -->|"types, parsing, specs"| pkg
120
- core --> document
121
- core --> rule
122
- document --> ruleMapper
123
- rule --> ruleContext
124
- pkg -->|"MLDOM, MLRule, MLCore"| downstream
125
- ```
126
-
127
- ## Linting Pipeline
128
-
129
- The `MLCore.verify()` method orchestrates the full linting flow:
130
-
131
- ```mermaid
132
- flowchart LR
133
- A["MLCore\nconstructor"]
134
- B["_parse()\nParser → MLASTDocument"]
135
- C["_createDocument()\nMLASTDocument → MLDocument"]
136
- D["verify(fix? | options?)\nFor each rule:"]
137
- E["document.setRule(rule)\nRuleMapper maps config → nodes"]
138
- F["rule.verify(document)\nMLRuleContext collects reports"]
139
- G["Violations[]"]
140
-
141
- A --> B --> C --> D --> E --> F --> G
142
- ```
143
-
144
- ### Step-by-step
145
-
146
- 1. **Parse**: `MLCore` invokes the configured parser (`MLParser`) to produce an `MLASTDocument`
147
- 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
148
- 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`. Rules may attach inline `fix` callbacks to reports that return `TextEdit` objects
149
- 4. **Fix** (optional): When `fix=true`, fix callbacks on reports are executed via `RuleFixer` to produce `TextEdit[]`. `FixApplier.applyFixes(sourceCode, fixes)` applies all edits to the source text with overlap detection. When fixes require multiple passes, `_multiPassFix()` orchestrates re-parsing and re-verification, returning a `FixSummary` with pass count, applied/skipped totals, and first-pass edits for cursor offset computation
150
-
151
- ## MLDOM Class Hierarchy
152
-
153
- ```
154
- MLToken<A extends MLASTToken>
155
- └── MLNode<T, O, A extends MLASTNode>
156
- ├── MLAttr<T, O>
157
- ├── MLCharacterData<T, O, A> (abstract)
158
- │ ├── MLText<T, O>
159
- │ └── MLComment<T, O>
160
- ├── MLDocumentType<T, O>
161
- ├── MLBlock<T, O>
162
- ├── MLElementCloseTag<T, O>
163
- └── MLParentNode<T, O, A> (abstract)
164
- ├── MLElement<T, O>
165
- ├── MLDocumentFragment<T, O>
166
- └── MLDocument<T, O>
167
- ```
168
-
169
- ### Class Responsibilities
170
-
171
- | Class | DOM Interface | Key Responsibility |
172
- | -------------------- | ------------------ | ------------------------------------------------------------------------------------------- |
173
- | `MLToken` | — | Base token with position tracking (`startLine`, `endCol`, `raw`, `fixed`), `fix()` method |
174
- | `MLNode` | `Node` | Tree structure (`parentNode`, `childNodes`, `nextSibling`), rule storage, `is()` type guard |
175
- | `MLAttr` | `Attr` | Attribute name/value tokens, `isDynamicValue`, `isDirective`, `valueType`, `tokenList` |
176
- | `MLCharacterData` | `CharacterData` | Abstract base for text content nodes (`data`, `nodeValue`) |
177
- | `MLText` | `Text` | Text nodes, `isWhitespace()`, `isRawTextElementContent()` |
178
- | `MLComment` | `Comment` | Comment nodes with `textContent` |
179
- | `MLDocumentType` | `DocumentType` | `<!DOCTYPE>` with `name`, `publicId`, `systemId` |
180
- | `MLBlock` | — | Preprocessor-specific blocks (if/each/switch), `blockBehavior`, `isTransparent` |
181
- | `MLElementCloseTag` | — | Close tag paired with its open tag element |
182
- | `MLParentNode` | `ParentNode` | `querySelector()`, `querySelectorAll()`, `children`, `childElementCount` |
183
- | `MLElement` | `Element` | Attributes, selectors, namespaces, pretender context, `elementType`, `closeTag` |
184
- | `MLDocumentFragment` | `DocumentFragment` | Fragment root node |
185
- | `MLDocument` | `Document` | Root node, `nodeList`, `walkOn()`, `setRule()`, rule mapping, spec access |
186
-
187
- ## MLDocument
188
-
189
- `MLDocument` is the root of the MLDOM tree and the primary interface for rule execution.
190
-
191
- ### Construction
192
-
193
- The constructor receives an `MLASTDocument`, a `Ruleset`, and an `MLSchema` tuple. It:
194
-
195
- 1. Builds the flat `nodeList` by traversing the AST and calling `createNode()` for each AST node
196
- 2. Initializes `RuleMapper` to distribute rule configuration across nodes
197
- 3. Sets up pretender contexts when pretender definitions are provided
198
-
199
- ### Key Properties
200
-
201
- | Property | Type | Description |
202
- | ------------- | ----------------------- | --------------------------------------------------------- |
203
- | `nodeList` | `ReadonlyArray<MLNode>` | Flat list of all nodes in document order |
204
- | `specs` | `MLMLSpec` | HTML/ARIA specification data |
205
- | `isFragment` | `boolean` | Whether the document is a fragment |
206
- | `currentRule` | `MLRule \| null` | The rule currently being evaluated |
207
- | `endTag` | `EndTagType` | End tag handling mode (`'xml'`, `'omittable'`, `'never'`) |
208
-
209
- ### Key Methods
210
-
211
- | Method | Description |
212
- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
213
- | `walkOn(type, walker)` | Walks nodes of a given type (`'Element'`, `'Text'`, `'Comment'`, `'Attr'`, `'ElementCloseTag'`) |
214
- | `setRule(rule)` | Sets the current rule, used by `MLCore` during verification |
215
- | `searchNodeByLocation(line, col)` | Finds the node at a given source position |
216
- | `getAccessibilityProp(node)` | Computes ARIA accessibility properties (delegates to `MLElement.getAccessibleName()` for cached accessible name) |
217
- | `toString()` | Returns the raw source code of the document |
218
-
219
- ## MLElement
220
-
221
- `MLElement` represents an HTML/SVG/MathML element with full attribute access and selector matching.
222
-
223
- ### Key Properties
224
-
225
- | Property | Type | Description |
226
- | ------------------ | --------------------------- | -------------------------------------------- |
227
- | `localName` | `string` | Lowercase tag name (for HTML) |
228
- | `namespaceURI` | `NamespaceURI` | Element namespace (HTML, SVG, MathML) |
229
- | `attributes` | `MLNamedNodeMap` | Named attribute collection |
230
- | `elementType` | `ElementType` | `'html'`, `'web-component'`, or `'authored'` |
231
- | `closeTag` | `MLElementCloseTag \| null` | Paired close tag |
232
- | `pretenderContext` | `PretenderContext \| null` | Pretender mapping context |
233
- | `isForeignElement` | `boolean` | `true` for SVG/MathML elements |
234
- | `isOmitted` | `boolean` | `true` for implicitly inserted elements |
235
-
236
- ### Key Methods
237
-
238
- | Method | Description |
239
- | ---------------------------- | ---------------------------------------------------------------- |
240
- | `getAttribute(name)` | Returns attribute value or `null` |
241
- | `getAttributeToken(name)` | Returns `MLAttr[]` for the named attribute |
242
- | `hasAttribute(name)` | Checks attribute existence |
243
- | `getAccessibleName(version)` | Cached accessible name computation (memoized per ARIA version) |
244
- | `matches(selector)` | CSS selector matching |
245
- | `matchMLSelector(selector)` | Extended markuplint selector matching (supports `RegexSelector`) |
246
- | `querySelector(selector)` | Finds first matching descendant |
247
- | `querySelectorAll(selector)` | Finds all matching descendants |
248
-
249
- ## Rule System
250
-
251
- ### MLRule
252
-
253
- `MLRule<T, O>` encapsulates a linting rule with verification and optional fix logic.
254
-
255
- | Property/Method | Description |
256
- | --------------------------------- | --------------------------------------------------------------------------- |
257
- | `name` | Rule identifier (e.g., `"attr-duplication"`) |
258
- | `defaultSeverity` | Default severity level |
259
- | `defaultValue` / `defaultOptions` | Default configuration |
260
- | `baseRuleId` | For virtual rules: the base rule's name (e.g., `"required-attr"`) |
261
- | `groupName` | For multi-entry virtual rules: group name for batch disable |
262
- | `specConformance` | For virtual rules: `'normative'` or `'non-normative'` (from named nodeRule) |
263
- | `verify(document, locale, fix)` | Executes the rule and returns violations |
264
- | `createAlias(name, options?)` | Creates a virtual rule that reuses this rule's verify/fix logic |
265
- | `optimizeOption(settings)` | Normalizes raw rule configuration into `RuleInfo` |
266
-
267
- ### RuleSeed
268
-
269
- The `RuleSeed<T, O>` type defines the rule implementation:
270
-
271
- ```typescript
272
- type RuleSeed<T, O> = {
273
- meta?: {
274
- category?: 'validation' | 'style' | 'naming-convention' | 'a11y' | 'maintainability';
275
- };
276
- defaultSeverity?: Severity;
277
- defaultValue?: T;
278
- defaultOptions?: O;
279
- verify(context): void | Promise<void>;
280
- fix?(context): void | Promise<void>;
281
- };
282
- ```
283
-
284
- ### createRule
285
-
286
- `createRule(seed)` is a factory function for type-safe rule seed creation. It returns the seed as-is, serving primarily as a type helper.
287
-
288
- ### MLRuleContext
289
-
290
- `MLRuleContext<T, O>` provides the execution context for rules:
291
-
292
- - `document` — The current `MLDocument`
293
- - `translate` / `t` — Locale-aware message translator
294
- - `report(report)` — Reports a violation with node, message, and optional fix
295
-
296
- The `provide()` method returns the context object passed to `RuleSeed.verify()`. Auto-fix logic is provided as an inline `fix` callback on individual `report()` calls, not as a separate lifecycle method.
297
-
298
- ### Rule Configuration Resolution
299
-
300
- Rules are configured at three levels, resolved by `RuleMapper`:
301
-
302
- 1. **Global rules** (`rules`) — Apply to all nodes; lowest priority
303
- 2. **Node rules** (`nodeRules`) — Apply to nodes matching a selector; medium priority
304
- 3. **Child node rules** (`childNodeRules`) — Apply to children of nodes matching a selector; highest priority
305
-
306
- 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`.
307
-
308
- ### Rule Execution Flow
309
-
310
- ```mermaid
311
- flowchart TD
312
- A["MLCore.verify()"] --> B["For each MLRule"]
313
- B --> C["document.setRule(rule)"]
314
- C --> D["rule.verify(document, locale, fix)"]
315
- D --> E["rule.getRuleInfo(ruleset)\nResolve global config"]
316
- E --> F["document.walkOn(type, walker)\nIterate matching nodes"]
317
- F --> G["context.report()\nCollect violations per node"]
318
- G --> H["Return Violation[]"]
319
- ```
320
-
321
- ### Virtual Rule System
322
-
323
- Source: `src/virtual-rule.ts`
324
-
325
- > **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.
326
-
327
- 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`.
328
-
329
- #### Named NodeRule Expansion
330
-
331
- `expandNamedNodeRules()` converts named nodeRules (and childNodeRules) into virtual rules during `MLCore` construction:
332
-
333
- ```
334
- Named nodeRule (config) Virtual MLRule (runtime)
335
- ┌─────────────────────────┐ ┌──────────────────────────┐
336
- │ name: "a11y/html-lang" │ │ name: "a11y/html-lang" │
337
- │ specConformance: "norm."│ ──────► │ baseRuleId: "required-attr" │
338
- │ selector: ":where(html)"│ │ specConformance: metadata│
339
- │ rules: │ │ verify/fix: from base │
340
- │ required-attr: [lang] │ └──────────────────────────┘
341
- └─────────────────────────┘
342
- ```
343
-
344
- Key behaviors:
345
-
346
- - **False entry separation**: `false` entries in `rules` are automatically separated into unnamed nodeRules, preserving their semantics as base-rule specificity overrides
347
- - **Multi-entry support**: Named nodeRules with 2+ non-false entries create derived names (`name/baseRuleName`) with a `groupName` for group disable
348
- - **Metadata**: `specConformance` is attached to the virtual rule as metadata for downstream tools and reporting
349
- - **Hot-reload**: Pre-expansion nodeRules are preserved in `#originalNodeRules` / `#originalChildNodeRules` so `update()` can re-expand them
350
-
351
- #### Why `specConformance` Is Restricted to Named NodeRules
352
-
353
- `specConformance` is intentionally available **only on named nodeRules** (in presets), not on regular built-in rules. The design rationale:
354
-
355
- 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.
356
-
357
- 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.
358
-
359
- 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.
360
-
361
- 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.
362
-
363
- #### Virtual Rule Disable
364
-
365
- Virtual rules can be disabled at three levels in the `rules` config:
366
-
367
- 1. **Exact name**: `rules["a11y/html-lang"]: false`
368
- 2. **Group disable**: `rules["custom/multi"]: false` (for multi-entry named nodeRules)
369
- 3. **Namespace wildcard**: `rules["a11y/*"]: false` (disables all virtual rules starting with `a11y/`)
370
-
371
- ## Autofix System
372
-
373
- The autofix system allows rules to provide automatic fixes for violations. It operates through three components: **RuleFixer** (TextEdit builder), **fix callbacks** (rule-authored logic), and **FixApplier** (edit application engine).
374
-
375
- ### Autofix Data Flow
376
-
377
- ```mermaid
378
- flowchart LR
379
- subgraph RulePhase ["Rule Phase"]
380
- report["context.report({\n message,\n scope,\n fix: callback\n})"]
381
- end
382
-
383
- subgraph FixPhase ["Fix Callback Execution"]
384
- callback["fix(fixer) → TextEdit[]"]
385
- fixer["RuleFixer\n(shared instance)"]
386
- callback --> fixer
387
- end
388
-
389
- subgraph ApplyPhase ["Apply Phase"]
390
- fixdata["FixData\n{ edits: TextEdit[] }"]
391
- applier["applyFixes(\n sourceCode,\n allFixes\n)"]
392
- output["fixedCode"]
393
- fixdata --> applier --> output
394
- end
395
-
396
- report --> callback
397
- fixer --> fixdata
398
- ```
399
-
400
- ### How Fix Callbacks Work
401
-
402
- Rules attach an optional `fix` callback to each `report()` call. The callback is **not** executed during rule verification — it is stored and only invoked when `fix=true` is passed to `MLCore.verify()`.
403
-
404
- ```mermaid
405
- sequenceDiagram
406
- participant Rule as Rule (verify)
407
- participant Ctx as MLRuleContext
408
- participant MLR as MLRule.verify()
409
- participant Fixer as RuleFixer
410
- participant Core as MLCore.verify()
411
- participant FA as applyFixes()
412
-
413
- Rule->>Ctx: report({ scope, message, fix })
414
- Note over Ctx: Stores report with fix callback
415
-
416
- MLR->>Ctx: context.reports
417
- loop Each report with fix callback
418
- MLR->>Fixer: report.fix(sharedFixer)
419
- Fixer-->>MLR: TextEdit | TextEdit[]
420
- MLR->>MLR: Wrap as FixData { edits }
421
- end
422
- MLR-->>Core: Violation[] (with FixData)
423
-
424
- Core->>Core: Collect all FixData from violations
425
- Core->>FA: applyFixes(sourceCode, allFixes)
426
- FA-->>Core: FixResult { output, applied, skipped, appliedEdits }
427
- ```
428
-
429
- ### RuleFixer API
430
-
431
- `RuleFixer` implements `IRuleFixer` (defined in `@markuplint/ml-config`). It is a **stateless** helper — a single instance is shared across all rules. Each method builds a `TextEdit` object describing a range replacement on the source code.
432
-
433
- | Method | Input | TextEdit Produced |
434
- | --------------------------- | -------------------------------- | ------------------------------------------ |
435
- | `replaceText(token, text)` | Token with `startOffset` + `raw` | `range: [start, start+len], text` |
436
- | `replaceRange(range, text)` | Explicit `[start, end)` range | `range: [start, end], text` |
437
- | `insertBefore(token, text)` | Token with `startOffset` | `range: [start, start], text` (zero-width) |
438
- | `insertAfter(token, text)` | Token with `startOffset` + `raw` | `range: [end, end], text` (zero-width) |
439
- | `remove(token)` | Token with `startOffset` + `raw` | `range: [start, start+len], text: ""` |
440
- | `removeRange(range)` | Explicit `[start, end)` range | `range: [start, end], text: ""` |
441
-
442
- The `token` parameter accepts any object satisfying the `FixToken` type (defined in `@markuplint/ml-config`) — i.e., `{ startOffset: number; raw: string }`. MLDOM tokens (`MLToken`, `MLAttr`, etc.) satisfy this naturally.
443
-
444
- ### FixApplier Algorithm
445
-
446
- `applyFixes()` (in `fix-applier.ts`) merges all `FixData` from all rules and applies them in a single pass:
447
-
448
- ```mermaid
449
- flowchart TD
450
- A["Flatten: FixData[] → tagged TextEdit[]"]
451
- B["Sort: by range start ascending,\nthen range end descending"]
452
- C["Apply sequentially:\nfor each edit, check overlap"]
453
- D{{"edit.start < lastAppliedEnd?"}}
454
- E["Skip edit\n(mark parent FixData as skipped)"]
455
- F["Apply edit\n(splice into output)"]
456
- G["Classify: each FixData as\napplied or skipped"]
457
- H["Return FixResult\n{ output, applied, skipped, appliedEdits }"]
458
-
459
- A --> B --> C --> D
460
- D -- Yes --> E --> C
461
- D -- No --> F --> C
462
- C -. "all edits processed" .-> G --> H
463
- ```
464
-
465
- Key constraints:
466
-
467
- - Edits within a single `FixData` must not overlap each other
468
- - Inter-`FixData` overlap is handled by the skip mechanism
469
- - If any edit in a `FixData` is skipped, the entire `FixData` is classified as skipped
470
- - `appliedEdits` is a flat list of all successfully applied `TextEdit` objects, sorted by `range[0]` ascending — used for cursor offset computation
471
-
472
- ### Multi-Pass Fix Loop
473
-
474
- When `applyFixes()` skips some fixes due to range overlap, the engine enters a multi-pass loop (`_multiPassFix()`) that re-parses and re-verifies until all fixable violations are resolved:
475
-
476
- ```mermaid
477
- flowchart TD
478
- A["Extract fixes from violations"] --> B["applyFixes(code, fixes)"]
479
- B --> C{"applied.length === 0?"}
480
- C -- Yes --> Z["Return current code"]
481
- C -- No --> D{"output === currentCode?"}
482
- D -- Yes --> Z
483
- D -- No --> E{"Cycle detected?\n(output === code from 2 passes ago)"}
484
- E -- Yes --> Z
485
- E -- No --> F{"skipped.length === 0?"}
486
- F -- Yes --> Z["Return fixed code\n(all fixes applied)"]
487
- F -- No --> G["Re-parse + re-verify"]
488
- G --> H{"ParserError?"}
489
- H -- Yes --> Z["Revert to last good code"]
490
- H -- No --> I{"New fixable violations?"}
491
- I -- No --> Z
492
- I -- Yes --> B
493
- ```
494
-
495
- Key design points:
496
-
497
- - **Zero-cost path**: If no violations have fixes, the multi-pass loop is skipped entirely
498
- - **Single-pass fast path**: When `skipped.length === 0`, the loop exits immediately — equivalent to Phase 1 behavior
499
- - **Cycle detection**: Compares current output against the output from two passes ago to detect A→B→A oscillation
500
- - **Safety cap**: Maximum 10 passes (same as ESLint's `SourceCodeFixer`)
501
- - **State restoration**: `verify()` saves and restores `#sourceCode`, `#ast`, and `#document` via `try/finally`
502
-
503
- **Important**: The `violations` array in `VerifyResult` reflects the first pass only, while `fixedCode` may be the result of multiple passes. Callers needing an accurate violation list for the fixed code should re-verify the output.
504
-
505
- ### VerifyResult and FixSummary
506
-
507
- `MLCore.verify()` accepts either a `boolean` or a `VerifyOptions` object:
508
-
509
- ```typescript
510
- verify(fix?: boolean): Promise<VerifyResult>;
511
- verify(options?: VerifyOptions): Promise<VerifyResult>;
512
- ```
513
-
514
- `VerifyResult` contains:
515
-
516
- | Field | Type | Description |
517
- | ------------ | ------------------------- | ------------------------------------------------------------- |
518
- | `violations` | `readonly Violation[]` | Violations from the first verification pass |
519
- | `fixedCode` | `string \| undefined` | Source after all fix passes; `undefined` when fix is disabled |
520
- | `fixSummary` | `FixSummary \| undefined` | Fix process summary; present when `fix=true` |
521
-
522
- `FixSummary` provides diagnostics about the multi-pass fix process:
523
-
524
- | Field | Type | Description |
525
- | ------------------ | --------------------- | ---------------------------------------------------------------- |
526
- | `passCount` | `number` | Number of fix passes executed |
527
- | `totalApplied` | `number` | Total fixes applied across all passes |
528
- | `totalSkipped` | `number` | Total fixes skipped (overlap) across all passes |
529
- | `reachedMaxPasses` | `boolean` | Whether the 10-pass safety cap was reached |
530
- | `firstPassEdits` | `readonly TextEdit[]` | Applied edits from the first pass only (original source offsets) |
531
-
532
- `firstPassEdits` references the original source code offsets, making them suitable for cursor remapping via `computeCursorOffset()`.
533
-
534
- ### Cursor Offset Computation
535
-
536
- `computeCursorOffset()` (in `cursor-offset.ts`) maps a cursor position from the original source to the fixed source using the first-pass applied edits:
537
-
538
- ```typescript
539
- import { computeCursorOffset } from '@markuplint/ml-core';
540
-
541
- const newOffset = computeCursorOffset(fixSummary.firstPassEdits, originalCursorOffset);
542
- ```
543
-
544
- Algorithm:
545
-
546
- 1. Walk through edits sorted by `range[0]` ascending
547
- 2. For each edit before the cursor: accumulate `delta = text.length - (end - start)`
548
- 3. For edits after the cursor: stop (no effect)
549
- 4. If the cursor falls inside a replaced range `[start, end)`: place at `start + text.length`
550
-
551
- Ranges use half-open intervals: a cursor at position `end` is considered **outside** the edit.
552
-
553
- ### Example: Rule Fix in Practice
554
-
555
- ```typescript
556
- // In a rule's verify function:
557
- context.report({
558
- scope: node,
559
- message: 'Attribute value must use double quotes',
560
- fix: fixer => fixer.replaceText(node.attrValueToken, `"${value}"`),
561
- });
562
- ```
563
-
564
- This produces:
565
-
566
- 1. **Report** → stored in `MLRuleContext`
567
- 2. **Fix callback** → `(fixer) => fixer.replaceText(token, text)` (not yet called)
568
- 3. **When `fix=true`** → callback invoked with shared `RuleFixer` → returns `TextEdit`
569
- 4. **TextEdit** → wrapped as `FixData { edits: [{ range: [12, 17], text: '"hello"' }] }`
570
- 5. **applyFixes** → splices the replacement into source code
571
-
572
- ## Pretender System
573
-
574
- 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>`).
575
-
576
- ### Configuration
577
-
578
- Pretenders are defined in the markuplint config as an array of `Pretender` objects:
579
-
580
- ```typescript
581
- type Pretender = {
582
- selector: string; // CSS selector matching the component
583
- as: string; // HTML element to pretend as
584
- aria?: PretenderARIA; // Optional ARIA overrides
585
- };
586
- ```
587
-
588
- ### How It Works
589
-
590
- 1. During `MLDocument` construction, pretender definitions are processed
591
- 2. Each `MLElement` matching a pretender selector gets a `pretenderContext` with `type: 'pretender'`
592
- 3. The target HTML element gets a `pretenderContext` with `type: 'origin'`
593
- 4. Rules can access `element.pretenderContext` to check the semantic mapping
594
- 5. Accessibility computations use pretender context for role/name resolution
595
-
596
- ## Conditional Child Nodes
597
-
598
- Template engines (Pug, EJS, Nunjucks, etc.) produce preprocessor-specific blocks represented by `MLBlock` nodes. These blocks can wrap child nodes conditionally:
599
-
600
- | `blockBehavior.type` | Template Construct | Description |
601
- | -------------------- | ------------------ | -------------------------- |
602
- | `'if'` | `{% if %}` | Start of conditional block |
603
- | `'if:else'` | `{% else %}` | Alternative branch |
604
- | `'end'` | `{% endif %}` | End of conditional block |
605
- | `'each'` | `{% for %}` | Start of loop |
606
- | `'end'` | `{% endfor %}` | End of loop |
607
- | `'switch:case'` | `{% switch %}` | Start of switch |
608
- | `'switch:default'` | `{% case %}` | Switch case |
609
- | `'end'` | `{% endswitch %}` | End of switch |
610
-
611
- `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.
612
-
613
- ## Plugin System
614
-
615
- Plugins extend markuplint with custom rules and shared configurations.
616
-
617
- ### Plugin Type
618
-
619
- ```typescript
620
- type Plugin = {
621
- readonly name: string;
622
- readonly rules?: Record<string, RuleSeed<any, any>>;
623
- readonly configs?: Record<string, Config>;
624
- };
625
- ```
626
-
627
- ### PluginCreator
628
-
629
- For plugins that accept settings:
630
-
631
- ```typescript
632
- type PluginCreator<S> = {
633
- readonly name: string;
634
- create(setting: S): Omit<Plugin, 'name'>;
635
- };
636
- ```
637
-
638
- `createPlugin(creator)` is a factory function for type-safe plugin creator definitions.
639
-
640
- ## Test Utilities
641
-
642
- The `test/` module provides helpers for rule testing:
643
-
644
- | Function | Description |
645
- | ------------------------------------------- | ----------------------------------------------- |
646
- | `createTestDocument(sourceCode, options?)` | Parses source into an `MLDocument` for testing |
647
- | `createTestElement(sourceCode, options?)` | Parses source and returns the first `MLElement` |
648
- | `createTestNodeList(sourceCode, options?)` | Returns the flat node list from parsed source |
649
- | `createTestTokenList(sourceCode, options?)` | Returns the flat token list from parsed source |
650
- | `dummySchemas()` | Returns the default HTML spec as a schema tuple |
651
-
652
- `CreateTestOptions` accepts `config`, `parser`, `specs`, and `pretenders` overrides.
653
-
654
- ## External Dependencies
655
-
656
- | Dependency | Purpose |
657
- | ---------------------------- | ----------------------------------------------------------------- |
658
- | `@markuplint/ml-ast` | AST type definitions (`MLASTDocument`, `MLASTNode`, etc.) |
659
- | `@markuplint/ml-config` | Configuration types (`Config`, `RuleConfigValue`, `Pretender`) |
660
- | `@markuplint/ml-spec` | HTML/ARIA specification access (`MLMLSpec`, role/attribute specs) |
661
- | `@markuplint/html-spec` | Default HTML specification data |
662
- | `@markuplint/html-parser` | Default HTML parser (used in test utilities) |
663
- | `@markuplint/parser-utils` | Parser options and types |
664
- | `@markuplint/selector` | CSS and extended selector matching |
665
- | `@markuplint/i18n` | Internationalization (`LocaleSet`, `Translator`) |
666
- | `@markuplint/shared` | Shared utilities |
667
- | `@markuplint/config-presets` | Built-in configuration presets |
668
- | `debug` | Debug logging |
669
- | `is-plain-object` | Plain object type checking |
670
- | `type-fest` | TypeScript utility types |
671
-
672
- ## Integration Points
673
-
674
- ```mermaid
675
- flowchart TD
676
- subgraph upstream ["Upstream"]
677
- mlAst["@markuplint/ml-ast"]
678
- mlConfig["@markuplint/ml-config"]
679
- mlSpec["@markuplint/ml-spec"]
680
- htmlSpec["@markuplint/html-spec"]
681
- htmlParser["@markuplint/html-parser"]
682
- parserUtils["@markuplint/parser-utils"]
683
- selector["@markuplint/selector"]
684
- i18n["@markuplint/i18n"]
685
- shared["@markuplint/shared"]
686
- configPresets["@markuplint/config-presets"]
687
- end
688
-
689
- subgraph pkg ["@markuplint/ml-core"]
690
- core["MLCore Engine"]
691
- end
692
-
693
- subgraph downstream ["Downstream"]
694
- rules["@markuplint/rules\n(Built-in rule implementations)"]
695
- markuplint["markuplint\n(CLI, API, MLEngine)"]
696
- end
697
-
698
- upstream -->|"types, parsing, specs, i18n"| core
699
- core -->|"MLDOM classes, MLRule,\ncreateRule, test utils"| rules
700
- core -->|"MLCore, ViolationCollector,\nconvertRuleset, Plugin types"| markuplint
701
- ```
702
-
703
- ### Upstream
704
-
705
- - **`@markuplint/ml-ast`** — AST types used to construct the MLDOM tree
706
- - **`@markuplint/ml-config`** — Config and rule configuration types
707
- - **`@markuplint/ml-spec`** — HTML/ARIA specification for element validation, role computation
708
- - **`@markuplint/html-spec`** — Default spec data bundle
709
- - **`@markuplint/html-parser`** — Default parser used in test utilities
710
- - **`@markuplint/parser-utils`** — Parser option types
711
- - **`@markuplint/selector`** — CSS selector engine for `querySelector`, `matches`, and `RegexSelector`
712
- - **`@markuplint/i18n`** — Locale sets and translation for rule messages
713
- - **`@markuplint/shared`** — Shared utility functions
714
- - **`@markuplint/config-presets`** — Built-in configuration presets
715
-
716
- ### Downstream
717
-
718
- - **`@markuplint/rules`** — Imports MLDOM classes, `createRule`, `MLRuleContext`, and test utilities to implement built-in rules
719
- - **`markuplint`** — Imports `MLCore`, `ViolationCollector`, `convertRuleset`, and plugin types to provide the CLI and API
720
-
721
- ## Documentation Map
722
-
723
- - [MLDOM Reference](docs/ml-dom.md) ([日本語](docs/ml-dom.ja.md)) — Class hierarchy, node properties, tree traversal
724
- - [Rule System](docs/rule-system.md) ([日本語](docs/rule-system.ja.md)) — MLRule, RuleSeed, MLRuleContext, configuration resolution
725
- - [Linting Pipeline](docs/linting-pipeline.md) ([日本語](docs/linting-pipeline.ja.md)) — MLCore engine, verify flow, pretender, plugin system
726
- - [Maintenance Guide](docs/maintenance.md) ([日本語](docs/maintenance.ja.md)) — Commands, recipes, and troubleshooting