@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,371 @@
1
+ # Rule Mapping — How Rules Are Applied to Nodes
2
+
3
+ **Source:** `src/ml-dom/node/document.ts` (`_ruleMapping()`), `src/ml-dom/node/rule-mapper.ts` (`RuleMapper`)
4
+
5
+ ## Overview
6
+
7
+ Rule mapping is the process of distributing rule configurations from the user's config to individual MLDOM nodes. Each node has a `rules` property (`Record<string, AnyRule>`) that stores the resolved rule configuration for every rule that applies to it.
8
+
9
+ The mapping uses three configuration layers — `rules`, `nodeRules`, and `childNodeRules` — processed in a defined order, with CSS selector specificity used to resolve conflicts.
10
+
11
+ For configuration syntax, see the [markuplint configuration documentation](https://markuplint.dev/docs/configuration).
12
+
13
+ ## Architecture
14
+
15
+ ### When Rule Mapping Happens
16
+
17
+ Rule mapping happens **once** during `MLDocument` construction, after pretender initialization and before any rule execution:
18
+
19
+ ```
20
+ MLDocument constructor
21
+ ├── 1. Parse AST → create MLDOM nodes (nodeList)
22
+ ├── 2. _pretending(pretenders)
23
+ ├── 3. _ruleMapping(ruleset) ← Rule mapping
24
+ └── 4. (ready for rule verification)
25
+ ```
26
+
27
+ This ordering matters:
28
+
29
+ - Pretenders must be established first, so that selector matching in rule mapping can match against pretender identities (e.g., a `nodeRules` entry targeting `button` should match `<MyButton>` pretending to be `<button>`)
30
+ - Rule mapping must complete before verification, so that rules can read `node.rules` during `walkOn()`
31
+
32
+ ### Components
33
+
34
+ | Component | Source | Role |
35
+ | ---------------- | -------------------------------- | ------------------------------------------------------------------------- |
36
+ | `Ruleset` | `src/ruleset/index.ts` | Extracts `rules`, `nodeRules`, `childNodeRules` from user config |
37
+ | `RuleMapper` | `src/ml-dom/node/rule-mapper.ts` | Accumulates rule-to-node mappings with specificity, applies them to nodes |
38
+ | `_ruleMapping()` | `src/ml-dom/node/document.ts` | Orchestrates the three-layer processing |
39
+
40
+ ## The Three Layers
41
+
42
+ ### Layer 1: Global Rules (`rules`)
43
+
44
+ ```json
45
+ {
46
+ "rules": {
47
+ "attr-duplication": true,
48
+ "case-sensitive-tag-name": "warning"
49
+ }
50
+ }
51
+ ```
52
+
53
+ Global rules are applied to **every node** in the document, including the `#document` node itself. They have a fixed specificity of `[0, 0, 0]`.
54
+
55
+ **Processing:**
56
+
57
+ ```typescript
58
+ // Apply to #document
59
+ for (const ruleName of Object.keys(ruleset.rules)) {
60
+ ruleMapper.set(document, ruleName, {
61
+ from: 'rules',
62
+ specificity: [0, 0, 0],
63
+ rule,
64
+ });
65
+ }
66
+
67
+ // Apply to every node in nodeList
68
+ for (const node of document.nodeList) {
69
+ for (const ruleName of Object.keys(ruleset.rules)) {
70
+ ruleMapper.set(node, ruleName, {
71
+ from: 'rules',
72
+ specificity: [0, 0, 0],
73
+ rule,
74
+ });
75
+ }
76
+ }
77
+ ```
78
+
79
+ ### Layer 2: Node Rules (`nodeRules`)
80
+
81
+ ```json
82
+ {
83
+ "nodeRules": [
84
+ {
85
+ "selector": "img",
86
+ "rules": {
87
+ "required-attr": { "value": "alt" }
88
+ }
89
+ }
90
+ ]
91
+ }
92
+ ```
93
+
94
+ Node rules override global rules for **elements that match the selector**. Only `ELEMENT_NODE` and `TEXT_NODE` are considered; text nodes cannot be selector targets, so only elements are actually matched.
95
+
96
+ **Processing:**
97
+
98
+ 1. For each `nodeRule` entry, check if the current element matches via `matchMLSelector(selector)`
99
+ 2. If matched, for each rule in the entry:
100
+ - `exchangeValueOnRule(rule, matches.data)` — render Mustache template variables from regex selector captures
101
+ - `mergeRule(globalRule, convertedRule)` — merge with the global rule config (see [Merging](#merging-with-global-rules))
102
+ - `ruleMapper.set(node, ruleName, { from: 'nodeRules', specificity: matches.specificity, rule: mergedRule })`
103
+
104
+ The specificity comes from the CSS selector used to match the element.
105
+
106
+ ### Layer 3: Child Node Rules (`childNodeRules`)
107
+
108
+ ```json
109
+ {
110
+ "childNodeRules": [
111
+ {
112
+ "selector": "table",
113
+ "inheritance": true,
114
+ "rules": {
115
+ "class-naming": "/^table-/"
116
+ }
117
+ }
118
+ ]
119
+ }
120
+ ```
121
+
122
+ Child node rules apply rules to **children (or descendants)** of matched elements. The selector matches the **parent**, and the rules are distributed to its children.
123
+
124
+ **Processing:**
125
+
126
+ 1. For each `childNodeRule` entry, check if the current element matches via `matchMLSelector(selector)`
127
+ 2. If matched, determine the target nodes:
128
+ - `inheritance: true` → all **descendants** (collected via `syncWalk`)
129
+ - `inheritance: false` (default) → only **direct children** (`childNodes`)
130
+ 3. For each rule in the entry, apply the merged rule to every target node
131
+
132
+ ```typescript
133
+ const targetDescendants = nodeRule.inheritance ? descendants : children;
134
+
135
+ for (const descendant of targetDescendants) {
136
+ ruleMapper.set(descendant, ruleName, {
137
+ from: 'childNodeRules',
138
+ specificity: matches.specificity,
139
+ rule: mergedRule,
140
+ });
141
+ }
142
+ ```
143
+
144
+ Note: the specificity is that of the **parent's** selector match, not the child's.
145
+
146
+ ## Processing Order
147
+
148
+ The three layers are processed in a specific order within `_ruleMapping()`:
149
+
150
+ ```
151
+ For the #document node:
152
+ 1. Apply all global rules (specificity [0,0,0])
153
+
154
+ For each node in nodeList:
155
+ 2. Apply all global rules (specificity [0,0,0])
156
+ 3. Apply matching nodeRules (selector specificity)
157
+ 4. Apply matching childNodeRules (selector specificity of parent)
158
+ ```
159
+
160
+ Because `RuleMapper.set()` resolves conflicts by specificity, the processing order within the same specificity level matters:
161
+
162
+ - **Same specificity**: later `set()` calls overwrite earlier ones (last-write-wins)
163
+ - **Higher specificity**: always wins regardless of order
164
+ - **Lower specificity**: silently skipped
165
+
166
+ This means:
167
+
168
+ | Scenario | Winner |
169
+ | ------------------------------------------------------------------------ | ---------------------------------------------------------- |
170
+ | `rules` vs `nodeRules` (any selector) | `nodeRules` (selector specificity `≥ [0,0,1]` > `[0,0,0]`) |
171
+ | `rules` vs `childNodeRules` (any selector) | `childNodeRules` (same reasoning) |
172
+ | `nodeRules[0]` vs `nodeRules[1]` (same specificity) | `nodeRules[1]` (later in array) |
173
+ | `nodeRules` (lower specificity) vs `childNodeRules` (higher specificity) | `childNodeRules` (higher specificity wins) |
174
+ | `nodeRules` (higher specificity) vs `childNodeRules` (lower specificity) | `nodeRules` (higher specificity wins) |
175
+
176
+ ## Specificity
177
+
178
+ ### What is Specificity?
179
+
180
+ Specificity is a three-element tuple `[a, b, c]` based on the CSS Selectors specification:
181
+
182
+ | Component | Counts | Examples |
183
+ | --------- | ---------------------------------------------------- | --------------------------------------------------- |
184
+ | `a` | ID selectors | `#main` → `[1, 0, 0]` |
185
+ | `b` | Class selectors, attribute selectors, pseudo-classes | `.foo` → `[0, 1, 0]`, `[type="text"]` → `[0, 1, 0]` |
186
+ | `c` | Type selectors, pseudo-elements | `div` → `[0, 0, 1]`, `img` → `[0, 0, 1]` |
187
+
188
+ ### How Specificity is Compared
189
+
190
+ `compareSpecificity(a, b)` from `@markuplint/selector` performs a lexicographic comparison:
191
+
192
+ ```typescript
193
+ function compareSpecificity(a: Specificity, b: Specificity): -1 | 0 | 1 {
194
+ // Compare a[0] vs b[0], then a[1] vs b[1], then a[2] vs b[2]
195
+ // Returns: -1 (a < b), 0 (equal), 1 (a > b)
196
+ }
197
+ ```
198
+
199
+ ### How `RuleMapper.set()` Uses Specificity
200
+
201
+ ```typescript
202
+ set(node, ruleName, rule: MappingLayer) {
203
+ const currentRule = rules[ruleName];
204
+ if (currentRule) {
205
+ const order = compareSpecificity(currentRule.specificity, rule.specificity);
206
+ if (order === 1) {
207
+ return; // Current has higher specificity → skip new rule
208
+ }
209
+ // order === 0 or -1 → overwrite with new rule
210
+ }
211
+ rules[ruleName] = rule;
212
+ }
213
+ ```
214
+
215
+ - `order === 1` (current > new): **skip** — existing higher-specificity rule is preserved
216
+ - `order === 0` (equal): **overwrite** — later mapping wins
217
+ - `order === -1` (current < new): **overwrite** — higher-specificity rule wins
218
+
219
+ ### Specificity Examples
220
+
221
+ ```json
222
+ {
223
+ "rules": {
224
+ "class-naming": "/^prefix-/"
225
+ },
226
+ "nodeRules": [
227
+ {
228
+ "selector": "div",
229
+ "rules": { "class-naming": "/^div-/" }
230
+ },
231
+ {
232
+ "selector": "div.special",
233
+ "rules": { "class-naming": "/^special-/" }
234
+ },
235
+ {
236
+ "selector": "#main",
237
+ "rules": { "class-naming": "/^main-/" }
238
+ }
239
+ ]
240
+ }
241
+ ```
242
+
243
+ For `<div id="main" class="special">`:
244
+
245
+ | Source | Selector | Specificity | `class-naming` value |
246
+ | -------------- | ------------- | ----------- | -------------------- |
247
+ | `rules` | (global) | `[0, 0, 0]` | `/^prefix-/` |
248
+ | `nodeRules[0]` | `div` | `[0, 0, 1]` | `/^div-/` |
249
+ | `nodeRules[1]` | `div.special` | `[0, 1, 1]` | `/^special-/` |
250
+ | `nodeRules[2]` | `#main` | `[1, 0, 0]` | `/^main-/` |
251
+
252
+ Processing order: `rules` → `nodeRules[0]` → `nodeRules[1]` → `nodeRules[2]`
253
+
254
+ Result: `class-naming` = `/^main-/` (specificity `[1, 0, 0]` is highest)
255
+
256
+ ## Merging with Global Rules
257
+
258
+ When a `nodeRules` or `childNodeRules` entry specifies a rule, the value is **merged** with the global rule configuration rather than simply replacing it:
259
+
260
+ ```typescript
261
+ const globalRule = ruleset.rules[ruleName];
262
+ const mergedRule = globalRule == null ? convertedRule : mergeRule(globalRule, convertedRule);
263
+ ```
264
+
265
+ `mergeRule(a, b)` (from `@markuplint/ml-config`) applies right-side precedence:
266
+
267
+ | Scenario | Result |
268
+ | ------------------------ | -------------------------------------------------------------------------------------------------- |
269
+ | `b` is `false` | Rule is disabled (returns `false`) |
270
+ | `b` is a primitive value | Replaces `a`'s value |
271
+ | Both are objects | Properties from `b` override `a`; `severity`, `value`, `options`, `reason` are individually merged |
272
+ | `b.options` exists | Merged with `a.options` (objects are spread, arrays are concatenated) |
273
+ | `b.value` is not set | Inherits from `a.value` |
274
+
275
+ ### Merging Example
276
+
277
+ ```json
278
+ {
279
+ "rules": {
280
+ "my-rule": {
281
+ "severity": "error",
282
+ "value": "strict",
283
+ "options": { "allow": ["a", "b"] }
284
+ }
285
+ },
286
+ "nodeRules": [
287
+ {
288
+ "selector": "nav",
289
+ "rules": {
290
+ "my-rule": {
291
+ "options": { "allow": ["c"] }
292
+ }
293
+ }
294
+ }
295
+ ]
296
+ }
297
+ ```
298
+
299
+ For `<nav>`, `my-rule` is resolved as:
300
+
301
+ ```json
302
+ {
303
+ "severity": "error",
304
+ "value": "strict",
305
+ "options": { "allow": ["c"] }
306
+ }
307
+ ```
308
+
309
+ `severity` and `value` are inherited from the global config. `options.allow` is overwritten by the `nodeRules` entry (object spread, not array concatenation at the `options` level — `options` itself is spread, not individual sub-properties).
310
+
311
+ ## Regex Selector and Template Variables
312
+
313
+ `nodeRules` and `childNodeRules` support regex selectors, which can capture data from element attributes. Captured values are available as Mustache template variables in the rule configuration:
314
+
315
+ ```json
316
+ {
317
+ "nodeRules": [
318
+ {
319
+ "regexSelector": {
320
+ "attrName": "data-prefix",
321
+ "attrValue": "/^(?<prefix>.+)$/"
322
+ },
323
+ "rules": {
324
+ "class-naming": "/^{{ prefix }}-/"
325
+ }
326
+ }
327
+ ]
328
+ }
329
+ ```
330
+
331
+ `exchangeValueOnRule(rule, matches.data)` renders these templates before merging.
332
+
333
+ ## Data Flow Diagram
334
+
335
+ ```
336
+ Config
337
+
338
+
339
+ Ruleset (rules, nodeRules, childNodeRules)
340
+
341
+
342
+ _ruleMapping(ruleset)
343
+
344
+ ├─── Layer 1: Global rules
345
+ │ └─ For every node (including #document):
346
+ │ ruleMapper.set(node, name, { from: 'rules', specificity: [0,0,0], rule })
347
+
348
+ ├─── Layer 2: Node rules
349
+ │ └─ For each nodeRule entry:
350
+ │ └─ For each ELEMENT_NODE matching the selector:
351
+ │ ├─ exchangeValueOnRule (template rendering)
352
+ │ ├─ mergeRule (merge with global)
353
+ │ └─ ruleMapper.set(node, name, { from: 'nodeRules', specificity, rule })
354
+
355
+ └─── Layer 3: Child node rules
356
+ └─ For each childNodeRule entry:
357
+ └─ For each ELEMENT_NODE matching the selector:
358
+ └─ For each child (or descendant if inheritance: true):
359
+ ├─ exchangeValueOnRule (template rendering)
360
+ ├─ mergeRule (merge with global)
361
+ └─ ruleMapper.set(child, name, { from: 'childNodeRules', specificity, rule })
362
+
363
+
364
+ ruleMapper.apply()
365
+
366
+ └─ For each node in the map:
367
+ node.rules[ruleName] = rule
368
+
369
+
370
+ node.rules populated → ready for walkOn() verification
371
+ ```
@@ -0,0 +1,18 @@
1
+ # MLDOM リファレンス
2
+
3
+ `@markuplint/ml-core` の MLDOM サブシステムの詳細リファレンスです。
4
+
5
+ ## ドキュメント
6
+
7
+ | ドキュメント | 説明 |
8
+ | -------------------------------------------------- | ------------------------------------------------------------------------------------------ |
9
+ | [概要](./ml-dom/overview.ja.md) | 概要、クラス階層、MLToken |
10
+ | [MLNode](./ml-dom/node.ja.md) | MLNode 抽象基底、MLParentNode |
11
+ | [MLDocument](./ml-dom/document.ja.md) | ドキュメントルートノード |
12
+ | [MLElement](./ml-dom/element.ja.md) | 要素ノード(セレクタマッチング、属性、省略要素) |
13
+ | [Pretender システム](./ml-dom/pretender.ja.md) | コンポーネントリンティング用の仮想要素マッピング(初期化、プロパティ委譲、アクセシブル名) |
14
+ | [MLAttr](./ml-dom/attr.ja.md) | 属性ノード(トークン分解、スプレッド属性) |
15
+ | [MLBlock](./ml-dom/block.ja.md) | プリプロセッサブロックノード(透過性、条件分岐子ノード、コンテンツモデル検証) |
16
+ | [ルールマッピング](./ml-dom/rule-mapping.ja.md) | ルールがノードに適用される仕組み(3層処理、詳細度解決) |
17
+ | [その他のノード型](./ml-dom/others.ja.md) | MLCharacterData, MLText, MLComment, MLDocumentType, MLElementCloseTag, MLDocumentFragment |
18
+ | [ヘルパーとユーティリティ](./ml-dom/helpers.ja.md) | ヘルパー関数、補助クラス、型ユーティリティ |
package/docs/ml-dom.md ADDED
@@ -0,0 +1,18 @@
1
+ # MLDOM Reference
2
+
3
+ Detailed reference for the MLDOM subsystem in `@markuplint/ml-core`.
4
+
5
+ ## Documents
6
+
7
+ | Document | Description |
8
+ | ------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
9
+ | [Overview](./ml-dom/overview.md) | Overview, class hierarchy, MLToken |
10
+ | [MLNode](./ml-dom/node.md) | MLNode abstract base, MLParentNode |
11
+ | [MLDocument](./ml-dom/document.md) | Document root node |
12
+ | [MLElement](./ml-dom/element.md) | Element node (selector matching, attributes, omitted elements) |
13
+ | [Pretender System](./ml-dom/pretender.md) | Virtual element mapping for component linting (initialization, property delegation, accessible name) |
14
+ | [MLAttr](./ml-dom/attr.md) | Attribute node (token decomposition, spread attributes) |
15
+ | [MLBlock](./ml-dom/block.md) | Preprocessor block node (transparency, conditional child nodes, content model validation) |
16
+ | [Rule Mapping](./ml-dom/rule-mapping.md) | How rules are applied to nodes (three-layer processing, specificity resolution) |
17
+ | [Other Node Types](./ml-dom/others.md) | MLCharacterData, MLText, MLComment, MLDocumentType, MLElementCloseTag, MLDocumentFragment |
18
+ | [Helpers & Utilities](./ml-dom/helpers.md) | Helper functions, supplementary classes, type utilities |