@markuplint/html-parser 4.6.22 → 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.
@@ -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) -- コマンド、レシピ、トラブルシューティング
@@ -0,0 +1,197 @@
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 and parser
12
+ ├── parser.ts — HtmlParser class extending Parser<Node, State>
13
+ ├── types.ts — Re-exports parse5 types (Node, Element, etc.)
14
+ ├── is-document-fragment.ts — Regex-based fragment vs document detection
15
+ └── optimize-starts-head-or-body.ts — Head/body tag placeholder optimization
16
+ ```
17
+
18
+ ## Architecture Diagram
19
+
20
+ ```mermaid
21
+ flowchart TD
22
+ subgraph upstream ["Upstream"]
23
+ mlAst["@markuplint/ml-ast\n(AST types)"]
24
+ parserUtils["@markuplint/parser-utils\n(Abstract Parser class)"]
25
+ parse5["parse5\n(HTML tokenizer)"]
26
+ end
27
+
28
+ subgraph pkg ["@markuplint/html-parser"]
29
+ htmlParser["HtmlParser\nextends Parser‹Node, State›"]
30
+ isFragment["isDocumentFragment()\nFragment detection"]
31
+ optimize["optimizeStartsHeadTagOrBodyTag\nHead/body optimization"]
32
+ types["types.ts\nparse5 type re-exports"]
33
+ end
34
+
35
+ subgraph downstream ["Downstream Parsers"]
36
+ jsx["@markuplint/jsx-parser\n(extends HtmlParser)"]
37
+ vue["@markuplint/vue-parser\n(imports HtmlParser)"]
38
+ svelte["@markuplint/svelte-parser\n(imports HtmlParser)"]
39
+ astro["@markuplint/astro-parser\n(imports HtmlParser)"]
40
+ end
41
+
42
+ mlAst -->|"AST types"| htmlParser
43
+ parserUtils -->|"Parser base class"| htmlParser
44
+ parse5 -->|"parse / parseFragment"| htmlParser
45
+ htmlParser --> isFragment
46
+ htmlParser --> optimize
47
+
48
+ htmlParser -->|"extends / imports"| downstream
49
+ ```
50
+
51
+ ## HtmlParser Class
52
+
53
+ ### Inheritance
54
+
55
+ ```
56
+ Parser<Node, State> (from @markuplint/parser-utils)
57
+ └── HtmlParser (this package)
58
+ ```
59
+
60
+ ### State Type
61
+
62
+ The parser maintains internal state through the `State` type:
63
+
64
+ | Field | Type | Purpose |
65
+ | ------------------------ | --------------------------------------- | ---------------------------------------------------------------------------------------------------- |
66
+ | `startsHeadTagOrBodyTag` | `Replacements \| null` | Tracks head/body placeholder replacements when the source starts with `<head>` or `<body>` |
67
+ | `afterPosition` | `{ endOffset, endLine, endCol, depth }` | Tracks the end position of the last processed node at each depth, used for ghost element positioning |
68
+
69
+ ### Override Methods
70
+
71
+ | Method | Purpose |
72
+ | ------------------- | ------------------------------------------------------------------------------------------- |
73
+ | `tokenize()` | Invokes parse5 `parse()` or `parseFragment()` based on fragment detection |
74
+ | `beforeParse()` | Sets up head/body optimization and offset tracking |
75
+ | `afterParse()` | Restores original head/body tag names from placeholders |
76
+ | `nodeize()` | Converts parse5 nodes to markuplint AST nodes, handling ghost elements and template content |
77
+ | `afterNodeize()` | Updates `afterPosition` state for ghost element positioning |
78
+ | `visitText()` | Delegates to parent with `researchTags: true` and `invalidTagAsText: true` |
79
+ | `visitSpreadAttr()` | Returns `null` (HTML does not support spread attributes) |
80
+
81
+ ## Parse Pipeline
82
+
83
+ The HTML-specific pipeline extends the base `Parser` pipeline:
84
+
85
+ ```mermaid
86
+ flowchart LR
87
+ A["beforeParse\n- super.beforeParse()\n- head/body optimization setup\n- offset tracking"]
88
+ B["tokenize\n- isDocumentFragment() check\n- parse5 parse/parseFragment"]
89
+ C["nodeize\n- Ghost element handling\n- Doctype/text/comment/element dispatch\n- Template content extraction"]
90
+ D["afterNodeize\n- Update afterPosition state"]
91
+ E["afterParse\n- Restore head/body names"]
92
+
93
+ A --> B --> C --> D --> E
94
+ ```
95
+
96
+ ## Ghost Element Handling
97
+
98
+ 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.
99
+
100
+ ### Detection
101
+
102
+ Ghost elements are identified by having no `sourceCodeLocation` in the parse5 output (`!location`).
103
+
104
+ ### Position Calculation
105
+
106
+ Since ghost elements have no source position, the parser calculates their position using the `afterPosition` state:
107
+
108
+ 1. `afterNodeize()` records the end position of each processed node at its depth level
109
+ 2. When `nodeize()` encounters a ghost element, it uses `afterPosition` if the depth matches, otherwise falls back to the parent node's position
110
+ 3. The ghost element is created with an empty `raw` string and the calculated start position
111
+
112
+ This ensures ghost elements are positioned correctly in the AST without disrupting the source mapping of real elements.
113
+
114
+ ## Head/Body Tag Optimization
115
+
116
+ ### Problem
117
+
118
+ 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.
119
+
120
+ ### Solution
121
+
122
+ The optimization uses a placeholder replacement strategy:
123
+
124
+ 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
125
+ 2. **Parse**: parse5 parses the modified source with placeholder tag names, treating them as custom elements
126
+ 3. **Resume** (`optimizeStartsHeadTagOrBodyTagResume`): After parsing, restores the original tag names in the AST using `parser.updateRaw()` and `parser.updateElement()`
127
+
128
+ ## Namespace Resolution
129
+
130
+ Namespace resolution is handled by `getNamespace()` in `@markuplint/parser-utils`. The HTML parser delegates namespace detection to the base `Parser` class, which automatically determines namespaces from tag names and parent node context.
131
+
132
+ ## Fragment vs Document Detection
133
+
134
+ `isDocumentFragment()` uses a regex to determine whether the input should be parsed as a fragment or a full document:
135
+
136
+ - **Document**: Input starts with `<!doctype html...>` or `<html`
137
+ - **Fragment**: Everything else
138
+
139
+ This distinction matters because parse5's `parse()` applies the full document parsing algorithm (inserting implicit `<html>`, `<head>`, `<body>`), while `parseFragment()` parses content as-is.
140
+
141
+ ## External Dependencies
142
+
143
+ | Dependency | Purpose |
144
+ | -------------------------- | ---------------------------------------------------------------------- |
145
+ | `@markuplint/ml-ast` | AST type definitions (`MLASTNodeTreeItem`, `MLASTParentNode`, etc.) |
146
+ | `@markuplint/parser-utils` | Abstract `Parser` class, `ChildToken`, `ParseOptions`, `ParserOptions` |
147
+ | `parse5` | HTML parsing (`parse`, `parseFragment`, `DefaultTreeAdapterMap`) |
148
+ | `type-fest` | TypeScript utility types |
149
+
150
+ ## Integration Points
151
+
152
+ ```mermaid
153
+ flowchart TD
154
+ subgraph upstream ["Upstream"]
155
+ mlAst["@markuplint/ml-ast\n(AST types)"]
156
+ parserUtils["@markuplint/parser-utils\n(Parser base class)"]
157
+ parse5["parse5\n(HTML tokenizer)"]
158
+ end
159
+
160
+ subgraph pkg ["@markuplint/html-parser"]
161
+ htmlParser["HtmlParser"]
162
+ end
163
+
164
+ subgraph downstream ["Downstream"]
165
+ jsx["@markuplint/jsx-parser"]
166
+ vue["@markuplint/vue-parser"]
167
+ svelte["@markuplint/svelte-parser"]
168
+ astro["@markuplint/astro-parser"]
169
+ end
170
+
171
+ subgraph indirect ["Indirect Downstream"]
172
+ mlCore["@markuplint/ml-core\n(MLASTDocument → MLDOM)"]
173
+ end
174
+
175
+ upstream -->|"types, parsing"| htmlParser
176
+ htmlParser -->|"extends / imports"| downstream
177
+ downstream -->|"produces MLASTDocument"| mlCore
178
+ ```
179
+
180
+ ### Upstream
181
+
182
+ - **`@markuplint/ml-ast`** -- AST type definitions used throughout the parser
183
+ - **`@markuplint/parser-utils`** -- Abstract `Parser` class that `HtmlParser` extends, plus utility types
184
+ - **`parse5`** -- The underlying HTML parser that performs tokenization and tree construction
185
+
186
+ ### Downstream
187
+
188
+ Four parser packages depend on `HtmlParser`:
189
+
190
+ - **`@markuplint/jsx-parser`** -- Extends `HtmlParser` to add JSX support
191
+ - **`@markuplint/vue-parser`** -- Imports `HtmlParser` for HTML portions of Vue SFCs
192
+ - **`@markuplint/svelte-parser`** -- Imports `HtmlParser` for HTML portions of Svelte components
193
+ - **`@markuplint/astro-parser`** -- Imports `HtmlParser` for HTML portions of Astro components
194
+
195
+ ## Documentation Map
196
+
197
+ - [Maintenance Guide](docs/maintenance.md) -- Commands, recipes, and troubleshooting
package/CHANGELOG.md CHANGED
@@ -3,13 +3,32 @@
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.22](https://github.com/markuplint/markuplint/compare/@markuplint/html-parser@4.6.21...@markuplint/html-parser@4.6.22) (2025-11-05)
6
+ # [5.0.0-alpha.0](https://github.com/markuplint/markuplint/compare/v4.14.1...v5.0.0-alpha.0) (2026-02-20)
7
7
 
8
- **Note:** Version bump only for package @markuplint/html-parser
8
+ ### Bug Fixes
9
+
10
+ - **ml-core:** improve detection of namespace ([5b507ad](https://github.com/markuplint/markuplint/commit/5b507ad7c19c5015b8ce587845d901e31dfa6518))
11
+
12
+ - refactor(html-parser)!: update for simplified AST token properties ([524ce5d](https://github.com/markuplint/markuplint/commit/524ce5d6fc23c8bff73583ed4ac42fdff1759938))
13
+
14
+ ### BREAKING CHANGES
9
15
 
16
+ - Adapt to renamed MLASTToken properties.
10
17
 
18
+ * Use getEndPosition() for ghost element position calculation
19
+ * Update test assertions: startCol -> col, startOffset -> offset,
20
+ startLine -> line
21
+ * Remove endOffset/endLine/endCol assertions from tests
11
22
 
23
+ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
12
24
 
25
+ ## [4.6.23](https://github.com/markuplint/markuplint/compare/@markuplint/html-parser@4.6.22...@markuplint/html-parser@4.6.23) (2026-02-10)
26
+
27
+ **Note:** Version bump only for package @markuplint/html-parser
28
+
29
+ ## [4.6.22](https://github.com/markuplint/markuplint/compare/@markuplint/html-parser@4.6.21...@markuplint/html-parser@4.6.22) (2025-11-05)
30
+
31
+ **Note:** Version bump only for package @markuplint/html-parser
13
32
 
14
33
  ## [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
34
 
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
@@ -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;
@@ -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/index.d.ts CHANGED
@@ -1,2 +1 @@
1
- export { getNamespace } from './get-namespace.js';
2
1
  export { parser, HtmlParser } from './parser.js';
package/lib/index.js CHANGED
@@ -1,2 +1 @@
1
- export { getNamespace } from './get-namespace.js';
2
1
  export { parser, HtmlParser } from './parser.js';
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,12 @@ 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
+ import { getEndPosition } from '@markuplint/parser-utils/location';
6
+ /**
7
+ * Parser implementation for standard HTML, built on top of parse5.
8
+ * Handles document and fragment parsing, ghost elements (omitted tags),
9
+ * and optimizations for `<head>` / `<body>` tag handling.
10
+ */
5
11
  export class HtmlParser extends Parser {
6
12
  constructor(options) {
7
13
  super(options, {
@@ -52,24 +58,26 @@ export class HtmlParser extends Parser {
52
58
  nodeize(
53
59
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
54
60
  originNode, parentNode, depth) {
55
- const namespace = 'namespaceURI' in originNode ? originNode.namespaceURI : '';
56
61
  const location = originNode.sourceCodeLocation;
57
62
  if (!location) {
58
63
  // Ghost element
59
- const afterNode = this.state.afterPosition.depth === depth ? this.state.afterPosition : parentNode;
60
- const startOffset = afterNode?.endOffset ?? 0;
61
- const startLine = afterNode?.endLine ?? 0;
62
- const startCol = afterNode?.endCol ?? 0;
64
+ const afterNode = this.state.afterPosition.depth === depth
65
+ ? this.state.afterPosition
66
+ : parentNode
67
+ ? getEndPosition(parentNode.raw, parentNode.offset, parentNode.line, parentNode.col)
68
+ : null;
69
+ const offset = afterNode?.endOffset ?? 0;
70
+ const line = afterNode?.endLine ?? 0;
71
+ const col = afterNode?.endCol ?? 0;
63
72
  const childNodes = 'childNodes' in originNode ? originNode.childNodes : [];
64
73
  return this.visitElement({
65
74
  raw: '',
66
- startOffset,
67
- startLine,
68
- startCol,
75
+ offset,
76
+ line,
77
+ col,
69
78
  depth,
70
79
  parentNode,
71
80
  nodeName: originNode.nodeName,
72
- namespace,
73
81
  }, childNodes);
74
82
  }
75
83
  const { startOffset, endOffset } = location;
@@ -117,7 +125,6 @@ export class HtmlParser extends Parser {
117
125
  depth,
118
126
  parentNode,
119
127
  nodeName: originNode.nodeName,
120
- namespace,
121
128
  }, childNodes, {
122
129
  createEndTagToken: () => {
123
130
  const endTagLoc = 'endTag' in location ? location.endTag : null;
@@ -140,10 +147,9 @@ export class HtmlParser extends Parser {
140
147
  const after = super.afterNodeize(siblings, parentNode, depth);
141
148
  const prevNode = after.siblings.at(-1) ?? after.ancestors.findLast(n => n.depth === depth);
142
149
  if (prevNode) {
150
+ const endPos = getEndPosition(prevNode.raw, prevNode.offset, prevNode.line, prevNode.col);
143
151
  this.state.afterPosition = {
144
- endOffset: prevNode.endOffset,
145
- endLine: prevNode.endLine,
146
- endCol: prevNode.endCol,
152
+ ...endPos,
147
153
  depth,
148
154
  };
149
155
  }
@@ -159,4 +165,7 @@ export class HtmlParser extends Parser {
159
165
  return null;
160
166
  }
161
167
  }
168
+ /**
169
+ * Default singleton instance of the HTML parser.
170
+ */
162
171
  export const parser = new HtmlParser();
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@markuplint/html-parser",
3
- "version": "4.6.22",
3
+ "version": "5.0.0-alpha.0",
4
4
  "description": "HTML parser for markuplint",
5
5
  "repository": "git@github.com:markuplint/markuplint.git",
6
6
  "author": "Yusuke Hirao <yusukehirao@me.com>",
7
7
  "license": "MIT",
8
+ "engines": {
9
+ "node": ">=22"
10
+ },
8
11
  "type": "module",
9
12
  "exports": {
10
13
  ".": {
@@ -24,10 +27,10 @@
24
27
  "clean": "tsc --build --clean tsconfig.build.json"
25
28
  },
26
29
  "dependencies": {
27
- "@markuplint/ml-ast": "4.4.10",
28
- "@markuplint/parser-utils": "4.8.10",
30
+ "@markuplint/ml-ast": "5.0.0-alpha.0",
31
+ "@markuplint/parser-utils": "5.0.0-alpha.0",
29
32
  "parse5": "8.0.0",
30
- "type-fest": "4.41.0"
33
+ "type-fest": "5.4.4"
31
34
  },
32
- "gitHead": "6213ea30269ef404f030e67bbcc7fc7443ec1060"
35
+ "gitHead": "13dcfc84ec83d87360c720e253383b60767e1b56"
33
36
  }