@markuplint/html-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.
- package/ARCHITECTURE.ja.md +207 -0
- package/ARCHITECTURE.md +207 -0
- package/CHANGELOG.md +3 -3
- package/README.md +5 -0
- package/SKILL.md +120 -0
- package/docs/maintenance.ja.md +134 -0
- package/docs/maintenance.md +134 -0
- package/lib/get-namespace.d.ts +10 -0
- package/lib/get-namespace.js +10 -0
- package/lib/parser.d.ts +8 -0
- package/lib/parser.js +8 -0
- package/package.json +4 -4
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# @markuplint/html-parser
|
|
2
|
+
|
|
3
|
+
## 概要
|
|
4
|
+
|
|
5
|
+
`@markuplint/html-parser` は markuplint の標準 HTML パーサーです。parse5 の薄いラッパーとして構築されており、HTML ソースコードを統一された markuplint AST 形式(`MLASTDocument`)に変換します。完全なドキュメントと HTML フラグメントの両方を処理し、ゴースト要素(HTML 仕様により暗黙的に挿入されるタグ)の管理や、`<head>` / `<body>` タグパースの最適化を提供します。
|
|
6
|
+
|
|
7
|
+
## ディレクトリ構成
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/
|
|
11
|
+
├── index.ts — HtmlParser, parser, getNamespace を再エクスポート
|
|
12
|
+
├── parser.ts — Parser<Node, State> を拡張する HtmlParser クラス
|
|
13
|
+
├── types.ts — parse5 の型を再エクスポート(Node, Element 等)
|
|
14
|
+
├── get-namespace.ts — 名前空間 URI 解決(HTML/SVG/MathML)
|
|
15
|
+
├── is-document-fragment.ts — 正規表現によるフラグメント/ドキュメント判定
|
|
16
|
+
└── optimize-starts-head-or-body.ts — head/body タグのプレースホルダー最適化
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## アーキテクチャ図
|
|
20
|
+
|
|
21
|
+
```mermaid
|
|
22
|
+
flowchart TD
|
|
23
|
+
subgraph upstream ["上流"]
|
|
24
|
+
mlAst["@markuplint/ml-ast\n(AST 型定義)"]
|
|
25
|
+
parserUtils["@markuplint/parser-utils\n(抽象 Parser クラス)"]
|
|
26
|
+
parse5["parse5\n(HTML トークナイザ)"]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
subgraph pkg ["@markuplint/html-parser"]
|
|
30
|
+
htmlParser["HtmlParser\nextends Parser‹Node, State›"]
|
|
31
|
+
getNs["getNamespace()\n名前空間解決"]
|
|
32
|
+
isFragment["isDocumentFragment()\nフラグメント判定"]
|
|
33
|
+
optimize["optimizeStartsHeadTagOrBodyTag\nhead/body 最適化"]
|
|
34
|
+
types["types.ts\nparse5 型再エクスポート"]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
subgraph downstream ["下流パーサー"]
|
|
38
|
+
jsx["@markuplint/jsx-parser\n(HtmlParser を継承)"]
|
|
39
|
+
vue["@markuplint/vue-parser\n(HtmlParser をインポート)"]
|
|
40
|
+
svelte["@markuplint/svelte-parser\n(HtmlParser をインポート)"]
|
|
41
|
+
astro["@markuplint/astro-parser\n(HtmlParser をインポート)"]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
mlAst -->|"AST 型"| htmlParser
|
|
45
|
+
parserUtils -->|"Parser 基底クラス"| htmlParser
|
|
46
|
+
parse5 -->|"parse / parseFragment"| htmlParser
|
|
47
|
+
parse5 -->|"parseFragment"| getNs
|
|
48
|
+
|
|
49
|
+
htmlParser --> isFragment
|
|
50
|
+
htmlParser --> optimize
|
|
51
|
+
htmlParser --> getNs
|
|
52
|
+
|
|
53
|
+
htmlParser -->|"継承 / インポート"| downstream
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## HtmlParser クラス
|
|
57
|
+
|
|
58
|
+
### 継承関係
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
Parser<Node, State> (@markuplint/parser-utils)
|
|
62
|
+
└── HtmlParser (このパッケージ)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### State 型
|
|
66
|
+
|
|
67
|
+
パーサーは `State` 型を通じて内部状態を管理します:
|
|
68
|
+
|
|
69
|
+
| フィールド | 型 | 用途 |
|
|
70
|
+
| ------------------------ | --------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
71
|
+
| `startsHeadTagOrBodyTag` | `Replacements \| null` | ソースが `<head>` または `<body>` で始まる場合のプレースホルダー置換を追跡 |
|
|
72
|
+
| `afterPosition` | `{ endOffset, endLine, endCol, depth }` | 各深さレベルで最後に処理されたノードの終了位置を追跡。ゴースト要素の位置計算に使用 |
|
|
73
|
+
|
|
74
|
+
### オーバーライドメソッド
|
|
75
|
+
|
|
76
|
+
| メソッド | 用途 |
|
|
77
|
+
| ------------------- | ------------------------------------------------------------------------------------------------- |
|
|
78
|
+
| `tokenize()` | フラグメント判定に基づき parse5 の `parse()` または `parseFragment()` を呼び出す |
|
|
79
|
+
| `beforeParse()` | head/body 最適化のセットアップとオフセット追跡 |
|
|
80
|
+
| `afterParse()` | プレースホルダーから元の head/body タグ名を復元 |
|
|
81
|
+
| `nodeize()` | parse5 ノードを markuplint AST ノードに変換。ゴースト要素、テンプレートコンテンツ、名前空間を処理 |
|
|
82
|
+
| `afterNodeize()` | ゴースト要素の位置計算用に `afterPosition` 状態を更新 |
|
|
83
|
+
| `visitText()` | `researchTags: true` と `invalidTagAsText: true` で親に委譲 |
|
|
84
|
+
| `visitSpreadAttr()` | `null` を返す(HTML はスプレッド属性をサポートしない) |
|
|
85
|
+
|
|
86
|
+
## パースパイプライン
|
|
87
|
+
|
|
88
|
+
HTML 固有のパイプラインは基底 `Parser` のパイプラインを拡張します:
|
|
89
|
+
|
|
90
|
+
```mermaid
|
|
91
|
+
flowchart LR
|
|
92
|
+
A["beforeParse\n- super.beforeParse()\n- head/body 最適化セットアップ\n- オフセット追跡"]
|
|
93
|
+
B["tokenize\n- isDocumentFragment() 判定\n- parse5 parse/parseFragment"]
|
|
94
|
+
C["nodeize\n- ゴースト要素処理\n- Doctype/text/comment/element 振り分け\n- テンプレートコンテンツ抽出\n- 名前空間解決"]
|
|
95
|
+
D["afterNodeize\n- afterPosition 状態更新"]
|
|
96
|
+
E["afterParse\n- head/body 名の復元"]
|
|
97
|
+
|
|
98
|
+
A --> B --> C --> D --> E
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## ゴースト要素処理
|
|
102
|
+
|
|
103
|
+
parse5 が HTML をパースする際、HTML 仕様に従ってソースコードに存在しない要素を暗黙的に挿入することがあります。これらは **ゴースト要素** と呼ばれ、`<html>`、`<head>`、`<body>` のようにソース位置を持たない要素です。
|
|
104
|
+
|
|
105
|
+
### 検出
|
|
106
|
+
|
|
107
|
+
ゴースト要素は parse5 の出力で `sourceCodeLocation` を持たないことで識別されます(`!location`)。
|
|
108
|
+
|
|
109
|
+
### 位置計算
|
|
110
|
+
|
|
111
|
+
ゴースト要素にはソース位置がないため、パーサーは `afterPosition` 状態を使用して位置を計算します:
|
|
112
|
+
|
|
113
|
+
1. `afterNodeize()` が各深さレベルで処理済みノードの終了位置を記録
|
|
114
|
+
2. `nodeize()` がゴースト要素に遭遇した際、深さが一致すれば `afterPosition` を使用し、そうでなければ親ノードの位置にフォールバック
|
|
115
|
+
3. ゴースト要素は空の `raw` 文字列と計算された開始位置で作成
|
|
116
|
+
|
|
117
|
+
これにより、実際の要素のソースマッピングを崩すことなく、ゴースト要素が AST 内で正しく配置されます。
|
|
118
|
+
|
|
119
|
+
## Head/Body タグ最適化
|
|
120
|
+
|
|
121
|
+
### 問題
|
|
122
|
+
|
|
123
|
+
HTML ソースが `<head>` または `<body>` で始まる場合(前に `<html>` タグがない場合)、parse5 はこれらをリテラルにパースするのではなく暗黙的な構造タグとして扱います。これにより不正な AST 出力が発生します。
|
|
124
|
+
|
|
125
|
+
### 解決策
|
|
126
|
+
|
|
127
|
+
この最適化はプレースホルダー置換戦略を使用します:
|
|
128
|
+
|
|
129
|
+
1. **セットアップ**(`optimizeStartsHeadTagOrBodyTagSetup`): ソースが `<head>` または `<body>` で始まるかを検出。該当する場合、すべての `head`/`body` タグ名をユニークなプレースホルダー名(`x-\uFFFDh` / `x-\uFFFDb`)に置換し、元の名前を記録
|
|
130
|
+
2. **パース**: parse5 がプレースホルダータグ名の修正済みソースをカスタム要素として扱いパース
|
|
131
|
+
3. **復元**(`optimizeStartsHeadTagOrBodyTagResume`): パース後、`parser.updateRaw()` と `parser.updateElement()` を使用して AST 内の元のタグ名を復元
|
|
132
|
+
|
|
133
|
+
## 名前空間解決
|
|
134
|
+
|
|
135
|
+
`getNamespace()` は要素の名前空間 URI を決定します:
|
|
136
|
+
|
|
137
|
+
- **デフォルト**: `http://www.w3.org/1999/xhtml`(HTML 名前空間)
|
|
138
|
+
- **SVG コンテキスト**: 親の名前空間が `http://www.w3.org/2000/svg` の場合、タグを `<svg>` で囲んでパースし解決された名前空間を判定
|
|
139
|
+
- **MathML コンテキスト**: 親の名前空間が `http://www.w3.org/1998/Math/MathML` の場合、`<math>` で囲んでパース
|
|
140
|
+
- **フォールバック**: フラグメントとしてノードが生成されないタグの場合、`parse()`(フルドキュメントモード)にフォールバック
|
|
141
|
+
|
|
142
|
+
## フラグメント vs ドキュメント判定
|
|
143
|
+
|
|
144
|
+
`isDocumentFragment()` は正規表現を使用して、入力をフラグメントとしてパースするかフルドキュメントとしてパースするかを判定します:
|
|
145
|
+
|
|
146
|
+
- **ドキュメント**: 入力が `<!doctype html...>` または `<html` で始まる
|
|
147
|
+
- **フラグメント**: それ以外すべて
|
|
148
|
+
|
|
149
|
+
この区別は重要です。parse5 の `parse()` はフルドキュメントパースアルゴリズムを適用し(暗黙の `<html>`、`<head>`、`<body>` を挿入)、`parseFragment()` はコンテンツをそのままパースするためです。
|
|
150
|
+
|
|
151
|
+
## 外部依存
|
|
152
|
+
|
|
153
|
+
| 依存パッケージ | 用途 |
|
|
154
|
+
| -------------------------- | ------------------------------------------------------------------- |
|
|
155
|
+
| `@markuplint/ml-ast` | AST 型定義(`MLASTNodeTreeItem`、`MLASTParentNode` 等) |
|
|
156
|
+
| `@markuplint/parser-utils` | 抽象 `Parser` クラス、`ChildToken`、`ParseOptions`、`ParserOptions` |
|
|
157
|
+
| `parse5` | HTML パース(`parse`、`parseFragment`、`DefaultTreeAdapterMap`) |
|
|
158
|
+
| `type-fest` | TypeScript ユーティリティ型 |
|
|
159
|
+
|
|
160
|
+
## 統合ポイント
|
|
161
|
+
|
|
162
|
+
```mermaid
|
|
163
|
+
flowchart TD
|
|
164
|
+
subgraph upstream ["上流"]
|
|
165
|
+
mlAst["@markuplint/ml-ast\n(AST 型定義)"]
|
|
166
|
+
parserUtils["@markuplint/parser-utils\n(Parser 基底クラス)"]
|
|
167
|
+
parse5["parse5\n(HTML トークナイザ)"]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
subgraph pkg ["@markuplint/html-parser"]
|
|
171
|
+
htmlParser["HtmlParser"]
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
subgraph downstream ["下流"]
|
|
175
|
+
jsx["@markuplint/jsx-parser"]
|
|
176
|
+
vue["@markuplint/vue-parser"]
|
|
177
|
+
svelte["@markuplint/svelte-parser"]
|
|
178
|
+
astro["@markuplint/astro-parser"]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
subgraph indirect ["間接的"]
|
|
182
|
+
mlCore["@markuplint/ml-core\n(MLASTDocument → MLDOM)"]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
upstream -->|"型、パース"| htmlParser
|
|
186
|
+
htmlParser -->|"継承 / インポート"| downstream
|
|
187
|
+
downstream -->|"MLASTDocument を生成"| mlCore
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 上流
|
|
191
|
+
|
|
192
|
+
- **`@markuplint/ml-ast`** -- パーサー全体で使用される AST 型定義
|
|
193
|
+
- **`@markuplint/parser-utils`** -- `HtmlParser` が拡張する抽象 `Parser` クラスとユーティリティ型
|
|
194
|
+
- **`parse5`** -- トークン化とツリー構築を行う基盤 HTML パーサー
|
|
195
|
+
|
|
196
|
+
### 下流
|
|
197
|
+
|
|
198
|
+
4つのパーサーパッケージが `HtmlParser` に依存しています:
|
|
199
|
+
|
|
200
|
+
- **`@markuplint/jsx-parser`** -- `HtmlParser` を継承して JSX サポートを追加
|
|
201
|
+
- **`@markuplint/vue-parser`** -- Vue SFC の HTML 部分に `HtmlParser` をインポート
|
|
202
|
+
- **`@markuplint/svelte-parser`** -- Svelte コンポーネントの HTML 部分に `HtmlParser` をインポート
|
|
203
|
+
- **`@markuplint/astro-parser`** -- Astro コンポーネントの HTML 部分に `HtmlParser` をインポート
|
|
204
|
+
|
|
205
|
+
## ドキュメントマップ
|
|
206
|
+
|
|
207
|
+
- [メンテナンスガイド](docs/maintenance.ja.md) -- コマンド、レシピ、トラブルシューティング
|
package/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# @markuplint/html-parser
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`@markuplint/html-parser` is the standard HTML parser for markuplint. Built as a thin wrapper around parse5, it converts HTML source code into the unified markuplint AST format (`MLASTDocument`). The package handles both full documents and HTML fragments, manages ghost elements (tags implicitly inserted by the HTML spec), and provides optimizations for `<head>` / `<body>` tag parsing.
|
|
6
|
+
|
|
7
|
+
## Directory Structure
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/
|
|
11
|
+
├── index.ts — Re-exports HtmlParser, parser, getNamespace
|
|
12
|
+
├── parser.ts — HtmlParser class extending Parser<Node, State>
|
|
13
|
+
├── types.ts — Re-exports parse5 types (Node, Element, etc.)
|
|
14
|
+
├── get-namespace.ts — Namespace URI resolution (HTML/SVG/MathML)
|
|
15
|
+
├── is-document-fragment.ts — Regex-based fragment vs document detection
|
|
16
|
+
└── optimize-starts-head-or-body.ts — Head/body tag placeholder optimization
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Architecture Diagram
|
|
20
|
+
|
|
21
|
+
```mermaid
|
|
22
|
+
flowchart TD
|
|
23
|
+
subgraph upstream ["Upstream"]
|
|
24
|
+
mlAst["@markuplint/ml-ast\n(AST types)"]
|
|
25
|
+
parserUtils["@markuplint/parser-utils\n(Abstract Parser class)"]
|
|
26
|
+
parse5["parse5\n(HTML tokenizer)"]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
subgraph pkg ["@markuplint/html-parser"]
|
|
30
|
+
htmlParser["HtmlParser\nextends Parser‹Node, State›"]
|
|
31
|
+
getNs["getNamespace()\nNamespace resolution"]
|
|
32
|
+
isFragment["isDocumentFragment()\nFragment detection"]
|
|
33
|
+
optimize["optimizeStartsHeadTagOrBodyTag\nHead/body optimization"]
|
|
34
|
+
types["types.ts\nparse5 type re-exports"]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
subgraph downstream ["Downstream Parsers"]
|
|
38
|
+
jsx["@markuplint/jsx-parser\n(extends HtmlParser)"]
|
|
39
|
+
vue["@markuplint/vue-parser\n(imports HtmlParser)"]
|
|
40
|
+
svelte["@markuplint/svelte-parser\n(imports HtmlParser)"]
|
|
41
|
+
astro["@markuplint/astro-parser\n(imports HtmlParser)"]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
mlAst -->|"AST types"| htmlParser
|
|
45
|
+
parserUtils -->|"Parser base class"| htmlParser
|
|
46
|
+
parse5 -->|"parse / parseFragment"| htmlParser
|
|
47
|
+
parse5 -->|"parseFragment"| getNs
|
|
48
|
+
|
|
49
|
+
htmlParser --> isFragment
|
|
50
|
+
htmlParser --> optimize
|
|
51
|
+
htmlParser --> getNs
|
|
52
|
+
|
|
53
|
+
htmlParser -->|"extends / imports"| downstream
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## HtmlParser Class
|
|
57
|
+
|
|
58
|
+
### Inheritance
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
Parser<Node, State> (from @markuplint/parser-utils)
|
|
62
|
+
└── HtmlParser (this package)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### State Type
|
|
66
|
+
|
|
67
|
+
The parser maintains internal state through the `State` type:
|
|
68
|
+
|
|
69
|
+
| Field | Type | Purpose |
|
|
70
|
+
| ------------------------ | --------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
71
|
+
| `startsHeadTagOrBodyTag` | `Replacements \| null` | Tracks head/body placeholder replacements when the source starts with `<head>` or `<body>` |
|
|
72
|
+
| `afterPosition` | `{ endOffset, endLine, endCol, depth }` | Tracks the end position of the last processed node at each depth, used for ghost element positioning |
|
|
73
|
+
|
|
74
|
+
### Override Methods
|
|
75
|
+
|
|
76
|
+
| Method | Purpose |
|
|
77
|
+
| ------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
78
|
+
| `tokenize()` | Invokes parse5 `parse()` or `parseFragment()` based on fragment detection |
|
|
79
|
+
| `beforeParse()` | Sets up head/body optimization and offset tracking |
|
|
80
|
+
| `afterParse()` | Restores original head/body tag names from placeholders |
|
|
81
|
+
| `nodeize()` | Converts parse5 nodes to markuplint AST nodes, handling ghost elements, template content, and namespaces |
|
|
82
|
+
| `afterNodeize()` | Updates `afterPosition` state for ghost element positioning |
|
|
83
|
+
| `visitText()` | Delegates to parent with `researchTags: true` and `invalidTagAsText: true` |
|
|
84
|
+
| `visitSpreadAttr()` | Returns `null` (HTML does not support spread attributes) |
|
|
85
|
+
|
|
86
|
+
## Parse Pipeline
|
|
87
|
+
|
|
88
|
+
The HTML-specific pipeline extends the base `Parser` pipeline:
|
|
89
|
+
|
|
90
|
+
```mermaid
|
|
91
|
+
flowchart LR
|
|
92
|
+
A["beforeParse\n- super.beforeParse()\n- head/body optimization setup\n- offset tracking"]
|
|
93
|
+
B["tokenize\n- isDocumentFragment() check\n- parse5 parse/parseFragment"]
|
|
94
|
+
C["nodeize\n- Ghost element handling\n- Doctype/text/comment/element dispatch\n- Template content extraction\n- Namespace resolution"]
|
|
95
|
+
D["afterNodeize\n- Update afterPosition state"]
|
|
96
|
+
E["afterParse\n- Restore head/body names"]
|
|
97
|
+
|
|
98
|
+
A --> B --> C --> D --> E
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Ghost Element Handling
|
|
102
|
+
|
|
103
|
+
When parse5 parses HTML, it follows the HTML specification and may implicitly insert elements that are not present in the source code. These are called **ghost elements** — elements like `<html>`, `<head>`, and `<body>` that have no corresponding source location.
|
|
104
|
+
|
|
105
|
+
### Detection
|
|
106
|
+
|
|
107
|
+
Ghost elements are identified by having no `sourceCodeLocation` in the parse5 output (`!location`).
|
|
108
|
+
|
|
109
|
+
### Position Calculation
|
|
110
|
+
|
|
111
|
+
Since ghost elements have no source position, the parser calculates their position using the `afterPosition` state:
|
|
112
|
+
|
|
113
|
+
1. `afterNodeize()` records the end position of each processed node at its depth level
|
|
114
|
+
2. When `nodeize()` encounters a ghost element, it uses `afterPosition` if the depth matches, otherwise falls back to the parent node's position
|
|
115
|
+
3. The ghost element is created with an empty `raw` string and the calculated start position
|
|
116
|
+
|
|
117
|
+
This ensures ghost elements are positioned correctly in the AST without disrupting the source mapping of real elements.
|
|
118
|
+
|
|
119
|
+
## Head/Body Tag Optimization
|
|
120
|
+
|
|
121
|
+
### Problem
|
|
122
|
+
|
|
123
|
+
When HTML source starts with `<head>` or `<body>` (without a preceding `<html>` tag), parse5 treats them as implicit structural tags rather than parsing them literally. This causes incorrect AST output.
|
|
124
|
+
|
|
125
|
+
### Solution
|
|
126
|
+
|
|
127
|
+
The optimization uses a placeholder replacement strategy:
|
|
128
|
+
|
|
129
|
+
1. **Setup** (`optimizeStartsHeadTagOrBodyTagSetup`): Detects if the source starts with `<head>` or `<body>`. If so, replaces all `head`/`body` tag names with unique placeholder names (`x-\uFFFDh` / `x-\uFFFDb`) and records the original names
|
|
130
|
+
2. **Parse**: parse5 parses the modified source with placeholder tag names, treating them as custom elements
|
|
131
|
+
3. **Resume** (`optimizeStartsHeadTagOrBodyTagResume`): After parsing, restores the original tag names in the AST using `parser.updateRaw()` and `parser.updateElement()`
|
|
132
|
+
|
|
133
|
+
## Namespace Resolution
|
|
134
|
+
|
|
135
|
+
`getNamespace()` determines the namespace URI for an element:
|
|
136
|
+
|
|
137
|
+
- **Default**: `http://www.w3.org/1999/xhtml` (HTML namespace)
|
|
138
|
+
- **SVG context**: When the parent namespace is `http://www.w3.org/2000/svg`, wraps the tag in `<svg>` and parses to determine the resolved namespace
|
|
139
|
+
- **MathML context**: When the parent namespace is `http://www.w3.org/1998/Math/MathML`, wraps in `<math>` and parses
|
|
140
|
+
- **Fallback**: For tags that produce no nodes as fragments, falls back to `parse()` (full document mode)
|
|
141
|
+
|
|
142
|
+
## Fragment vs Document Detection
|
|
143
|
+
|
|
144
|
+
`isDocumentFragment()` uses a regex to determine whether the input should be parsed as a fragment or a full document:
|
|
145
|
+
|
|
146
|
+
- **Document**: Input starts with `<!doctype html...>` or `<html`
|
|
147
|
+
- **Fragment**: Everything else
|
|
148
|
+
|
|
149
|
+
This distinction matters because parse5's `parse()` applies the full document parsing algorithm (inserting implicit `<html>`, `<head>`, `<body>`), while `parseFragment()` parses content as-is.
|
|
150
|
+
|
|
151
|
+
## External Dependencies
|
|
152
|
+
|
|
153
|
+
| Dependency | Purpose |
|
|
154
|
+
| -------------------------- | ---------------------------------------------------------------------- |
|
|
155
|
+
| `@markuplint/ml-ast` | AST type definitions (`MLASTNodeTreeItem`, `MLASTParentNode`, etc.) |
|
|
156
|
+
| `@markuplint/parser-utils` | Abstract `Parser` class, `ChildToken`, `ParseOptions`, `ParserOptions` |
|
|
157
|
+
| `parse5` | HTML parsing (`parse`, `parseFragment`, `DefaultTreeAdapterMap`) |
|
|
158
|
+
| `type-fest` | TypeScript utility types |
|
|
159
|
+
|
|
160
|
+
## Integration Points
|
|
161
|
+
|
|
162
|
+
```mermaid
|
|
163
|
+
flowchart TD
|
|
164
|
+
subgraph upstream ["Upstream"]
|
|
165
|
+
mlAst["@markuplint/ml-ast\n(AST types)"]
|
|
166
|
+
parserUtils["@markuplint/parser-utils\n(Parser base class)"]
|
|
167
|
+
parse5["parse5\n(HTML tokenizer)"]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
subgraph pkg ["@markuplint/html-parser"]
|
|
171
|
+
htmlParser["HtmlParser"]
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
subgraph downstream ["Downstream"]
|
|
175
|
+
jsx["@markuplint/jsx-parser"]
|
|
176
|
+
vue["@markuplint/vue-parser"]
|
|
177
|
+
svelte["@markuplint/svelte-parser"]
|
|
178
|
+
astro["@markuplint/astro-parser"]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
subgraph indirect ["Indirect Downstream"]
|
|
182
|
+
mlCore["@markuplint/ml-core\n(MLASTDocument → MLDOM)"]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
upstream -->|"types, parsing"| htmlParser
|
|
186
|
+
htmlParser -->|"extends / imports"| downstream
|
|
187
|
+
downstream -->|"produces MLASTDocument"| mlCore
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Upstream
|
|
191
|
+
|
|
192
|
+
- **`@markuplint/ml-ast`** -- AST type definitions used throughout the parser
|
|
193
|
+
- **`@markuplint/parser-utils`** -- Abstract `Parser` class that `HtmlParser` extends, plus utility types
|
|
194
|
+
- **`parse5`** -- The underlying HTML parser that performs tokenization and tree construction
|
|
195
|
+
|
|
196
|
+
### Downstream
|
|
197
|
+
|
|
198
|
+
Four parser packages depend on `HtmlParser`:
|
|
199
|
+
|
|
200
|
+
- **`@markuplint/jsx-parser`** -- Extends `HtmlParser` to add JSX support
|
|
201
|
+
- **`@markuplint/vue-parser`** -- Imports `HtmlParser` for HTML portions of Vue SFCs
|
|
202
|
+
- **`@markuplint/svelte-parser`** -- Imports `HtmlParser` for HTML portions of Svelte components
|
|
203
|
+
- **`@markuplint/astro-parser`** -- Imports `HtmlParser` for HTML portions of Astro components
|
|
204
|
+
|
|
205
|
+
## Documentation Map
|
|
206
|
+
|
|
207
|
+
- [Maintenance Guide](docs/maintenance.md) -- Commands, recipes, and troubleshooting
|
package/CHANGELOG.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
-
## [4.6.
|
|
6
|
+
## [4.6.23](https://github.com/markuplint/markuplint/compare/@markuplint/html-parser@4.6.22...@markuplint/html-parser@4.6.23) (2026-02-10)
|
|
7
7
|
|
|
8
8
|
**Note:** Version bump only for package @markuplint/html-parser
|
|
9
9
|
|
|
10
|
+
## [4.6.22](https://github.com/markuplint/markuplint/compare/@markuplint/html-parser@4.6.21...@markuplint/html-parser@4.6.22) (2025-11-05)
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
**Note:** Version bump only for package @markuplint/html-parser
|
|
13
13
|
|
|
14
14
|
## [4.6.21](https://github.com/markuplint/markuplint/compare/@markuplint/html-parser@4.6.20...@markuplint/html-parser@4.6.21) (2025-08-24)
|
|
15
15
|
|
package/README.md
CHANGED
|
@@ -16,3 +16,8 @@ $ yarn add @markuplint/html-parser
|
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
</details>
|
|
19
|
+
|
|
20
|
+
## Documentation
|
|
21
|
+
|
|
22
|
+
- [Architecture](ARCHITECTURE.md) ([日本語](ARCHITECTURE.ja.md)) — Package overview, parse5 integration, and ghost element handling
|
|
23
|
+
- [Maintenance Guide](docs/maintenance.md) ([日本語](docs/maintenance.ja.md)) — Commands, recipes, and troubleshooting
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Perform maintenance tasks for @markuplint/html-parser
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# html-parser-maintenance
|
|
6
|
+
|
|
7
|
+
Perform maintenance tasks for `@markuplint/html-parser`: modify HtmlParser override methods,
|
|
8
|
+
add namespaces, fix ghost element handling, and update head/body optimization.
|
|
9
|
+
|
|
10
|
+
## Input
|
|
11
|
+
|
|
12
|
+
`$ARGUMENTS` specifies the task. Supported tasks:
|
|
13
|
+
|
|
14
|
+
| Task | Description |
|
|
15
|
+
| --------------------- | ------------------------------------- |
|
|
16
|
+
| `modify-override` | Modify an HtmlParser override method |
|
|
17
|
+
| `add-namespace` | Add a new namespace to getNamespace() |
|
|
18
|
+
| `fix-ghost-element` | Fix ghost element position handling |
|
|
19
|
+
| `update-optimization` | Update head/body tag optimization |
|
|
20
|
+
|
|
21
|
+
If omitted, defaults to `modify-override`.
|
|
22
|
+
|
|
23
|
+
## Reference
|
|
24
|
+
|
|
25
|
+
Before executing any task, read `docs/maintenance.md` (or `docs/maintenance.ja.md`)
|
|
26
|
+
for the full guide. The recipes there are the source of truth for procedures.
|
|
27
|
+
|
|
28
|
+
Also read:
|
|
29
|
+
|
|
30
|
+
- `ARCHITECTURE.md` -- Package overview, parse5 integration, and ghost element handling
|
|
31
|
+
- `src/parser.ts` -- HtmlParser class (source of truth for override methods)
|
|
32
|
+
|
|
33
|
+
## Task: modify-override
|
|
34
|
+
|
|
35
|
+
Modify an HtmlParser override method. Follow recipe #1 in `docs/maintenance.md`.
|
|
36
|
+
|
|
37
|
+
### Step 1: Understand the method
|
|
38
|
+
|
|
39
|
+
1. Read `src/parser.ts` and identify the override method to change
|
|
40
|
+
2. Read the base `Parser` class in `@markuplint/parser-utils` to understand the parent behavior
|
|
41
|
+
|
|
42
|
+
### Step 2: Make the change
|
|
43
|
+
|
|
44
|
+
1. Preserve `super.*()` calls where required:
|
|
45
|
+
- `beforeParse()` — must call `super.beforeParse()` first
|
|
46
|
+
- `afterParse()` — must call `super.afterParse()` first
|
|
47
|
+
- `afterNodeize()` — must call `super.afterNodeize()` first
|
|
48
|
+
2. Maintain state updates if the method interacts with `this.state`
|
|
49
|
+
|
|
50
|
+
### Step 3: Verify
|
|
51
|
+
|
|
52
|
+
1. Build: `yarn build --scope @markuplint/html-parser`
|
|
53
|
+
2. Test: `yarn test --scope @markuplint/html-parser`
|
|
54
|
+
3. Run downstream tests (jsx-parser, vue-parser, svelte-parser, astro-parser)
|
|
55
|
+
|
|
56
|
+
## Task: add-namespace
|
|
57
|
+
|
|
58
|
+
Add a new namespace to `getNamespace()`. Follow recipe #3 in `docs/maintenance.md`.
|
|
59
|
+
|
|
60
|
+
### Step 1: Add the namespace case
|
|
61
|
+
|
|
62
|
+
1. Read `src/get-namespace.ts`
|
|
63
|
+
2. Add a new `case` in the `switch (parentNamespace)` block
|
|
64
|
+
3. Choose an appropriate wrapper element for the new namespace
|
|
65
|
+
|
|
66
|
+
### Step 2: Verify
|
|
67
|
+
|
|
68
|
+
1. Build: `yarn build --scope @markuplint/html-parser`
|
|
69
|
+
2. Add test cases to `src/get-namespace.spec.ts`
|
|
70
|
+
3. Test: `yarn test --scope @markuplint/html-parser`
|
|
71
|
+
|
|
72
|
+
## Task: fix-ghost-element
|
|
73
|
+
|
|
74
|
+
Fix ghost element position handling. Follow recipe #2 in `docs/maintenance.md`.
|
|
75
|
+
|
|
76
|
+
### Step 1: Understand the issue
|
|
77
|
+
|
|
78
|
+
1. Read the `nodeize()` method in `src/parser.ts` — the `if (!location)` block
|
|
79
|
+
2. Read `afterNodeize()` to understand `afterPosition` state tracking
|
|
80
|
+
|
|
81
|
+
### Step 2: Fix the position calculation
|
|
82
|
+
|
|
83
|
+
1. Check the depth comparison: `this.state.afterPosition.depth === depth`
|
|
84
|
+
2. Check the fallback to `parentNode` end position
|
|
85
|
+
3. Verify `afterNodeize()` correctly updates `this.state.afterPosition`
|
|
86
|
+
|
|
87
|
+
### Step 3: Verify
|
|
88
|
+
|
|
89
|
+
1. Build: `yarn build --scope @markuplint/html-parser`
|
|
90
|
+
2. Test with HTML that triggers ghost elements
|
|
91
|
+
3. Test: `yarn test --scope @markuplint/html-parser`
|
|
92
|
+
|
|
93
|
+
## Task: update-optimization
|
|
94
|
+
|
|
95
|
+
Update head/body tag optimization. Follow recipe #4 in `docs/maintenance.md`.
|
|
96
|
+
|
|
97
|
+
### Step 1: Understand the current optimization
|
|
98
|
+
|
|
99
|
+
1. Read `src/optimize-starts-head-or-body.ts`
|
|
100
|
+
2. Understand the three functions: detection, setup, and resume
|
|
101
|
+
|
|
102
|
+
### Step 2: Make the change
|
|
103
|
+
|
|
104
|
+
1. The placeholder character `\uFFFD` must remain unique
|
|
105
|
+
2. The `replaceAll` regex must match both opening and closing tags
|
|
106
|
+
3. Restoration must handle both `starttag` and `endtag` node types
|
|
107
|
+
|
|
108
|
+
### Step 3: Verify
|
|
109
|
+
|
|
110
|
+
1. Build: `yarn build --scope @markuplint/html-parser`
|
|
111
|
+
2. Test: `yarn test --scope @markuplint/html-parser`
|
|
112
|
+
3. Add or update test cases in `src/optimize-starts-head-or-body.spec.ts`
|
|
113
|
+
|
|
114
|
+
## Rules
|
|
115
|
+
|
|
116
|
+
1. **Always call `super.beforeParse()`, `super.afterParse()`, `super.afterNodeize()`** when overriding these methods.
|
|
117
|
+
2. **Never call `super.tokenize()` or `super.nodeize()`** — the parent defaults return empty arrays.
|
|
118
|
+
3. **Maintain `afterPosition` state** — ghost element positioning depends on it.
|
|
119
|
+
4. **Test across all downstream parsers** when modifying `HtmlParser` (jsx-parser, vue-parser, svelte-parser, astro-parser).
|
|
120
|
+
5. **Add JSDoc comments** to all new public methods and properties.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# メンテナンスガイド
|
|
2
|
+
|
|
3
|
+
## コマンド
|
|
4
|
+
|
|
5
|
+
| コマンド | 説明 |
|
|
6
|
+
| -------------------------------------------- | ---------------------- |
|
|
7
|
+
| `yarn build --scope @markuplint/html-parser` | このパッケージをビルド |
|
|
8
|
+
| `yarn dev --scope @markuplint/html-parser` | ウォッチモードでビルド |
|
|
9
|
+
| `yarn clean --scope @markuplint/html-parser` | ビルド成果物を削除 |
|
|
10
|
+
| `yarn test --scope @markuplint/html-parser` | テストを実行 |
|
|
11
|
+
|
|
12
|
+
## テスト
|
|
13
|
+
|
|
14
|
+
テストファイルは `*.spec.ts` の命名規則に従い、`src/` ディレクトリに配置されています:
|
|
15
|
+
|
|
16
|
+
| テストファイル | カバレッジ |
|
|
17
|
+
| -------------------------------------- | ---------------------------------------------------------------- |
|
|
18
|
+
| `index.spec.ts` | HtmlParser 統合テスト(HTML ドキュメントとフラグメントのパース) |
|
|
19
|
+
| `get-namespace.spec.ts` | HTML、SVG、MathML 要素の名前空間解決 |
|
|
20
|
+
| `optimize-starts-head-or-body.spec.ts` | head/body タグ最適化のセットアップと復元 |
|
|
21
|
+
|
|
22
|
+
主なテストパターンでは `nodeListToDebugMaps` を使用したスナップショット形式のアサーションを行います:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { nodeListToDebugMaps } from '@markuplint/parser-utils';
|
|
26
|
+
import { parser } from '@markuplint/html-parser';
|
|
27
|
+
|
|
28
|
+
const doc = parser.parse('<div class="foo">text</div>');
|
|
29
|
+
const debugMaps = nodeListToDebugMaps(doc.nodeList, true);
|
|
30
|
+
expect(debugMaps).toStrictEqual([
|
|
31
|
+
// 期待されるデバッグ出力
|
|
32
|
+
]);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## レシピ
|
|
36
|
+
|
|
37
|
+
### 1. HtmlParser のオーバーライドメソッド変更
|
|
38
|
+
|
|
39
|
+
1. `src/parser.ts` を読み、変更するオーバーライドメソッドを特定
|
|
40
|
+
2. `@markuplint/parser-utils` の基底 `Parser` クラスを確認し、親の動作を理解
|
|
41
|
+
3. 変更を行い、必要な箇所で `super.*()` 呼び出しが保持されていることを確認:
|
|
42
|
+
- `beforeParse()` — 最初に `super.beforeParse()` を呼び出す必要あり
|
|
43
|
+
- `afterParse()` — 最初に `super.afterParse()` を呼び出す必要あり
|
|
44
|
+
- `afterNodeize()` — 最初に `super.afterNodeize()` を呼び出す必要あり
|
|
45
|
+
- `visitText()` — オプション付きで `super.visitText()` を呼び出す
|
|
46
|
+
4. ビルド: `yarn build --scope @markuplint/html-parser`
|
|
47
|
+
5. テスト実行: `yarn test --scope @markuplint/html-parser`
|
|
48
|
+
6. 下流への影響を確認(下記チェックリスト参照)
|
|
49
|
+
|
|
50
|
+
### 2. ゴースト要素処理の変更
|
|
51
|
+
|
|
52
|
+
1. `src/parser.ts` の `nodeize()` メソッドを読む — ゴースト要素のブランチは `if (!location)` ブロック
|
|
53
|
+
2. `afterNodeize()` メソッドを読み、`afterPosition` 状態がどのように維持されているかを理解
|
|
54
|
+
3. 位置計算または要素作成ロジックを変更
|
|
55
|
+
4. ビルドとテスト: `yarn build --scope @markuplint/html-parser && yarn test --scope @markuplint/html-parser`
|
|
56
|
+
5. ゴースト要素を発生させる HTML でテスト(例: `<div>text</div>` をドキュメントとしてパース — ゴーストの `<html>`、`<head>`、`<body>` が作成される)
|
|
57
|
+
|
|
58
|
+
### 3. 新しい名前空間の追加
|
|
59
|
+
|
|
60
|
+
1. `src/get-namespace.ts` を読む
|
|
61
|
+
2. `switch (parentNamespace)` ブロックに新しい名前空間 URI の `case` を追加
|
|
62
|
+
3. 新しい名前空間に適切なラッパー要素を選択
|
|
63
|
+
4. ビルドとテスト: `yarn build --scope @markuplint/html-parser && yarn test --scope @markuplint/html-parser`
|
|
64
|
+
5. `src/get-namespace.spec.ts` にテストケースを追加
|
|
65
|
+
|
|
66
|
+
### 4. Head/Body 最適化の変更
|
|
67
|
+
|
|
68
|
+
1. `src/optimize-starts-head-or-body.ts` を読む
|
|
69
|
+
2. このモジュールには3つの主要関数がある:
|
|
70
|
+
- `isStartsHeadTagOrBodyTag()` — 検出用正規表現
|
|
71
|
+
- `optimizeStartsHeadTagOrBodyTagSetup()` — プレースホルダー置換
|
|
72
|
+
- `optimizeStartsHeadTagOrBodyTagResume()` — 名前の復元
|
|
73
|
+
3. 変更時の注意点:
|
|
74
|
+
- プレースホルダー文字 `\uFFFD`(Unicode Replacement Character)はユニークである必要がある
|
|
75
|
+
- `replaceAll` の正規表現は開始タグと閉じタグの両方にマッチする必要がある
|
|
76
|
+
- 復元は `starttag` と `endtag` の両方のノードタイプを処理する必要がある
|
|
77
|
+
4. ビルドとテスト: `yarn build --scope @markuplint/html-parser && yarn test --scope @markuplint/html-parser`
|
|
78
|
+
5. `src/optimize-starts-head-or-body.spec.ts` にテストケースを追加または更新
|
|
79
|
+
|
|
80
|
+
## 下流影響チェックリスト
|
|
81
|
+
|
|
82
|
+
このパッケージへの変更は、下流の4つのパーサーパッケージに影響を与える可能性があります:
|
|
83
|
+
|
|
84
|
+
| パッケージ | 関係 | 主な依存 |
|
|
85
|
+
| --------------------------- | ------------------------- | -------------------------------------------------- |
|
|
86
|
+
| `@markuplint/jsx-parser` | `HtmlParser` を継承 | 全オーバーライドメソッド、コンストラクタオプション |
|
|
87
|
+
| `@markuplint/vue-parser` | `HtmlParser` をインポート | `tokenize()`、`nodeize()` |
|
|
88
|
+
| `@markuplint/svelte-parser` | `HtmlParser` をインポート | `tokenize()`、`nodeize()` |
|
|
89
|
+
| `@markuplint/astro-parser` | `HtmlParser` をインポート | `tokenize()`、`nodeize()` |
|
|
90
|
+
|
|
91
|
+
`HtmlParser` を変更する際は、必ず下流パーサーのテストも実行してください:
|
|
92
|
+
|
|
93
|
+
```shell
|
|
94
|
+
yarn test --scope @markuplint/html-parser --scope @markuplint/jsx-parser \
|
|
95
|
+
--scope @markuplint/vue-parser --scope @markuplint/svelte-parser \
|
|
96
|
+
--scope @markuplint/astro-parser
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## トラブルシューティング
|
|
100
|
+
|
|
101
|
+
### ゴースト要素の位置がおかしい
|
|
102
|
+
|
|
103
|
+
**症状:** ゴースト要素(`<html>`、`<head>`、`<body>`)の行/列/オフセット値が AST 内で不正。
|
|
104
|
+
|
|
105
|
+
**原因:** `afterPosition` 状態が正しく更新されていないか、`nodeize()` 内の深さチェックが誤っている。
|
|
106
|
+
|
|
107
|
+
**解決策:**
|
|
108
|
+
|
|
109
|
+
1. `afterNodeize()` を確認 — `this.state.afterPosition` が正しい `endOffset`、`endLine`、`endCol`、`depth` で更新されていることを確認
|
|
110
|
+
2. `nodeize()` のゴースト要素ブランチを確認 — `depth === this.state.afterPosition.depth` の比較が正しいことを確認
|
|
111
|
+
|
|
112
|
+
### Head/body タグのパースで予期しない結果が出る
|
|
113
|
+
|
|
114
|
+
**症状:** ソースが `<head>` または `<body>` で始まる場合、パースされた AST にプレースホルダー名が残る、または要素が欠落する。
|
|
115
|
+
|
|
116
|
+
**原因:** 最適化のセットアップまたは復元ステップにバグがある。
|
|
117
|
+
|
|
118
|
+
**解決策:**
|
|
119
|
+
|
|
120
|
+
1. `isStartsHeadTagOrBodyTag()` を確認 — 検出用正規表現が入力にマッチすることを確認
|
|
121
|
+
2. `optimizeStartsHeadTagOrBodyTagSetup()` を確認 — プレースホルダー名が正しく生成されていることを検証
|
|
122
|
+
3. `optimizeStartsHeadTagOrBodyTagResume()` を確認 — 開始タグと終了タグの両方で元の名前が復元されていることを検証
|
|
123
|
+
|
|
124
|
+
### 名前空間解決が誤った値を返す
|
|
125
|
+
|
|
126
|
+
**症状:** SVG または MathML 要素に間違った名前空間 URI が割り当てられる。
|
|
127
|
+
|
|
128
|
+
**原因:** 親の名前空間コンテキストが正しく渡されていないか、parse5 が期待と異なる名前空間解決をしている。
|
|
129
|
+
|
|
130
|
+
**解決策:**
|
|
131
|
+
|
|
132
|
+
1. 呼び出し元のコードを確認 — `nodeize()` 内で `originNode.namespaceURI` が正しく読み取られていることを確認
|
|
133
|
+
2. `getNamespace()` を確認 — 特定のタグ名と親名前空間の組み合わせでテストケースを追加
|
|
134
|
+
3. parse5 がインテグレーションポイントルールを適用して名前空間を変更する場合があることに注意
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Maintenance Guide
|
|
2
|
+
|
|
3
|
+
## Commands
|
|
4
|
+
|
|
5
|
+
| Command | Description |
|
|
6
|
+
| -------------------------------------------- | ---------------------- |
|
|
7
|
+
| `yarn build --scope @markuplint/html-parser` | Build this package |
|
|
8
|
+
| `yarn dev --scope @markuplint/html-parser` | Watch mode build |
|
|
9
|
+
| `yarn clean --scope @markuplint/html-parser` | Remove build artifacts |
|
|
10
|
+
| `yarn test --scope @markuplint/html-parser` | Run tests |
|
|
11
|
+
|
|
12
|
+
## Testing
|
|
13
|
+
|
|
14
|
+
Test files follow the `*.spec.ts` naming convention and are located in the `src/` directory:
|
|
15
|
+
|
|
16
|
+
| Test File | Coverage |
|
|
17
|
+
| -------------------------------------- | ------------------------------------------------------------------- |
|
|
18
|
+
| `index.spec.ts` | HtmlParser integration tests (parsing HTML documents and fragments) |
|
|
19
|
+
| `get-namespace.spec.ts` | Namespace resolution for HTML, SVG, and MathML elements |
|
|
20
|
+
| `optimize-starts-head-or-body.spec.ts` | Head/body tag optimization setup and resume |
|
|
21
|
+
|
|
22
|
+
The primary testing pattern uses `nodeListToDebugMaps` for snapshot-style assertions:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { nodeListToDebugMaps } from '@markuplint/parser-utils';
|
|
26
|
+
import { parser } from '@markuplint/html-parser';
|
|
27
|
+
|
|
28
|
+
const doc = parser.parse('<div class="foo">text</div>');
|
|
29
|
+
const debugMaps = nodeListToDebugMaps(doc.nodeList, true);
|
|
30
|
+
expect(debugMaps).toStrictEqual([
|
|
31
|
+
// expected debug output
|
|
32
|
+
]);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Recipes
|
|
36
|
+
|
|
37
|
+
### 1. Modifying an HtmlParser Override Method
|
|
38
|
+
|
|
39
|
+
1. Read `src/parser.ts` and identify the override method to change
|
|
40
|
+
2. Review the base `Parser` class in `@markuplint/parser-utils` to understand the parent behavior
|
|
41
|
+
3. Make the change, ensuring `super.*()` calls are preserved where required:
|
|
42
|
+
- `beforeParse()` — must call `super.beforeParse()` first
|
|
43
|
+
- `afterParse()` — must call `super.afterParse()` first
|
|
44
|
+
- `afterNodeize()` — must call `super.afterNodeize()` first
|
|
45
|
+
- `visitText()` — calls `super.visitText()` with options
|
|
46
|
+
4. Build: `yarn build --scope @markuplint/html-parser`
|
|
47
|
+
5. Run tests: `yarn test --scope @markuplint/html-parser`
|
|
48
|
+
6. Check downstream impact (see checklist below)
|
|
49
|
+
|
|
50
|
+
### 2. Modifying Ghost Element Handling
|
|
51
|
+
|
|
52
|
+
1. Read the `nodeize()` method in `src/parser.ts` — the ghost element branch is the `if (!location)` block
|
|
53
|
+
2. Read the `afterNodeize()` method to understand how `afterPosition` state is maintained
|
|
54
|
+
3. Make changes to the position calculation or element creation logic
|
|
55
|
+
4. Build and test: `yarn build --scope @markuplint/html-parser && yarn test --scope @markuplint/html-parser`
|
|
56
|
+
5. Test with HTML that triggers ghost elements (e.g., `<div>text</div>` parsed as a document — will create ghost `<html>`, `<head>`, `<body>`)
|
|
57
|
+
|
|
58
|
+
### 3. Adding a New Namespace
|
|
59
|
+
|
|
60
|
+
1. Read `src/get-namespace.ts`
|
|
61
|
+
2. Add a new `case` in the `switch (parentNamespace)` block for the new namespace URI
|
|
62
|
+
3. Choose an appropriate wrapper element for the new namespace
|
|
63
|
+
4. Build and test: `yarn build --scope @markuplint/html-parser && yarn test --scope @markuplint/html-parser`
|
|
64
|
+
5. Add test cases to `src/get-namespace.spec.ts`
|
|
65
|
+
|
|
66
|
+
### 4. Modifying Head/Body Optimization
|
|
67
|
+
|
|
68
|
+
1. Read `src/optimize-starts-head-or-body.ts`
|
|
69
|
+
2. The module has three key functions:
|
|
70
|
+
- `isStartsHeadTagOrBodyTag()` — detection regex
|
|
71
|
+
- `optimizeStartsHeadTagOrBodyTagSetup()` — placeholder replacement
|
|
72
|
+
- `optimizeStartsHeadTagOrBodyTagResume()` — name restoration
|
|
73
|
+
3. Make changes, paying attention to:
|
|
74
|
+
- The placeholder character `\uFFFD` (Unicode Replacement Character) must remain unique
|
|
75
|
+
- The `replaceAll` regex must match both opening and closing tags
|
|
76
|
+
- Restoration must handle both `starttag` and `endtag` node types
|
|
77
|
+
4. Build and test: `yarn build --scope @markuplint/html-parser && yarn test --scope @markuplint/html-parser`
|
|
78
|
+
5. Add or update test cases in `src/optimize-starts-head-or-body.spec.ts`
|
|
79
|
+
|
|
80
|
+
## Downstream Impact Checklist
|
|
81
|
+
|
|
82
|
+
Changes to this package can affect 4 downstream parser packages:
|
|
83
|
+
|
|
84
|
+
| Package | Relationship | Key Dependencies |
|
|
85
|
+
| --------------------------- | -------------------- | ----------------------------------------- |
|
|
86
|
+
| `@markuplint/jsx-parser` | Extends `HtmlParser` | All override methods, constructor options |
|
|
87
|
+
| `@markuplint/vue-parser` | Imports `HtmlParser` | `tokenize()`, `nodeize()` |
|
|
88
|
+
| `@markuplint/svelte-parser` | Imports `HtmlParser` | `tokenize()`, `nodeize()` |
|
|
89
|
+
| `@markuplint/astro-parser` | Imports `HtmlParser` | `tokenize()`, `nodeize()` |
|
|
90
|
+
|
|
91
|
+
Always run downstream parser tests when modifying `HtmlParser`:
|
|
92
|
+
|
|
93
|
+
```shell
|
|
94
|
+
yarn test --scope @markuplint/html-parser --scope @markuplint/jsx-parser \
|
|
95
|
+
--scope @markuplint/vue-parser --scope @markuplint/svelte-parser \
|
|
96
|
+
--scope @markuplint/astro-parser
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Troubleshooting
|
|
100
|
+
|
|
101
|
+
### Ghost element position is incorrect
|
|
102
|
+
|
|
103
|
+
**Symptom:** Ghost elements (`<html>`, `<head>`, `<body>`) have wrong line/column/offset values in the AST.
|
|
104
|
+
|
|
105
|
+
**Cause:** The `afterPosition` state is not being updated correctly, or the depth check in `nodeize()` is wrong.
|
|
106
|
+
|
|
107
|
+
**Solution:**
|
|
108
|
+
|
|
109
|
+
1. Check `afterNodeize()` — ensure `this.state.afterPosition` is updated with the correct `endOffset`, `endLine`, `endCol`, and `depth`
|
|
110
|
+
2. Check the ghost element branch in `nodeize()` — the `depth === this.state.afterPosition.depth` comparison must match
|
|
111
|
+
|
|
112
|
+
### Head/body tag parsing produces unexpected results
|
|
113
|
+
|
|
114
|
+
**Symptom:** When source starts with `<head>` or `<body>`, the parsed AST contains placeholder names or missing elements.
|
|
115
|
+
|
|
116
|
+
**Cause:** The optimization setup or resume step has a bug.
|
|
117
|
+
|
|
118
|
+
**Solution:**
|
|
119
|
+
|
|
120
|
+
1. Check `isStartsHeadTagOrBodyTag()` — ensure the detection regex matches the input
|
|
121
|
+
2. Check `optimizeStartsHeadTagOrBodyTagSetup()` — verify placeholder names are correctly generated
|
|
122
|
+
3. Check `optimizeStartsHeadTagOrBodyTagResume()` — verify original names are restored for both start and end tags
|
|
123
|
+
|
|
124
|
+
### Namespace resolution returns wrong value
|
|
125
|
+
|
|
126
|
+
**Symptom:** SVG or MathML elements are assigned the wrong namespace URI.
|
|
127
|
+
|
|
128
|
+
**Cause:** The parent namespace context is not being passed correctly, or parse5 resolves the namespace differently than expected.
|
|
129
|
+
|
|
130
|
+
**Solution:**
|
|
131
|
+
|
|
132
|
+
1. Check the calling code — ensure `originNode.namespaceURI` is being read correctly in `nodeize()`
|
|
133
|
+
2. Check `getNamespace()` — add a test case with the specific tag name and parent namespace combination
|
|
134
|
+
3. Note that parse5 may apply integration point rules that change the namespace
|
package/lib/get-namespace.d.ts
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
1
|
import type { NamespaceURI } from '@markuplint/ml-ast';
|
|
2
|
+
/**
|
|
3
|
+
* Determines the namespace URI for an element given its tag name and the parent's namespace.
|
|
4
|
+
*
|
|
5
|
+
* Uses parse5 to simulate parsing and determine whether a tag belongs to
|
|
6
|
+
* the HTML, SVG, or MathML namespace within the given parent context.
|
|
7
|
+
*
|
|
8
|
+
* @param tagName - The element tag name to resolve
|
|
9
|
+
* @param parentNamespace - The namespace URI of the parent element (defaults to XHTML)
|
|
10
|
+
* @returns The resolved namespace URI for the tag
|
|
11
|
+
*/
|
|
2
12
|
export declare function getNamespace(tagName: string, parentNamespace?: string): NamespaceURI;
|
package/lib/get-namespace.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { parse, parseFragment } from 'parse5';
|
|
2
2
|
const DEFAULT_NAMESPACE = 'http://www.w3.org/1999/xhtml';
|
|
3
|
+
/**
|
|
4
|
+
* Determines the namespace URI for an element given its tag name and the parent's namespace.
|
|
5
|
+
*
|
|
6
|
+
* Uses parse5 to simulate parsing and determine whether a tag belongs to
|
|
7
|
+
* the HTML, SVG, or MathML namespace within the given parent context.
|
|
8
|
+
*
|
|
9
|
+
* @param tagName - The element tag name to resolve
|
|
10
|
+
* @param parentNamespace - The namespace URI of the parent element (defaults to XHTML)
|
|
11
|
+
* @returns The resolved namespace URI for the tag
|
|
12
|
+
*/
|
|
3
13
|
export function getNamespace(tagName, parentNamespace = DEFAULT_NAMESPACE) {
|
|
4
14
|
switch (parentNamespace) {
|
|
5
15
|
case 'http://www.w3.org/2000/svg':
|
package/lib/parser.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ type State = {
|
|
|
13
13
|
};
|
|
14
14
|
};
|
|
15
15
|
type ExtendsOptions = Pick<ParserOptions, 'ignoreTags' | 'maskChar'>;
|
|
16
|
+
/**
|
|
17
|
+
* Parser implementation for standard HTML, built on top of parse5.
|
|
18
|
+
* Handles document and fragment parsing, ghost elements (omitted tags),
|
|
19
|
+
* and optimizations for `<head>` / `<body>` tag handling.
|
|
20
|
+
*/
|
|
16
21
|
export declare class HtmlParser extends Parser<Node, State> {
|
|
17
22
|
constructor(options?: ExtendsOptions);
|
|
18
23
|
tokenize(): {
|
|
@@ -29,5 +34,8 @@ export declare class HtmlParser extends Parser<Node, State> {
|
|
|
29
34
|
visitText(token: ChildToken): readonly MLASTNodeTreeItem[];
|
|
30
35
|
visitSpreadAttr(): null;
|
|
31
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Default singleton instance of the HTML parser.
|
|
39
|
+
*/
|
|
32
40
|
export declare const parser: HtmlParser;
|
|
33
41
|
export {};
|
package/lib/parser.js
CHANGED
|
@@ -2,6 +2,11 @@ import { Parser } from '@markuplint/parser-utils';
|
|
|
2
2
|
import { parse, parseFragment } from 'parse5';
|
|
3
3
|
import { isDocumentFragment } from './is-document-fragment.js';
|
|
4
4
|
import { optimizeStartsHeadTagOrBodyTagResume, optimizeStartsHeadTagOrBodyTagSetup, } from './optimize-starts-head-or-body.js';
|
|
5
|
+
/**
|
|
6
|
+
* Parser implementation for standard HTML, built on top of parse5.
|
|
7
|
+
* Handles document and fragment parsing, ghost elements (omitted tags),
|
|
8
|
+
* and optimizations for `<head>` / `<body>` tag handling.
|
|
9
|
+
*/
|
|
5
10
|
export class HtmlParser extends Parser {
|
|
6
11
|
constructor(options) {
|
|
7
12
|
super(options, {
|
|
@@ -159,4 +164,7 @@ export class HtmlParser extends Parser {
|
|
|
159
164
|
return null;
|
|
160
165
|
}
|
|
161
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Default singleton instance of the HTML parser.
|
|
169
|
+
*/
|
|
162
170
|
export const parser = new HtmlParser();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/html-parser",
|
|
3
|
-
"version": "4.6.
|
|
3
|
+
"version": "4.6.23",
|
|
4
4
|
"description": "HTML parser for markuplint",
|
|
5
5
|
"repository": "git@github.com:markuplint/markuplint.git",
|
|
6
6
|
"author": "Yusuke Hirao <yusukehirao@me.com>",
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
"clean": "tsc --build --clean tsconfig.build.json"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@markuplint/ml-ast": "4.4.
|
|
28
|
-
"@markuplint/parser-utils": "4.8.
|
|
27
|
+
"@markuplint/ml-ast": "4.4.11",
|
|
28
|
+
"@markuplint/parser-utils": "4.8.11",
|
|
29
29
|
"parse5": "8.0.0",
|
|
30
30
|
"type-fest": "4.41.0"
|
|
31
31
|
},
|
|
32
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "193ee7c1262bbed95424e38efdf1a8e56ff049f4"
|
|
33
33
|
}
|