@markuplint/pug-parser 4.6.22 → 4.6.23

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.
@@ -0,0 +1,436 @@
1
+ # @markuplint/pug-parser
2
+
3
+ ## 概要
4
+
5
+ `@markuplint/pug-parser` は markuplint の Pug テンプレートパーサーです。Pug(旧 Jade)のインデントベースのテンプレート構文を統一された markuplint AST 形式(`MLASTDocument`)に変換します。上流のトークナイザ/パーサーとして `pug-lexer` と `pug-parser` を使用し、カスタム AST 最適化パス(`optimizeAST`)を実行して各ノードに正確なソースオフセット、生テキストスライス、終了位置を付与した後、メインの `PugParser` クラスが各ノードを markuplint AST アイテムに変換します。インライン HTML、タグ補間(`#[...]`)、ショートハンド属性(`#id` / `.class`)、`&attributes` スプレッド構文、ミックスイン、条件分岐、each ループ、インクルード、extends、フィルター、その他すべての Pug 固有の構文を処理します。
6
+
7
+ ## ディレクトリ構成
8
+
9
+ ```
10
+ src/
11
+ ├── index.ts — parser インスタンスを再エクスポート
12
+ ├── parser.ts — HtmlInPugParser、PugParser クラス、visitAttr、visitElement
13
+ ├── types.ts — 最適化済み AST 型(ASTNode、ASTBlock 等)と PugAST 名前空間
14
+ ├── pug-parser/
15
+ │ └── index.ts — pugParse()、optimizeAST()、ヘルパー関数群
16
+ └── utils/
17
+ └── get-offset-from-line-and-col.ts — マルチバイト対応オフセット計算
18
+ ```
19
+
20
+ ## アーキテクチャ図
21
+
22
+ ```mermaid
23
+ flowchart TD
24
+ subgraph upstream ["上流"]
25
+ pugLexer["pug-lexer\n(トークナイザ)"]
26
+ pugParserLib["pug-parser\n(AST ビルダー)"]
27
+ mlAst["@markuplint/ml-ast\n(AST 型定義)"]
28
+ parserUtils["@markuplint/parser-utils\n(抽象 Parser クラス)"]
29
+ htmlParser["@markuplint/html-parser\n(HtmlParser)"]
30
+ end
31
+
32
+ subgraph pkg ["@markuplint/pug-parser"]
33
+ pugParseFn["pugParse()\npug-lexer → pug-parser → optimizeAST"]
34
+ optimizeAST["optimizeAST()\nオフセット/生テキストでノードを強化"]
35
+ pugParserCls["PugParser\nextends Parser‹ASTNode›"]
36
+ htmlInPug["HtmlInPugParser\nextends HtmlParser"]
37
+ visitAttr["visitAttr()\n属性処理"]
38
+ types["types.ts\n最適化済み AST 型"]
39
+ end
40
+
41
+ subgraph downstream ["下流"]
42
+ mlCore["@markuplint/ml-core\n(MLASTDocument → MLDOM)"]
43
+ end
44
+
45
+ pugLexer -->|"Token[]"| pugParseFn
46
+ pugParserLib -->|"PugAST.Block"| pugParseFn
47
+ pugParseFn -->|"ASTBlock"| optimizeAST
48
+ optimizeAST -->|"強化済みノード"| pugParserCls
49
+ mlAst -->|"AST 型"| pugParserCls
50
+ parserUtils -->|"Parser 基底クラス"| pugParserCls
51
+ htmlParser -->|"継承"| htmlInPug
52
+ htmlInPug -->|"インライン HTML パース"| pugParserCls
53
+ pugParserCls -->|"visitAttr"| visitAttr
54
+ pugParserCls -->|"MLASTDocument"| mlCore
55
+ ```
56
+
57
+ ## HtmlInPugParser
58
+
59
+ `HtmlInPugParser` は `@markuplint/html-parser` の `HtmlParser` を拡張する内部クラスです。Pug テンプレート内に埋め込まれた**インライン HTML コンテンツ**(`<` や `#[` を含むテキストノード)をパースする目的でのみ使用されます。
60
+
61
+ ### コンストラクタ
62
+
63
+ ```ts
64
+ class HtmlInPugParser extends HtmlParser {
65
+ constructor() {
66
+ super({
67
+ ignoreTags: [
68
+ {
69
+ type: 'tag-interpolation',
70
+ start: '#[',
71
+ end: ']',
72
+ },
73
+ ],
74
+ });
75
+ }
76
+ }
77
+ ```
78
+
79
+ `ignoreTags` オプションは `#[...]` タグ補間シーケンスをマスクし、HTML パーサーがこれらを HTML としてパースしようとする代わりにプリプロセッサ固有ブロック(`#ps:tag-interpolation`)として扱うようにします。これらのブロックは後で新しい `PugParser` インスタンスによって再帰的にパースされます。
80
+
81
+ ## PugParser クラス
82
+
83
+ ### 継承関係
84
+
85
+ ```
86
+ Parser<ASTNode> (@markuplint/parser-utils)
87
+ └── PugParser (このパッケージ)
88
+ ```
89
+
90
+ ### コンストラクタ
91
+
92
+ ```ts
93
+ class PugParser extends Parser<ASTNode> {
94
+ constructor() {
95
+ super({
96
+ endTagType: 'never',
97
+ });
98
+ }
99
+ }
100
+ ```
101
+
102
+ `endTagType: 'never'` は、Pug が明示的な閉じタグを生成しないことを基底パーサーに伝えます — Pug はインデントベースのネストを使用します。
103
+
104
+ ### オーバーライドメソッド
105
+
106
+ | メソッド | 用途 |
107
+ | --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
108
+ | `tokenize()` | `pugParse()` を呼び出して最適化済み Pug AST を生成 |
109
+ | `parseError()` | pug-lexer/pug-parser のエラー(`msg`、`line`、`column`、`src`)を `ParserError` に変換 |
110
+ | `nodeize()` | 各 Pug AST ノードタイプを適切なビジターメソッドに振り分け |
111
+ | `afterFlattenNodes()` | `exposeInvalidNode: false` と `exposeWhiteSpace: false` で `super.afterFlattenNodes()` を呼び出す |
112
+ | `visitElement()` | パース済み属性を持つ `MLASTElement` 開始タグを構築し、子ノードを訪問 |
113
+ | `visitSpreadAttr()` | `null` を返す(スプレッド属性は `Tag` ケース内でインラインで処理され、基底クラスのスプレッド属性ビジターは使用しない) |
114
+ | `visitAttr()` | Pug 固有の属性構文を処理(ショートハンド、クォート名、非エスケープ、スクリプト値) |
115
+
116
+ ## tokenize()
117
+
118
+ ```ts
119
+ tokenize(options?: ParseOptions) {
120
+ const offsetOffset = options?.offsetOffset ?? 0;
121
+ const ast = pugParse(this.rawCode, offsetOffset >= 1).nodes;
122
+ return {
123
+ ast: [...ast],
124
+ isFragment: true,
125
+ };
126
+ }
127
+ ```
128
+
129
+ - 生の Pug ソースコードで `pugParse()` を呼び出す
130
+ - `useOffset` パラメータ(`offsetOffset >= 1` の場合 `true`)はレキサー出力から `indent` と `outdent` トークンをフィルタリング — これはゼロ以外のオフセットでサブテンプレート(例: タグ補間コンテンツ)をパースする際に必要(インデントコンテキストは親から継承されるため)
131
+ - Pug テンプレートは常にフラグメントとして扱われるため、常に `isFragment: true` を返す
132
+
133
+ ## nodeize() 詳細
134
+
135
+ `nodeize()` メソッドは、各最適化済み Pug AST ノードを markuplint AST アイテムに変換する中央のディスパッチです。まず親の名前空間を決定し、ノードの計算済みオフセットを使用してソースフラグメントをスライスします。
136
+
137
+ ### Doctype
138
+
139
+ ```ts
140
+ case 'Doctype':
141
+ return this.visitDoctype({ ...token, depth, parentNode, name: originNode.raw ?? '', publicId: '', systemId: '' });
142
+ ```
143
+
144
+ 生の doctype 文字列で `visitDoctype()` に委譲。Pug の doctype はショートハンド構文(`doctype html`)を使用するため、public ID と system ID は空です。
145
+
146
+ ### Text
147
+
148
+ テキストノードには3つの処理パスがあります:
149
+
150
+ 1. **空テキスト**(`raw.trim() === ''`): 空配列を返す(無視)
151
+ 2. **単純テキスト**(`<` や `#[` を含まない): `visitText()` に直接委譲
152
+ 3. **HTML やタグ補間を含むテキスト**: `HtmlInPugParser` でパース:
153
+ - 新しい `HtmlInPugParser` インスタンスを作成し、オフセット/行/列コンテキストでテキストコンテンツをパース
154
+ - 結果のノードリストを反復処理
155
+ - `#ps:tag-interpolation` という名前のノードは `#[` プレフィックスと `]` サフィックスが除去され、内部コンテンツが新しい `PugParser` インスタンスで再帰的にパースされる
156
+ - その他のノードはそのまま通過
157
+
158
+ この再帰的なパースチェーンにより、Pug のタグ補間(`#[strong 太字テキスト]`)が markuplint ノードに完全に解決されます。
159
+
160
+ ### Comment / BlockComment
161
+
162
+ - **Comment**: 単一行 Pug コメント(`//- comment` または `// comment`)。`isBogus: false` で `visitComment()` に委譲
163
+ - **BlockComment**: 複数行ブロックコメント。最後の子ブロックノードから終了オフセットを計算し、`visitComment()` に委譲
164
+
165
+ ### Tag
166
+
167
+ タグ処理は最も複雑なパスです:
168
+
169
+ 1. **名前空間解決**: `@markuplint/html-parser` の `getNamespace()` をタグ名と親名前空間で呼び出す
170
+ 2. **通常属性**: `originNode.attrs` の各属性を処理:
171
+ - `this.getOffsetsFromCode()` でオフセット/終了オフセットを計算
172
+ - ショートハンド属性(`#id` / `.class`)の場合、Pug AST では `offset === endOffset` となるため、`endOffset` を `attr.offset + attr.val.length - 1` として再計算
173
+ - 各属性トークンを `this.visitAttr()` に渡す
174
+ 3. **`&attributes` スプレッド構文**: 各 `attributeBlock` を処理:
175
+ - `&attributes(` プレフィックス長を列に加算してスキップ
176
+ - 内部式からトークンを作成
177
+ - 結果は `{ type: 'spread', nodeName: '#spread' }` として型付け
178
+ 4. **要素作成**: タグトークン、子ブロックノード、結合された属性配列(通常 + スプレッド)で `this.visitElement()` を呼び出す
179
+
180
+ ### Default(Pug 固有の構文)
181
+
182
+ その他すべてのノードタイプ — `Conditional`、`Code`、`Each`、`Mixin`、`MixinBlock`、`Include`、`RawInclude`、`Extends`、`NamedBlock`、`Case`、`When`、`While`、`Filter`、`YieldBlock`、`InterpolatedTag`、`FileReference` — は `visitPsBlock()` を通じてプリプロセッサ固有ブロックにマッピングされます。
183
+
184
+ `file` プロパティを持つノード(例: `Include`、`Extends`)では、ノードの終了位置からファイル参照オフセットを計算して、生ソースにファイルパスを含むようトークンが拡張されます。
185
+
186
+ 子ノードはノードタイプに応じて `block.nodes` または `nodes` から抽出されます。
187
+
188
+ ## 属性処理 (visitAttr)
189
+
190
+ `visitAttr()` は Pug 属性構文の全範囲を処理します:
191
+
192
+ ### ショートハンド属性
193
+
194
+ 生の属性が `#` または `.` で始まる場合:
195
+
196
+ ```ts
197
+ if (token.raw[0] === '#' || token.raw[0] === '.') {
198
+ // 値のみとしてパース(AttrState.BeforeValue)
199
+ // potentialName を設定: '#' → 'id'、'.' → 'class'
200
+ // isDuplicatable: class の場合 true(複数クラスを許可)
201
+ }
202
+ ```
203
+
204
+ - `#id-value` は `potentialName: 'id'`、`potentialValue: 'id-value'` としてパース
205
+ - `.class-name` は `potentialName: 'class'`、`potentialValue: 'class-name'`、`isDuplicatable: true` としてパース
206
+ - `startState: AttrState.BeforeValue` はトークン全体が値であることをパーサーに伝える(name=value 構造ではない)
207
+ - `quoteSet: []` と `endOfUnquotedValueChars: []` でクォート検出を無効化
208
+
209
+ ### 通常属性
210
+
211
+ ショートハンド以外の属性:
212
+
213
+ - `quoteSet: []` — Pug 属性は属性自体に HTML スタイルのクォートを使用しない
214
+ - `noQuoteValueType: 'script'` — クォートなしの値は JavaScript 式として扱う
215
+ - `endOfUnquotedValueChars: []` — 特定の値終了区切り文字なし
216
+ - 属性名が `class` の場合、`isDuplicatable` を `true` に設定
217
+
218
+ ### クォート付き属性名
219
+
220
+ ```ts
221
+ if (attr.name.raw.startsWith("'") && attr.name.raw.endsWith("'")) {
222
+ this.updateAttr(attr, { potentialName: attr.name.raw.slice(1, -1) });
223
+ }
224
+ ```
225
+
226
+ Pug では属性名をシングルクォートで囲むことができます(例: `'data-value'="foo"`)。クォートを除去して実際の属性名を取得します。
227
+
228
+ ### 非エスケープ属性
229
+
230
+ ```ts
231
+ if (attr.name.raw.endsWith('!')) {
232
+ this.updateAttr(attr, { potentialName: attr.name.raw.slice(0, -1) });
233
+ }
234
+ ```
235
+
236
+ Pug の属性名の `!` サフィックス(例: `href!="/url"`)は、値が HTML エスケープされるべきでないことを示します。`!` は potential name から除去されます。
237
+
238
+ ### 値の型パース
239
+
240
+ 属性値は `@markuplint/parser-utils` の `scriptParser()` を使用して分析されます:
241
+
242
+ | scriptParser トークン型 | 結果 |
243
+ | ----------------------- | ------------------------------------------------------------------------------------------------ |
244
+ | `Numeric` | `valueType: 'number'` |
245
+ | `Boolean` | `valueType: 'boolean'` |
246
+ | `String` / `Template` | `super.visitAttr()` で再パースしてクォートと値を抽出。`!` サフィックスの場合 `valueType: 'code'` |
247
+ | 複数トークン | `isDynamicValue: true`、`valueType: 'code'`(複雑な JavaScript 式) |
248
+
249
+ ## Pug AST 最適化 (pug-parser/index.ts)
250
+
251
+ ### pugParse()
252
+
253
+ Pug テンプレートパースのエントリーポイント:
254
+
255
+ ```
256
+ Pug ソース → pug-lexer → [オプションの indent/outdent フィルタ] → pug-parser → optimizeAST → ASTBlock
257
+ ```
258
+
259
+ 1. **レキシング**: `lexer(pug)` が `Token[]` 配列を生成
260
+ 2. **インデントフィルタリング**: `useOffset` が `true` の場合、サブテンプレートのパース時のインデントエラーを防ぐため `indent` と `outdent` トークンを除去
261
+ 3. **クローン**: パーサーと最適化パスの両方が独立したトークン参照を必要とするため、`structuredClone()` でトークンをクローン
262
+ 4. **パース**: `parser(lexOrigin)` が生の `PugAST.Block` を生成
263
+ 5. **最適化**: `optimizeAST(originAst, lex, pug)` がすべてのノードに計算済みオフセットと生ソースを付与
264
+
265
+ ### optimizeAST()
266
+
267
+ 生の pug-parser AST を最適化済み AST に再帰的に変換します。各ノードに対して:
268
+
269
+ 1. **オフセット計算**: `getOffsetsFromLines()` を使用して行/列から文字オフセットを計算
270
+ 2. **終了位置**: `getLocationFromToken()` でマッチするレキサートークンを見つけ、終了行/列/オフセットを決定
271
+ 3. **生ソース**: 元のソースをスライス: `pug.slice(offset, endOffset)`
272
+ 4. **タイプ別処理**:
273
+
274
+ | ノードタイプ | 処理 |
275
+ | ----------------- | ------------------------------------------------------------------------------------------- |
276
+ | `Block` | 再帰的に最適化し、親にフラット化 |
277
+ | `Tag` | 属性に `getAttrs()`、タグ終了に `getEndAttributeLocation()`、ブロックの再帰的最適化 |
278
+ | `Conditional` | consequent ブロックを最適化、次に else-if/else チェーンに `optimizeASTOfConditionalNode()` |
279
+ | `Each` | 子ブロックを最適化 |
280
+ | `Include` | 子ブロックを最適化 |
281
+ | `RawInclude` | フィルターを保持 |
282
+ | `Mixin` | `['mixin', 'call']` タイプフィルターで `getLocationFromToken()`、オプションのブロック最適化 |
283
+ | `MixinBlock` | 単純な強化 |
284
+ | `NamedBlock` | 再帰的最適化のために `Block` として再ラップ |
285
+ | `Comment` | 単純な強化 |
286
+ | `BlockComment` | 子ブロックを最適化 |
287
+ | `Code` | 子ブロックを最適化 |
288
+ | `Text` | `getPipelessText()` チェック、次にマルチテキスト処理に `getRawTextAndLocationEnd()` |
289
+ | `Doctype` | 単純な強化 |
290
+ | `Case` / `When` | 子ブロックを最適化 |
291
+ | `Filter` | フィルターオプションに `getAttrs()`、終了に `getEndAttributeLocation()`、ブロック最適化 |
292
+ | `Extends` | 単純な強化 |
293
+ | `FileReference` | 単純な強化 |
294
+ | `IncludeFilter` | 単純な強化 |
295
+ | `InterpolatedTag` | 子ブロックを最適化 |
296
+ | `While` | 単純な強化 |
297
+ | `YieldBlock` | 単純な強化 |
298
+
299
+ 5. **テキストマージ**: すべてのノードの処理後、`mergeTextNode()` が連続する `Text` ノードを単一ノードに結合
300
+
301
+ ### getOffsetsFromLines()
302
+
303
+ ソース文字列から累積オフセットルックアップテーブルを構築します:
304
+
305
+ ```ts
306
+ function getOffsetsFromLines(pug: string): number[] {
307
+ const lines = pug.split(/\n/);
308
+ let chars = 0;
309
+ return lines.map(line => {
310
+ chars += line.length + 1; // +1 は改行文字
311
+ return chars;
312
+ });
313
+ }
314
+ ```
315
+
316
+ 各エントリ `offsets[i]` は `i+1` 行目までの累積文字数(改行を含む)を保持。使用方法: `lineOffset = offsets[line - 2]` で対象行の開始オフセットを取得。
317
+
318
+ ### mergeTextNode()
319
+
320
+ 最初のノードの `raw`、`endColumn`、`endLine`、`endOffset` を拡張して、連続する `Text` ノードを結合します:
321
+
322
+ ```ts
323
+ if (prevNode.type === 'Text' && node.type === 'Text') {
324
+ prevNode.raw = pug.slice(prevNode.offset, node.endOffset);
325
+ prevNode.endColumn = node.endColumn;
326
+ prevNode.endLine = node.endLine;
327
+ prevNode.endOffset = node.endOffset;
328
+ }
329
+ ```
330
+
331
+ ### getAttrs()
332
+
333
+ Pug AST の各属性を対応するレキサートークンと照合して属性データを強化します:
334
+
335
+ 1. `offsets[attr.line - 2] + attr.column - 1` から属性のオフセットを計算
336
+ 2. 行/列でマッチするレキサートークンを検索
337
+ 3. トークンの位置範囲から属性の長さを計算
338
+ 4. 生ソースをスライスして強化済み `ASTAttr` を作成
339
+
340
+ ### getPipelessText()
341
+
342
+ `Text` ノードが**パイプレステキストブロック** — パイプ文字なしでタグの下にインデントされたテキストコンテンツ — の一部であるかを検出します:
343
+
344
+ ```pug
345
+ p.
346
+ これはパイプレステキストです。
347
+ 複数行にわたります。
348
+ ```
349
+
350
+ レキサー出力で `start-pipeless-text` と `end-pipeless-text` トークンを検索。テキストノードがそのような範囲内にある場合、パイプレステキストブロックの全体範囲を返します。
351
+
352
+ ### getEndAttributeLocation()
353
+
354
+ タグの位置以降のレキサートークンをスキャンして、すべての属性を含むタグの終了位置を決定します。`attribute`、`start-attributes`、`end-attributes`、`id`、`class` 以外のトークンに遭遇するまでトラッキングし、最後の属性関連トークンの終了位置を返します。
355
+
356
+ ### getRawTextAndLocationEnd()
357
+
358
+ 複数行テキストとパイプテキストの複雑なテキストノード処理を行います:
359
+
360
+ 1. テキストノードの開始位置からレキサートークンを走査
361
+ 2. `text` と `text-html` トークンで終了位置をトラッキング
362
+ 3. `indent` / `outdent` トークンで深さをモニタリング
363
+ 4. パイプテキスト(`|` で始まる行)を検出して処理を停止
364
+ 5. 計算済み位置データを持つ `ASTText` ノードの配列を返す
365
+
366
+ ### optimizeASTOfConditionalNode()
367
+
368
+ 条件分岐ノードの `else if` / `else` チェーンを再帰的に処理します:
369
+
370
+ 1. `else-if` 分岐の場合: レキサー出力で `else-if` トークンを検索し、位置を計算して `Conditional` ノードを作成
371
+ 2. `else` 分岐(`Block` タイプの `alternate`)の場合: `else` トークンを検索し、位置を計算して `Conditional` ノードを作成
372
+ 3. 連鎖条件分岐(`Conditional` タイプの `alternate`)の場合: 深さを増加させて再帰的に呼び出し
373
+
374
+ ## バージョン互換性
375
+
376
+ このパッケージは Pug 3 構文仕様をサポートする `pug-lexer` と `pug-parser` を使用しています。`types.ts` の Pug AST 型は [pug-ast-spec](https://github.com/pugjs/pug-ast-spec/blob/master/parser.md) をモデルにしており、属性ブロックと追加位置データの拡張が加えられています。
377
+
378
+ ## 主要ソースファイル
379
+
380
+ | ファイル | 用途 |
381
+ | ------------------------------------------- | --------------------------------------------------------------------- |
382
+ | `src/parser.ts` | `HtmlInPugParser` と `PugParser` クラス、全ビジターメソッド |
383
+ | `src/pug-parser/index.ts` | `pugParse()`、`optimizeAST()`、全 AST 強化ヘルパー関数 |
384
+ | `src/types.ts` | `ASTNode` 共用体、`ASTBlock`、最適化済みノード型、`PugAST` 名前空間型 |
385
+ | `src/utils/get-offset-from-line-and-col.ts` | `getOffsetFromLineAndCol()` マルチバイト対応オフセット計算 |
386
+ | `src/index.ts` | `parser` インスタンスの再エクスポート |
387
+
388
+ ## 外部依存
389
+
390
+ | 依存パッケージ | 用途 |
391
+ | -------------------------- | -------------------------------------------------------------------------------- |
392
+ | `@markuplint/html-parser` | `HtmlParser` クラス(`HtmlInPugParser` が拡張)と `getNamespace()` 関数 |
393
+ | `@markuplint/ml-ast` | AST 型定義(`MLASTElement`、`MLASTAttr`、`MLASTParentNode` 等) |
394
+ | `@markuplint/parser-utils` | 抽象 `Parser` クラス、`ParserError`、`AttrState`、`scriptParser`、ユーティリティ |
395
+ | `pug-lexer` | Pug テンプレートのトークン化 |
396
+ | `pug-parser` | Pug トークンストリームから AST への変換 |
397
+
398
+ ## 統合ポイント
399
+
400
+ ```mermaid
401
+ flowchart TD
402
+ subgraph upstream ["上流"]
403
+ pugLexer["pug-lexer"]
404
+ pugParserLib["pug-parser"]
405
+ mlAst["@markuplint/ml-ast\n(AST 型定義)"]
406
+ parserUtils["@markuplint/parser-utils\n(Parser 基底クラス)"]
407
+ htmlParser["@markuplint/html-parser\n(HtmlParser)"]
408
+ end
409
+
410
+ subgraph pkg ["@markuplint/pug-parser"]
411
+ parser["PugParser"]
412
+ end
413
+
414
+ subgraph downstream ["下流"]
415
+ mlCore["@markuplint/ml-core\n(MLASTDocument → MLDOM)"]
416
+ end
417
+
418
+ upstream -->|"トークン化、パース、型"| parser
419
+ parser -->|"MLASTDocument"| mlCore
420
+ ```
421
+
422
+ ### 上流
423
+
424
+ - **`pug-lexer`** -- Pug ソースをトークンストリームにトークン化
425
+ - **`pug-parser`** -- トークンストリームを生の Pug AST に変換
426
+ - **`@markuplint/html-parser`** -- `HtmlParser`(`HtmlInPugParser` が拡張)と名前空間解決用の `getNamespace()` を提供
427
+ - **`@markuplint/ml-ast`** -- パーサー全体で使用される AST 型定義
428
+ - **`@markuplint/parser-utils`** -- `PugParser` が拡張する抽象 `Parser` クラス、`ParserError`、`AttrState`、`scriptParser`、位置ユーティリティ
429
+
430
+ ### 下流
431
+
432
+ - **`@markuplint/ml-core`** -- `PugParser` が生成する `MLASTDocument` を消費して MLDOM を構築
433
+
434
+ ## ドキュメントマップ
435
+
436
+ - [メンテナンスガイド](docs/maintenance.ja.md) -- コマンド、レシピ、トラブルシューティング