@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.
- package/ARCHITECTURE.ja.md +524 -0
- package/ARCHITECTURE.md +524 -0
- package/CHANGELOG.md +52 -2
- package/README.md +5 -0
- package/SKILL.md +61 -0
- package/docs/linting-pipeline.ja.md +307 -0
- package/docs/linting-pipeline.md +307 -0
- package/docs/maintenance.ja.md +210 -0
- package/docs/maintenance.md +210 -0
- package/docs/ml-dom/attr.ja.md +103 -0
- package/docs/ml-dom/attr.md +103 -0
- package/docs/ml-dom/block.ja.md +272 -0
- package/docs/ml-dom/block.md +272 -0
- package/docs/ml-dom/document.ja.md +141 -0
- package/docs/ml-dom/document.md +141 -0
- package/docs/ml-dom/element.ja.md +176 -0
- package/docs/ml-dom/element.md +176 -0
- package/docs/ml-dom/helpers.ja.md +203 -0
- package/docs/ml-dom/helpers.md +203 -0
- package/docs/ml-dom/node.ja.md +199 -0
- package/docs/ml-dom/node.md +199 -0
- package/docs/ml-dom/others.ja.md +120 -0
- package/docs/ml-dom/others.md +120 -0
- package/docs/ml-dom/overview.ja.md +102 -0
- package/docs/ml-dom/overview.md +102 -0
- package/docs/ml-dom/pretender.ja.md +269 -0
- package/docs/ml-dom/pretender.md +269 -0
- package/docs/ml-dom/rule-mapping.ja.md +371 -0
- package/docs/ml-dom/rule-mapping.md +371 -0
- package/docs/ml-dom.ja.md +18 -0
- package/docs/ml-dom.md +18 -0
- package/docs/rule-system.ja.md +287 -0
- package/docs/rule-system.md +287 -0
- package/lib/convert-ruleset.d.ts +7 -0
- package/lib/convert-ruleset.js +7 -0
- package/lib/debug.d.ts +4 -0
- package/lib/debug.js +4 -0
- package/lib/index.d.ts +4 -3
- package/lib/index.js +1 -1
- package/lib/ml-core.d.ts +37 -1
- package/lib/ml-core.js +171 -82
- package/lib/ml-dom/helper/accname.d.ts +8 -0
- package/lib/ml-dom/helper/accname.js +71 -55
- package/lib/ml-dom/helper/create-node.js +1 -0
- package/lib/ml-dom/helper/get-indent.d.ts +4 -1
- package/lib/ml-dom/helper/get-indent.js +21 -30
- package/lib/ml-dom/node/attr.d.ts +65 -4
- package/lib/ml-dom/node/attr.js +151 -53
- package/lib/ml-dom/node/block.d.ts +23 -2
- package/lib/ml-dom/node/block.js +24 -1
- package/lib/ml-dom/node/child-node.d.ts +9 -0
- package/lib/ml-dom/node/child-node.js +9 -0
- package/lib/ml-dom/node/comment.d.ts +7 -0
- package/lib/ml-dom/node/comment.js +7 -0
- package/lib/ml-dom/node/document-fragment.d.ts +8 -0
- package/lib/ml-dom/node/document-fragment.js +8 -0
- package/lib/ml-dom/node/document-type.d.ts +22 -0
- package/lib/ml-dom/node/document-type.js +25 -0
- package/lib/ml-dom/node/document.d.ts +88 -7
- package/lib/ml-dom/node/document.js +128 -32
- package/lib/ml-dom/node/dom-token-list.js +17 -30
- package/lib/ml-dom/node/element-close-tag.js +1 -0
- package/lib/ml-dom/node/element.d.ts +151 -5
- package/lib/ml-dom/node/element.js +242 -50
- package/lib/ml-dom/node/node-store.js +6 -15
- package/lib/ml-dom/node/node.d.ts +19 -1
- package/lib/ml-dom/node/node.js +175 -166
- package/lib/ml-dom/node/parent-node.js +14 -30
- package/lib/ml-dom/node/rule-mapper.js +7 -20
- package/lib/ml-dom/node/text.d.ts +19 -0
- package/lib/ml-dom/node/text.js +21 -0
- package/lib/ml-dom/node/types.d.ts +68 -0
- package/lib/ml-dom/token/token.d.ts +42 -0
- package/lib/ml-dom/token/token.js +59 -39
- package/lib/ml-rule/create-rule.d.ts +17 -1
- package/lib/ml-rule/ml-rule-context.js +7 -11
- package/lib/ml-rule/ml-rule.d.ts +66 -1
- package/lib/ml-rule/ml-rule.js +95 -25
- package/lib/ml-rule/types.d.ts +41 -0
- package/lib/plugin/plugin.d.ts +8 -0
- package/lib/plugin/plugin.js +8 -0
- package/lib/plugin/types.d.ts +21 -0
- package/lib/ruleset/index.d.ts +10 -0
- package/lib/ruleset/index.js +13 -0
- package/lib/test/index.d.ts +42 -1
- package/lib/test/index.js +39 -2
- package/lib/types.d.ts +10 -1
- package/lib/violation-collector.d.ts +33 -0
- package/lib/violation-collector.js +48 -28
- package/lib/virtual-rule.d.ts +72 -0
- package/lib/virtual-rule.js +233 -0
- package/package.json +16 -13
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# MLBlock — プリプロセッサブロックノード
|
|
2
|
+
|
|
3
|
+
**ソース:** `src/ml-dom/node/block.ts`
|
|
4
|
+
|
|
5
|
+
## 概要
|
|
6
|
+
|
|
7
|
+
`MLBlock` はテンプレートエンジンの条件分岐、ループ、その他のプリプロセッサディレクティブを表現する markuplint 固有の DOM ノードです。DOM Standard に対応するものはなく、`nodeType` はカスタム値 `101`(`MARKUPLINT_PREPROCESSOR_BLOCK`)です。
|
|
8
|
+
|
|
9
|
+
Svelte、Nunjucks、EJS、Pug などのテンプレートエンジンは、HTML コンテンツを非 HTML ブロック(例: `{#if}`、`{#each}`、`{% if %}`)でラップする構文を生成します。パーサーはこれらを `MLASTPreprocessorSpecificBlock` AST ノードに変換し、`MLBlock` がそれをラップします。
|
|
10
|
+
|
|
11
|
+
MLBlock はテンプレート構文と HTML コンテンツモデル検証の橋渡しをします。条件分岐、イテレーション、その他のフロー制御が、レンダリングされた HTML にどの子ノードが実際に存在するかに影響することを、markuplint が推論できるようにします。
|
|
12
|
+
|
|
13
|
+
- `nodeName`: `'#ml-block'`
|
|
14
|
+
- `nodeType`: `101`(`MARKUPLINT_PREPROCESSOR_BLOCK`)
|
|
15
|
+
|
|
16
|
+
## プロパティ
|
|
17
|
+
|
|
18
|
+
| プロパティ | 型 | 説明 |
|
|
19
|
+
| --------------- | ---------------------------- | -------------------------------------------------------------------------------------------- |
|
|
20
|
+
| `blockBehavior` | `MLASTBlockBehavior \| null` | ブロックの動作を示す構文の種類(下表参照)、非条件ブロックの場合は `null` |
|
|
21
|
+
| `isTransparent` | `boolean` | ツリー走査で透過的かどうか。現在は常に `true`(ソースの TODO を参照) |
|
|
22
|
+
| `isFragment` | `boolean` | ブロックが透過フラグメントとして機能するか(MLNode から継承、`astNode.isFragment` から設定) |
|
|
23
|
+
|
|
24
|
+
## blockBehavior の型
|
|
25
|
+
|
|
26
|
+
`blockBehavior` は、ブロックが条件分岐子ノードパターン生成にどのように参加するかを決定します(後述の[条件分岐子ノード](#条件分岐子ノード)を参照)。`type` プロパティを持つオブジェクト、または非条件ブロックの場合は `null` です。
|
|
27
|
+
|
|
28
|
+
### 条件グループ
|
|
29
|
+
|
|
30
|
+
認識された `blockBehavior.type` を持つブロックは条件グループを形成します。各グループは「開始」型で始まり、「分岐」型を含む場合があります:
|
|
31
|
+
|
|
32
|
+
| グループ | 開始 | 分岐 | 終了 |
|
|
33
|
+
| ---------- | --------------- | ------------------------------- | ------------------ |
|
|
34
|
+
| **if** | `'if'` | `'if:elseif'`、`'if:else'` | `'end'` または暗黙 |
|
|
35
|
+
| **each** | `'each'` | `'each:empty'` | `'end'` または暗黙 |
|
|
36
|
+
| **switch** | `'switch:case'` | `'switch:default'` | `'end'` または暗黙 |
|
|
37
|
+
| **await** | `'await'` | `'await:then'`、`'await:catch'` | `'end'` または暗黙 |
|
|
38
|
+
|
|
39
|
+
### すべての値
|
|
40
|
+
|
|
41
|
+
| `blockBehavior.type` | 説明 | 役割 |
|
|
42
|
+
| -------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------- |
|
|
43
|
+
| `'if'` | 条件ブロックの開始 | 新しい条件グループを開始 |
|
|
44
|
+
| `'if:elseif'` | 代替条件分岐 | 新しい条件グループを開始(パターン生成では `'if'` と同じ扱い) |
|
|
45
|
+
| `'if:else'` | デフォルト(else)分岐 | 現在のグループ内の分岐 |
|
|
46
|
+
| `'switch:case'` | switch の case 分岐 | 新しい条件グループを開始 |
|
|
47
|
+
| `'switch:default'` | switch のデフォルト分岐 | 現在のグループ内の分岐 |
|
|
48
|
+
| `'each'` | イテレーション(ループ)ブロックの開始 | 新しい条件グループを開始 |
|
|
49
|
+
| `'each:empty'` | イテレーションブロックの空状態 | 現在のグループ内の分岐 |
|
|
50
|
+
| `'await'` | 非同期ブロック(pending 状態) | 現在のグループ内の分岐 |
|
|
51
|
+
| `'await:then'` | 非同期ブロックの resolved 状態 | 現在のグループ内の分岐 |
|
|
52
|
+
| `'await:catch'` | 非同期ブロックの rejected 状態 | 現在のグループ内の分岐 |
|
|
53
|
+
| `'end'` | ブロック終了マーカー | 無視される(switch の `default` でフィルタされる) |
|
|
54
|
+
| `null` | ブロック動作なし(例: `{value}` のような式出力) | 条件グループではない。ミュータブルな子として扱われる(`blockBehavior` が `null`) |
|
|
55
|
+
|
|
56
|
+
## 透過性
|
|
57
|
+
|
|
58
|
+
`MLBlock` は常に透過的です(`isTransparent = true`)。透過性は MLBlock を DOM ツリー走査から見えなくしつつ、親子セマンティクスを保持するコア設計原則です。
|
|
59
|
+
|
|
60
|
+
### `parentNode` への影響
|
|
61
|
+
|
|
62
|
+
ノードの構文上の親が透過的な MLBlock の場合、`parentNode` はブロックをスキップし、ブロック自身の `parentNode` を再帰的に返します。つまり、テンプレート構文内のノードは、囲んでいる HTML 要素(ブロックではなく)を親として報告します:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
ソース: parentNode の戻り値:
|
|
66
|
+
<ul> ─┐
|
|
67
|
+
{#if cond} (MLBlock) │ ← スキップされる
|
|
68
|
+
<li>A</li> │ → <ul>(MLBlock ではない)
|
|
69
|
+
{/if} │
|
|
70
|
+
</ul> ─┘
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`MLNode.parentNode` の該当コード:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
if (parentNode.is(parentNode.MARKUPLINT_PREPROCESSOR_BLOCK)) {
|
|
77
|
+
if (parentNode.isTransparent) {
|
|
78
|
+
return parentNode.parentNode; // 上方に再帰
|
|
79
|
+
}
|
|
80
|
+
return null; // 非透過ブロック: 孤立
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
ブロックが非透過(`isTransparent = false`)の場合、子ノードは `parentNode === null`(孤立)を報告します。現在 `isTransparent` は常に `true` なので、この状況は発生しません。
|
|
85
|
+
|
|
86
|
+
### `childNodes` への影響
|
|
87
|
+
|
|
88
|
+
ブロックの子ノードは、`getPureChildNodes()` と `childNodes` の展開パイプラインを通じて親の `childNodes` にインライン化されます:
|
|
89
|
+
|
|
90
|
+
1. **`getPureChildNodes()`**: `MARKUPLINT_PREPROCESSOR_BLOCK` に対して `ELEMENT_NODE` や `DOCUMENT_FRAGMENT_NODE` と同様に動作します — `astNode.childNodes` を読み取り、`endtag` と `invalid` ノードをフィルタし、MLDOM ノードにマッピングします
|
|
91
|
+
2. **`childNodes`**(親側): `getPureChildNodes()` を呼び出し、`isFragment === true` の子については、その `childNodes` を再帰的にインライン化します
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
ソース: <ul>.childNodes の戻り値:
|
|
95
|
+
<ul> ─┐
|
|
96
|
+
{#if cond} │ ← MLBlock は childNodes に含まれない
|
|
97
|
+
<li>A</li> │ → [<li>A</li>, <li>B</li>]
|
|
98
|
+
<li>B</li> │
|
|
99
|
+
{/if} │
|
|
100
|
+
</ul> ─┘
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
これにより、親子関係を検証するルール(`permitted-contents` など)が、テンプレートエンジンのラッパーではなく、実効的な HTML の子ノードを確認できます。
|
|
104
|
+
|
|
105
|
+
### `syntacticalParentNode` と `parentNode` の比較
|
|
106
|
+
|
|
107
|
+
| プロパティ | 動作 | 用途 |
|
|
108
|
+
| ----------------------- | --------------------------------- | --------------------------------------- |
|
|
109
|
+
| `syntacticalParentNode` | MLBlock を含む直接の AST 親を返す | パースされた生の構造の理解 |
|
|
110
|
+
| `parentNode` | 透過的な MLBlock ノードをスキップ | リントルール用の DOM ライクなツリー走査 |
|
|
111
|
+
|
|
112
|
+
## 条件分岐子ノード
|
|
113
|
+
|
|
114
|
+
`MLNode` の `conditionalChildNodes()` メソッドは、MLBlock の `blockBehavior?.type` を使用して、レンダリングされた出力に現れうるすべての子ノードパターンを列挙します。これはテンプレート分岐がある場合のコンテンツモデル検証に不可欠です。
|
|
115
|
+
|
|
116
|
+
### アルゴリズム
|
|
117
|
+
|
|
118
|
+
1. 現在のノードの `childNodes` を走査する
|
|
119
|
+
2. 認識された `blockBehavior?.type` を持つ各 MLBlock 子に対して:
|
|
120
|
+
- `mode` を判定(`'if'` または `'switch'`)。`'each'` ブロックは新しいモードを開始しない
|
|
121
|
+
- ブロックに対して再帰的に `conditionalChildNodes()` を呼び出してサブパターンを取得
|
|
122
|
+
- すべての分岐の代替を `subBranches` 配列に収集
|
|
123
|
+
3. 条件グループ終了後に非ブロックの子が出現したとき:
|
|
124
|
+
- mode が `'if'` または `'switch'` の場合:`null` をセンチネルとしてプッシュ(どの分岐もレンダリングされない「空」ケースを表現)
|
|
125
|
+
- 現在のグループを閉じ、`subBranches` を `branches` にプッシュ
|
|
126
|
+
4. 空白のみのテキストノードはスキップ
|
|
127
|
+
5. 非ブロックの子は直接 `branches` に追加
|
|
128
|
+
6. `branches` を `branchesToPatterns()` に渡してデカルト積を計算
|
|
129
|
+
|
|
130
|
+
### `branchesToPatterns()`
|
|
131
|
+
|
|
132
|
+
このユーティリティ関数(`@markuplint/shared` から)は分岐の代替のデカルト積を計算します:
|
|
133
|
+
|
|
134
|
+
- 通常の要素(非配列)はすべてのパターンに出現する
|
|
135
|
+
- 配列の要素は代替を表す — 各要素が別のパターンを生成する
|
|
136
|
+
- `null` 値はフィルタされる(空の分岐を表現)
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
入力: [A, [B, C], D]
|
|
140
|
+
出力: [[A, B, D], [A, C, D]]
|
|
141
|
+
|
|
142
|
+
入力: [A, [B, null], C]
|
|
143
|
+
出力: [[A, B, C], [A, C]] ← null はフィルタ = 「分岐がレンダリングされない」
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 例
|
|
147
|
+
|
|
148
|
+
以下の Svelte テンプレートの場合:
|
|
149
|
+
|
|
150
|
+
```svelte
|
|
151
|
+
<ul>
|
|
152
|
+
{#if cond}
|
|
153
|
+
<li>A</li>
|
|
154
|
+
{:else}
|
|
155
|
+
<li>B</li>
|
|
156
|
+
{/if}
|
|
157
|
+
<li>C</li>
|
|
158
|
+
</ul>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
AST 構造:
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
MLElement <ul>
|
|
165
|
+
├── MLBlock (blockBehavior.type: 'if')
|
|
166
|
+
│ └── MLElement <li>A</li>
|
|
167
|
+
├── MLBlock (blockBehavior.type: 'if:else')
|
|
168
|
+
│ └── MLElement <li>B</li>
|
|
169
|
+
└── MLElement <li>C</li>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`<ul>` に対する `conditionalChildNodes()` の結果:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
branches = [[<li>A</li>, <li>B</li>, null], <li>C</li>]
|
|
176
|
+
↓ branchesToPatterns()
|
|
177
|
+
パターン 1: [<li>A</li>, <li>C</li>] ← if 分岐
|
|
178
|
+
パターン 2: [<li>B</li>, <li>C</li>] ← else 分岐
|
|
179
|
+
パターン 3: [<li>C</li>] ← 分岐なし(null はフィルタ)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`null` センチネルは、条件グループ(if/else)が何もレンダリングしない可能性があるため追加されます(例: パーサーが分岐が常に実行されるかどうかを判定できない場合)。`permitted-contents` ルールはすべてのパターンを検証し、すべてのケースでコンテンツモデルが満たされることを保証します。
|
|
183
|
+
|
|
184
|
+
### ネストされた条件分岐
|
|
185
|
+
|
|
186
|
+
アルゴリズムは再帰的な `conditionalChildNodes()` 呼び出しによって自然にネストを処理します:
|
|
187
|
+
|
|
188
|
+
```svelte
|
|
189
|
+
<div>
|
|
190
|
+
{#if a}
|
|
191
|
+
{#if b}
|
|
192
|
+
<span>X</span>
|
|
193
|
+
{:else}
|
|
194
|
+
<span>Y</span>
|
|
195
|
+
{/if}
|
|
196
|
+
{:else}
|
|
197
|
+
<span>Z</span>
|
|
198
|
+
{/if}
|
|
199
|
+
</div>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
内側の `{#if b}` ブロックは再帰的にパターン `[<span>X</span>]`、`[<span>Y</span>]` を生成し、外側のブロックの `subBranches` に展開されます。
|
|
203
|
+
|
|
204
|
+
## `hasMutableChildren()` との相互作用
|
|
205
|
+
|
|
206
|
+
`MLElement.hasMutableChildren()` は `blockBehavior` を使用して MLBlock を2つのカテゴリに区別します:
|
|
207
|
+
|
|
208
|
+
- **`blockBehavior` を持つブロック**(例: type `'if'`、`'each'`、`'switch:case'`):スキップ(`continue`)— `conditionalChildNodes()` がすべての可能なパターンを列挙して処理する
|
|
209
|
+
- **`blockBehavior` を持たないブロック**(`null`):即座に `true` を返す — `{value}` のような式出力やその他の非条件テンプレート構文を表し、内容が静的に決定できない
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
for (const child of this.getPureChildNodes()) {
|
|
213
|
+
if (child.is(child.MARKUPLINT_PREPROCESSOR_BLOCK)) {
|
|
214
|
+
if (child.blockBehavior) {
|
|
215
|
+
continue; // 条件セマンティクスあり → 別の場所で処理
|
|
216
|
+
}
|
|
217
|
+
return true; // 条件セマンティクスなし → 本当にミュータブル
|
|
218
|
+
}
|
|
219
|
+
// ...
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
この区別は重要です:`{#if}` ブロックは決定論的な分岐パターンを生成しますが、`{variable}` ブロックは任意のコンテンツを生成する可能性があります。`permitted-contents` のようなルールは前者を検証できますが、後者はスキップしなければなりません。
|
|
224
|
+
|
|
225
|
+
## リンティングパイプラインでの役割
|
|
226
|
+
|
|
227
|
+
MLBlock はリンティングパイプラインの複数のレベルで参加します:
|
|
228
|
+
|
|
229
|
+
### 1. パースフェーズ
|
|
230
|
+
|
|
231
|
+
テンプレートエンジンパーサー(Svelte、Nunjucks、EJS、Pug など)が `MLASTPreprocessorSpecificBlock` AST ノードを生成します。各パーサーは以下を担当します:
|
|
232
|
+
|
|
233
|
+
- `blockBehavior` の適切な設定(例: Svelte `{#if}` → `{ type: 'if' }`、`{#each}` → `{ type: 'each' }`)
|
|
234
|
+
- ブロック内への子 AST ノードのネスト
|
|
235
|
+
- フラグメントコンテナとして機能すべきブロックの `isFragment` 設定
|
|
236
|
+
|
|
237
|
+
### 2. MLDOM 構築
|
|
238
|
+
|
|
239
|
+
`createNode()` は `'psblock'` AST タイプを `MLBlock` にマッピングします。ブロックは要素、テキストノード、コメントと並んでドキュメントの `nodeList` に含まれます。
|
|
240
|
+
|
|
241
|
+
### 3. ツリー走査
|
|
242
|
+
|
|
243
|
+
透過性により、MLBlock は標準的な DOM 走査から見えなくなります:
|
|
244
|
+
|
|
245
|
+
- `parentNode` は透過ブロックをスキップする
|
|
246
|
+
- `childNodes` はブロックの子を親にインライン化する
|
|
247
|
+
- `walkOn('Element', ...)` は MLBlock ノードに遭遇しない(要素、テキスト、コメント、属性、閉じタグのみを走査)
|
|
248
|
+
|
|
249
|
+
### 4. コンテンツモデル検証
|
|
250
|
+
|
|
251
|
+
`permitted-contents` ルールは `conditionalChildNodes()` を使用してすべての可能なコンテンツパターンを検証します:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
const childNodesPatterns = options.evaluateConditionalChildNodes
|
|
255
|
+
? el.conditionalChildNodes().map(childNodes => [...childNodes])
|
|
256
|
+
: [[...el.childNodes].filter(/* ... */)];
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
各パターンは HTML コンテンツモデル仕様に対して独立に検証されます。
|
|
260
|
+
|
|
261
|
+
## メソッド
|
|
262
|
+
|
|
263
|
+
MLBlock は DOM API 互換性のために `ChildNode` インターフェースメソッドを実装しています:
|
|
264
|
+
|
|
265
|
+
| メソッド | シグネチャ | 説明 |
|
|
266
|
+
| ------------- | ------------------------------------------------ | ------------------------------ |
|
|
267
|
+
| `after` | `after(...nodes: (string \| MLElement)[])` | このブロックの後にノードを挿入 |
|
|
268
|
+
| `before` | `before(...nodes: (string \| MLElement)[])` | このブロックの前にノードを挿入 |
|
|
269
|
+
| `remove` | `remove()` | 親からこのブロックを削除 |
|
|
270
|
+
| `replaceWith` | `replaceWith(...nodes: (string \| MLElement)[])` | このブロックを他のノードで置換 |
|
|
271
|
+
|
|
272
|
+
これらのメソッドは `src/ml-dom/manipulations/child-node-methods.ts` の共有実装に委譲します。
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# MLBlock — Preprocessor Block Node
|
|
2
|
+
|
|
3
|
+
**Source:** `src/ml-dom/node/block.ts`
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`MLBlock` is a markuplint-specific DOM node that represents template engine constructs such as conditionals, loops, and other preprocessor directives. It has no DOM Standard equivalent; its `nodeType` is a custom value `101` (`MARKUPLINT_PREPROCESSOR_BLOCK`).
|
|
8
|
+
|
|
9
|
+
Template engines like Svelte, Nunjucks, EJS, Pug, and others produce constructs (e.g., `{#if}`, `{#each}`, `{% if %}`) that wrap HTML content in non-HTML blocks. The parser translates these into `MLASTPreprocessorSpecificBlock` AST nodes, which `MLBlock` wraps.
|
|
10
|
+
|
|
11
|
+
MLBlock serves as the bridge between template syntax and HTML content model validation. It enables markuplint to reason about conditional branches, iteration, and other control flow that affects which child nodes are actually present in the rendered HTML.
|
|
12
|
+
|
|
13
|
+
- `nodeName`: `'#ml-block'`
|
|
14
|
+
- `nodeType`: `101` (`MARKUPLINT_PREPROCESSOR_BLOCK`)
|
|
15
|
+
|
|
16
|
+
## Properties
|
|
17
|
+
|
|
18
|
+
| Property | Type | Description |
|
|
19
|
+
| --------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
20
|
+
| `blockBehavior` | `MLASTBlockBehavior \| null` | The block behavior describing the type of construct (see table below), or `null` for non-conditional blocks |
|
|
21
|
+
| `isTransparent` | `boolean` | Whether the block is transparent in tree traversal; currently always `true` (see source TODO) |
|
|
22
|
+
| `isFragment` | `boolean` | Whether this block acts as a transparent fragment (inherited from MLNode, set from `astNode.isFragment`) |
|
|
23
|
+
|
|
24
|
+
## blockBehavior Types
|
|
25
|
+
|
|
26
|
+
`blockBehavior` determines how the block participates in conditional child node pattern generation (see [Conditional Child Nodes](#conditional-child-nodes) below). It is an object with a `type` property, or `null` for non-conditional blocks.
|
|
27
|
+
|
|
28
|
+
### Conditional Groups
|
|
29
|
+
|
|
30
|
+
Blocks with a recognized `blockBehavior.type` form conditional groups. Each group starts with a "start" type and may include "branch" types:
|
|
31
|
+
|
|
32
|
+
| Group | Start | Branches | End |
|
|
33
|
+
| ---------- | --------------- | ------------------------------- | ------------------- |
|
|
34
|
+
| **if** | `'if'` | `'if:elseif'`, `'if:else'` | `'end'` or implicit |
|
|
35
|
+
| **each** | `'each'` | `'each:empty'` | `'end'` or implicit |
|
|
36
|
+
| **switch** | `'switch:case'` | `'switch:default'` | `'end'` or implicit |
|
|
37
|
+
| **await** | `'await'` | `'await:then'`, `'await:catch'` | `'end'` or implicit |
|
|
38
|
+
|
|
39
|
+
### All Values
|
|
40
|
+
|
|
41
|
+
| `blockBehavior.type` | Description | Role |
|
|
42
|
+
| -------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
43
|
+
| `'if'` | Start of conditional block | Starts a new conditional group |
|
|
44
|
+
| `'if:elseif'` | Alternative conditional branch | Starts a new conditional group (treated the same as `'if'` in pattern generation) |
|
|
45
|
+
| `'if:else'` | Default (else) branch | Branch within current group |
|
|
46
|
+
| `'switch:case'` | Switch case branch | Starts a new conditional group |
|
|
47
|
+
| `'switch:default'` | Switch default branch | Branch within current group |
|
|
48
|
+
| `'each'` | Start of iteration (loop) block | Continues in current mode (no new mode set); flattened into `childNodes` |
|
|
49
|
+
| `'each:empty'` | Empty state for an iteration block | Branch within current group |
|
|
50
|
+
| `'await'` | Asynchronous block (pending state) | Branch within current group |
|
|
51
|
+
| `'await:then'` | Resolved state of async block | Branch within current group |
|
|
52
|
+
| `'await:catch'` | Rejected state of async block | Branch within current group |
|
|
53
|
+
| `'end'` | Closing block marker | Ignored (filtered out by `default` in the switch) |
|
|
54
|
+
| `null` | No block behavior (e.g., expression output like `{value}`) | Not a conditional group; treated as a mutable child (`blockBehavior` is `null`) |
|
|
55
|
+
|
|
56
|
+
## Transparency
|
|
57
|
+
|
|
58
|
+
`MLBlock` is always transparent (`isTransparent = true`). Transparency is the core design principle that makes MLBlock invisible to DOM tree traversal while preserving parent-child semantics.
|
|
59
|
+
|
|
60
|
+
### Effect on `parentNode`
|
|
61
|
+
|
|
62
|
+
When a node's syntactical parent is a transparent MLBlock, `parentNode` skips the block and returns the block's own `parentNode` recursively. This means nodes inside template constructs report the enclosing HTML element (not the block) as their parent:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Source: parentNode returns:
|
|
66
|
+
<ul> ─┐
|
|
67
|
+
{#if cond} (MLBlock) │ ← skipped
|
|
68
|
+
<li>A</li> │ → <ul> (not the MLBlock)
|
|
69
|
+
{/if} │
|
|
70
|
+
</ul> ─┘
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The relevant code in `MLNode.parentNode`:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
if (parentNode.is(parentNode.MARKUPLINT_PREPROCESSOR_BLOCK)) {
|
|
77
|
+
if (parentNode.isTransparent) {
|
|
78
|
+
return parentNode.parentNode; // Recurse upward
|
|
79
|
+
}
|
|
80
|
+
return null; // Non-transparent block: orphan
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
If a block were non-transparent (`isTransparent = false`), its children would report `parentNode === null` (orphaned). Currently this does not happen since `isTransparent` is always `true`.
|
|
85
|
+
|
|
86
|
+
### Effect on `childNodes`
|
|
87
|
+
|
|
88
|
+
The block's children are inlined into the parent's `childNodes` through the `getPureChildNodes()` and `childNodes` expansion pipeline:
|
|
89
|
+
|
|
90
|
+
1. **`getPureChildNodes()`**: Works on `MARKUPLINT_PREPROCESSOR_BLOCK` the same way as on `ELEMENT_NODE` and `DOCUMENT_FRAGMENT_NODE` — it reads `astNode.childNodes`, filters out `endtag` and `invalid` nodes, and maps them to MLDOM nodes
|
|
91
|
+
2. **`childNodes`** (on the parent): Calls `getPureChildNodes()`, then for any child with `isFragment === true`, inlines its `childNodes` recursively
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
Source: <ul>.childNodes returns:
|
|
95
|
+
<ul> ─┐
|
|
96
|
+
{#if cond} │ ← MLBlock is not in childNodes
|
|
97
|
+
<li>A</li> │ → [<li>A</li>, <li>B</li>]
|
|
98
|
+
<li>B</li> │
|
|
99
|
+
{/if} │
|
|
100
|
+
</ul> ─┘
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
This ensures that rules validating parent-child relationships (like `permitted-contents`) see the effective HTML children, not the template engine wrappers.
|
|
104
|
+
|
|
105
|
+
### `syntacticalParentNode` vs `parentNode`
|
|
106
|
+
|
|
107
|
+
| Property | Behavior | Use case |
|
|
108
|
+
| ----------------------- | ------------------------------------------------ | -------------------------------------- |
|
|
109
|
+
| `syntacticalParentNode` | Returns the direct AST parent, including MLBlock | Understanding the raw parsed structure |
|
|
110
|
+
| `parentNode` | Skips transparent MLBlock nodes | DOM-like tree traversal for lint rules |
|
|
111
|
+
|
|
112
|
+
## Conditional Child Nodes
|
|
113
|
+
|
|
114
|
+
The `conditionalChildNodes()` method on `MLNode` uses MLBlock's `blockBehavior?.type` to enumerate all possible child node patterns that could appear in the rendered output. This is critical for content model validation in the presence of template branching.
|
|
115
|
+
|
|
116
|
+
### Algorithm
|
|
117
|
+
|
|
118
|
+
1. Walk through `childNodes` of the current node
|
|
119
|
+
2. For each MLBlock child with a recognized `blockBehavior?.type`:
|
|
120
|
+
- Determine the `mode` (`'if'` or `'switch'`); `'each'` blocks do not start a new mode
|
|
121
|
+
- Recursively call `conditionalChildNodes()` on the block to get its sub-patterns
|
|
122
|
+
- Collect all branch alternatives into a `subBranches` array
|
|
123
|
+
3. When a non-block child is encountered after a conditional group ends:
|
|
124
|
+
- If the mode was `'if'` or `'switch'`: push `null` as a sentinel (representing the "empty" case where none of the branches render)
|
|
125
|
+
- Close the current group and push `subBranches` to `branches`
|
|
126
|
+
4. Skip whitespace-only text nodes
|
|
127
|
+
5. Non-block children are added directly to `branches`
|
|
128
|
+
6. Pass `branches` to `branchesToPatterns()` to compute the Cartesian product
|
|
129
|
+
|
|
130
|
+
### `branchesToPatterns()`
|
|
131
|
+
|
|
132
|
+
This utility function (from `@markuplint/shared`) computes the Cartesian product of branch alternatives:
|
|
133
|
+
|
|
134
|
+
- Regular items (non-array) appear in every pattern
|
|
135
|
+
- Array items represent alternatives — each item produces a separate pattern
|
|
136
|
+
- `null` values are filtered out (representing empty branches)
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
Input: [A, [B, C], D]
|
|
140
|
+
Output: [[A, B, D], [A, C, D]]
|
|
141
|
+
|
|
142
|
+
Input: [A, [B, null], C]
|
|
143
|
+
Output: [[A, B, C], [A, C]] ← null filtered out = "no branch rendered"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Example
|
|
147
|
+
|
|
148
|
+
Given the following Svelte template:
|
|
149
|
+
|
|
150
|
+
```svelte
|
|
151
|
+
<ul>
|
|
152
|
+
{#if cond}
|
|
153
|
+
<li>A</li>
|
|
154
|
+
{:else}
|
|
155
|
+
<li>B</li>
|
|
156
|
+
{/if}
|
|
157
|
+
<li>C</li>
|
|
158
|
+
</ul>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The AST structure is:
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
MLElement <ul>
|
|
165
|
+
├── MLBlock (blockBehavior.type: 'if')
|
|
166
|
+
│ └── MLElement <li>A</li>
|
|
167
|
+
├── MLBlock (blockBehavior.type: 'if:else')
|
|
168
|
+
│ └── MLElement <li>B</li>
|
|
169
|
+
└── MLElement <li>C</li>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`conditionalChildNodes()` on `<ul>` produces:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
branches = [[<li>A</li>, <li>B</li>, null], <li>C</li>]
|
|
176
|
+
↓ branchesToPatterns()
|
|
177
|
+
Pattern 1: [<li>A</li>, <li>C</li>] ← if branch taken
|
|
178
|
+
Pattern 2: [<li>B</li>, <li>C</li>] ← else branch taken
|
|
179
|
+
Pattern 3: [<li>C</li>] ← no branch (null filtered out)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The `null` sentinel is added because the conditional group (if/else) might not render anything at all (e.g., if the parser cannot determine whether a branch always executes). The `permitted-contents` rule validates all patterns to ensure the content model is satisfied in every case.
|
|
183
|
+
|
|
184
|
+
### Nested Conditionals
|
|
185
|
+
|
|
186
|
+
The algorithm handles nesting naturally through recursive `conditionalChildNodes()` calls:
|
|
187
|
+
|
|
188
|
+
```svelte
|
|
189
|
+
<div>
|
|
190
|
+
{#if a}
|
|
191
|
+
{#if b}
|
|
192
|
+
<span>X</span>
|
|
193
|
+
{:else}
|
|
194
|
+
<span>Y</span>
|
|
195
|
+
{/if}
|
|
196
|
+
{:else}
|
|
197
|
+
<span>Z</span>
|
|
198
|
+
{/if}
|
|
199
|
+
</div>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The inner `{#if b}` block recursively generates its patterns `[<span>X</span>]`, `[<span>Y</span>]`, which are then flattened into the outer block's sub-branches.
|
|
203
|
+
|
|
204
|
+
## Interaction with `hasMutableChildren()`
|
|
205
|
+
|
|
206
|
+
`MLElement.hasMutableChildren()` uses `blockBehavior` to distinguish between two categories of MLBlock:
|
|
207
|
+
|
|
208
|
+
- **Blocks WITH `blockBehavior`** (e.g., type `'if'`, `'switch:case'`): Skipped (`continue`) — these are handled by `conditionalChildNodes()` which enumerates all possible patterns. Note that `'each'` and `'end'` blocks are flattened into `childNodes` (their children are inlined), so they do not reach `hasMutableChildren()` as blocks
|
|
209
|
+
- **Blocks WITHOUT `blockBehavior`** (`null`): Return `true` immediately — these represent expression outputs like `{value}` or other non-conditional template constructs whose content cannot be statically determined
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
for (const child of this.getPureChildNodes()) {
|
|
213
|
+
if (child.is(child.MARKUPLINT_PREPROCESSOR_BLOCK)) {
|
|
214
|
+
if (child.blockBehavior) {
|
|
215
|
+
continue; // Has conditional semantics → handled elsewhere
|
|
216
|
+
}
|
|
217
|
+
return true; // No conditional semantics → truly mutable
|
|
218
|
+
}
|
|
219
|
+
// ...
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
This distinction is crucial: an `{#if}` block produces deterministic branch patterns, while a `{variable}` block can produce arbitrary content. Rules like `permitted-contents` can validate the former but must skip the latter.
|
|
224
|
+
|
|
225
|
+
## Role in the Linting Pipeline
|
|
226
|
+
|
|
227
|
+
MLBlock participates at multiple levels of the linting pipeline:
|
|
228
|
+
|
|
229
|
+
### 1. Parsing Phase
|
|
230
|
+
|
|
231
|
+
Template engine parsers (Svelte, Nunjucks, EJS, Pug, etc.) produce `MLASTPreprocessorSpecificBlock` AST nodes. Each parser is responsible for:
|
|
232
|
+
|
|
233
|
+
- Setting `blockBehavior` appropriately (e.g., Svelte `{#if}` → `{ type: 'if' }`, `{#each}` → `{ type: 'each' }`)
|
|
234
|
+
- Nesting child AST nodes within the block
|
|
235
|
+
- Setting `isFragment` when the block should act as a fragment container
|
|
236
|
+
|
|
237
|
+
### 2. MLDOM Construction
|
|
238
|
+
|
|
239
|
+
`createNode()` maps `'psblock'` AST type to `MLBlock`. The block is included in the document's `nodeList` alongside elements, text nodes, and comments.
|
|
240
|
+
|
|
241
|
+
### 3. Tree Traversal
|
|
242
|
+
|
|
243
|
+
Transparency ensures MLBlock is invisible to standard DOM traversal:
|
|
244
|
+
|
|
245
|
+
- `parentNode` skips transparent blocks
|
|
246
|
+
- `childNodes` inlines block children into the parent
|
|
247
|
+
- `walkOn('Element', ...)` does not encounter MLBlock nodes (only walks elements, text, comments, attrs, and close tags)
|
|
248
|
+
|
|
249
|
+
### 4. Content Model Validation
|
|
250
|
+
|
|
251
|
+
The `permitted-contents` rule uses `conditionalChildNodes()` to validate all possible content patterns:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
const childNodesPatterns = options.evaluateConditionalChildNodes
|
|
255
|
+
? el.conditionalChildNodes().map(childNodes => [...childNodes])
|
|
256
|
+
: [[...el.childNodes].filter(/* ... */)];
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Each pattern is independently validated against the HTML content model specification.
|
|
260
|
+
|
|
261
|
+
## Methods
|
|
262
|
+
|
|
263
|
+
MLBlock implements the `ChildNode` interface methods for DOM API compatibility:
|
|
264
|
+
|
|
265
|
+
| Method | Signature | Description |
|
|
266
|
+
| ------------- | ------------------------------------------------ | ----------------------------------- |
|
|
267
|
+
| `after` | `after(...nodes: (string \| MLElement)[])` | Insert nodes after this block |
|
|
268
|
+
| `before` | `before(...nodes: (string \| MLElement)[])` | Insert nodes before this block |
|
|
269
|
+
| `remove` | `remove()` | Remove this block from its parent |
|
|
270
|
+
| `replaceWith` | `replaceWith(...nodes: (string \| MLElement)[])` | Replace this block with other nodes |
|
|
271
|
+
|
|
272
|
+
These methods delegate to shared implementations in `src/ml-dom/manipulations/child-node-methods.ts`.
|