@markuplint/astro-parser 4.6.23 → 4.18.3
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 +46 -17
- package/ARCHITECTURE.md +41 -12
- package/CHANGELOG.md +15 -0
- package/docs/maintenance.ja.md +54 -5
- package/docs/maintenance.md +54 -5
- package/lib/astro-parser.d.ts +31 -1
- package/lib/astro-parser.js +62 -7
- package/lib/parser.d.ts +4 -4
- package/lib/parser.js +31 -1
- package/lib/spread-attr.d.ts +35 -0
- package/lib/spread-attr.js +116 -0
- package/package.json +6 -6
- package/lib/detect-block-behavior.d.ts +0 -2
- package/lib/detect-block-behavior.js +0 -12
package/ARCHITECTURE.ja.md
CHANGED
|
@@ -191,20 +191,48 @@ Astro テンプレートディレクティブは `name:modifier` 構文を使用
|
|
|
191
191
|
- ショートハンド属性: `{prop}`
|
|
192
192
|
- ネストされた式: `style={{ a: b }}`
|
|
193
193
|
|
|
194
|
+
### スプレッド属性
|
|
195
|
+
|
|
196
|
+
スプレッド属性(`{...EXPR}`)は、基底の `Parser.visitAttr()` に委譲する **前**に、`visitAttr()` 内の波括弧対応プリパス(`src/spread-attr.ts` 参照)で抽出されます。プリパスは生のトークンを 1 文字ずつ走査し、以下を考慮します:
|
|
197
|
+
|
|
198
|
+
- 文字列リテラル(`'`、`"`)
|
|
199
|
+
- `${}` 補間付きテンプレートリテラル
|
|
200
|
+
- 行コメント(`//`)とブロックコメント(`/* */`)
|
|
201
|
+
- バックスラッシュでエスケープされたクォート(連続するバックスラッシュの偶奇判定)
|
|
202
|
+
|
|
203
|
+
スプレッドトークンに対しては上流の `safeScriptParser`(espree ベース)を意図的に回避します。理由:
|
|
204
|
+
|
|
205
|
+
1. espree は `{...x as any}` のような TypeScript 構文を理解せず、`as` の手前でスプレッドを誤って終端させる。
|
|
206
|
+
2. espree は「valid な JS プレフィックス」をスプレッドの閉じ `}` を超えて貪欲に伸ばすことがあり、例えば `{...props}>{label}` を二項 `>` 式として解釈してしまい、次の兄弟ノードを呑み込み `Invalid tag syntax` エラーになる。
|
|
207
|
+
|
|
208
|
+
両方とも [#3824](https://github.com/markuplint/markuplint/issues/3824) で報告された症状です。プリパスは `{...}` 境界を純粋な波括弧マッチング問題として扱うことで両方を解消します。
|
|
209
|
+
|
|
210
|
+
**波括弧マッチャの既知の制限**:
|
|
211
|
+
|
|
212
|
+
- 波括弧を含む正規表現リテラル(例: `{...x.match(/}/) ? a : b}`)は認識しません。`/` は常に除算演算子として扱われます。遭遇した場合は変数に切り出して回避してください。
|
|
213
|
+
|
|
214
|
+
**撤去条件**: `parser-utils/script-parser.ts` が TypeScript 構文を理解し、かつスプレッドの `}` を超えて伸びないように改善された場合、本パッケージの `src/spread-attr.ts` と `visitAttr()` のプリパスは削除し、基底パーサーのパスに戻すことが可能です。
|
|
215
|
+
|
|
216
|
+
### Raw-text 要素の本文(`<script>`、`<style>`)
|
|
217
|
+
|
|
218
|
+
`AstroParser.visitElement()` は **要素全体(本文・終了タグを含む)の raw 文字列**を `parser-utils` の `parseCodeFragment()` に渡し、最初のノードを開始タグ、最後のノードを終了タグとして使います。`<script>` や `<style>` の本文をそのまま再トークナイズすると、`/<br\s*\/?>/gi` のような正規表現がタグとして誤読され `Invalid tag syntax` で失敗します([#3860](https://github.com/markuplint/markuplint/issues/3860)、[#3825](https://github.com/markuplint/markuplint/issues/3825) の v4 backport)。
|
|
219
|
+
|
|
220
|
+
raw-text の安全性は `parser-utils` 側に委譲しています。`parseCodeFragment()` は `rawTextElements`(既定 `['style', 'script']`)を認識し、本文を次に現れる ASCII 大文字小文字非依存の `</tagName`(タブ/LF/FF/CR/空白/`>` /`/` のいずれかが続く)まで丸ごと消費します([HTML LS §13.2.5.1](https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions))。`astro-parser` 側に専用分岐は不要で、`parsedNodes[0]` と `parsedNodes[-1]` がそれぞれ開始タグ・終了タグになる既存フローのまま動作します。
|
|
221
|
+
|
|
194
222
|
## jsx-parser との比較
|
|
195
223
|
|
|
196
|
-
| 機能 | `astro-parser`
|
|
197
|
-
| ------------------------------ |
|
|
198
|
-
| **トークナイザ** | `astro-eslint-parser`
|
|
199
|
-
| **フロントマター** | サポート(`---...---` psblock)
|
|
200
|
-
| **式の構文** | `{expr}` を MustacheTag psblock として
|
|
201
|
-
| **テンプレートディレクティブ** | `class:list`、`set:html` 等
|
|
202
|
-
| **名前空間管理** | `#updateScopeNS()` で手動管理
|
|
203
|
-
| **コンポーネント検出** | `/^[A-Z]/` パターン
|
|
204
|
-
| **自己閉じタイプ** | `html+xml`
|
|
205
|
-
| **booleanish 属性** | 未設定
|
|
206
|
-
| **名前なしフラグメント** | `<>...</>` サポート
|
|
207
|
-
| **スプレッド属性** |
|
|
224
|
+
| 機能 | `astro-parser` | `jsx-parser` |
|
|
225
|
+
| ------------------------------ | ----------------------------------------------- | ------------------------------------------------- |
|
|
226
|
+
| **トークナイザ** | `astro-eslint-parser` | TypeScript ESTree(`@typescript-eslint/parser`) |
|
|
227
|
+
| **フロントマター** | サポート(`---...---` psblock) | 該当なし |
|
|
228
|
+
| **式の構文** | `{expr}` を MustacheTag psblock として | `{expr}` を JSXExpressionContainer psblock として |
|
|
229
|
+
| **テンプレートディレクティブ** | `class:list`、`set:html` 等 | 該当なし |
|
|
230
|
+
| **名前空間管理** | `#updateScopeNS()` で手動管理 | html-parser の `getNamespace()` に委譲 |
|
|
231
|
+
| **コンポーネント検出** | `/^[A-Z]/` パターン | `/^[A-Z]/` パターン |
|
|
232
|
+
| **自己閉じタイプ** | `html+xml` | デフォルト(XML のみ) |
|
|
233
|
+
| **booleanish 属性** | 未設定 | `booleanish: true` |
|
|
234
|
+
| **名前なしフラグメント** | `<>...</>` サポート | `<>...</>` サポート |
|
|
235
|
+
| **スプレッド属性** | `visitAttr()` 内の波括弧対応プリパス(TS 対応) | カスタム `visitSpreadAttr()` で IDL ルックアップ |
|
|
208
236
|
|
|
209
237
|
## バージョン互換性
|
|
210
238
|
|
|
@@ -218,11 +246,12 @@ astro-eslint-parser → @astrojs/compiler → Astro 構文サポート
|
|
|
218
246
|
|
|
219
247
|
## 主要ソースファイル
|
|
220
248
|
|
|
221
|
-
| ファイル | 用途
|
|
222
|
-
| ----------------- |
|
|
223
|
-
| `parser.ts` | `AstroParser` クラス — 全オーバーライドメソッドと名前空間スコーピング
|
|
224
|
-
| `astro-parser.ts` | `astroParse()` ラッパー — `astro-eslint-parser` に委譲し、診断を `ParserError` に変換
|
|
225
|
-
| `
|
|
249
|
+
| ファイル | 用途 |
|
|
250
|
+
| ----------------- | -------------------------------------------------------------------------------------- |
|
|
251
|
+
| `parser.ts` | `AstroParser` クラス — 全オーバーライドメソッドと名前空間スコーピング |
|
|
252
|
+
| `astro-parser.ts` | `astroParse()` ラッパー — `astro-eslint-parser` に委譲し、診断を `ParserError` に変換 |
|
|
253
|
+
| `spread-attr.ts` | `visitAttr()` が使用する波括弧対応スプレッド属性抽出器(上記「スプレッド属性」を参照) |
|
|
254
|
+
| `index.ts` | 公開 API — シングルトン `parser` インスタンスを再エクスポート |
|
|
226
255
|
|
|
227
256
|
## ドキュメントマップ
|
|
228
257
|
|
package/ARCHITECTURE.md
CHANGED
|
@@ -191,20 +191,48 @@ Any attribute whose start quote is `{` gets `isDynamicValue: true`. This applies
|
|
|
191
191
|
- Shorthand attributes: `{prop}`
|
|
192
192
|
- Nested expressions: `style={{ a: b }}`
|
|
193
193
|
|
|
194
|
+
### Spread Attributes
|
|
195
|
+
|
|
196
|
+
Spread attributes (`{...EXPR}`) are extracted by a brace-aware pre-pass in `visitAttr()` (see `src/spread-attr.ts`) **before** delegating to the base `Parser.visitAttr()`. The pre-pass walks the raw token character by character with awareness of:
|
|
197
|
+
|
|
198
|
+
- string literals (`'`, `"`)
|
|
199
|
+
- template literals with `${}` interpolation
|
|
200
|
+
- line (`//`) and block (`/* */`) comments
|
|
201
|
+
- backslash-escaped quotes (counting consecutive backslashes for parity)
|
|
202
|
+
|
|
203
|
+
This bypasses the upstream `safeScriptParser` (espree-based) for spread tokens because espree:
|
|
204
|
+
|
|
205
|
+
1. Does not understand TypeScript syntax such as `{...x as any}` and would terminate the spread early at `as`.
|
|
206
|
+
2. May greedily extend a "valid JS prefix" past the spread's closing `}` into surrounding HTML — for example `{...props}>{label}` is interpreted as a binary `>` expression, swallowing the next sibling and producing `Invalid tag syntax`.
|
|
207
|
+
|
|
208
|
+
Both failure modes were reported in [#3824](https://github.com/markuplint/markuplint/issues/3824). The pre-pass solves them by treating the `{...}` boundary as a pure brace-matching problem.
|
|
209
|
+
|
|
210
|
+
**Known limitations** of the brace matcher:
|
|
211
|
+
|
|
212
|
+
- Regular-expression literals containing braces (e.g. `{...x.match(/}/) ? a : b}`) are not recognised — `/` is always treated as a division operator. Rewrite via a variable indirection if encountered.
|
|
213
|
+
|
|
214
|
+
**Retraction condition**: if `parser-utils/script-parser.ts` is upgraded to handle TypeScript syntax and to stop extending past the spread's `}`, this package's `src/spread-attr.ts` and the `visitAttr()` pre-pass can be removed and the base parser path restored.
|
|
215
|
+
|
|
216
|
+
### Raw-text Element Bodies (`<script>`, `<style>`)
|
|
217
|
+
|
|
218
|
+
`AstroParser.visitElement()` hands the **entire element source** (including body and end tag) to `parser-utils`'s `parseCodeFragment()` and uses the first parsed node as the start tag and the last as the end tag. For `<script>` and `<style>`, the body would otherwise be re-tokenized as HTML — and a regex such as `/<br\s*\/?>/gi` inside a script body would be misread as a tag, causing `Invalid tag syntax` ([#3860](https://github.com/markuplint/markuplint/issues/3860), v4 backport of [#3825](https://github.com/markuplint/markuplint/issues/3825)).
|
|
219
|
+
|
|
220
|
+
Raw-text safety is delegated to `parser-utils`: `parseCodeFragment()` recognizes `rawTextElements` (default `['style', 'script']`) and consumes the body verbatim until the next ASCII-case-insensitive `</tagName` followed by a tab/LF/FF/CR/space/`>` /`/`, per [HTML LS §13.2.5.1](https://html.spec.whatwg.org/multipage/syntax.html#cdata-rcdata-restrictions). `astro-parser` requires no special-case branch — the existing `visitElement()` flow continues to work because the start tag and end tag still occupy `parsedNodes[0]` and `parsedNodes[-1]` respectively.
|
|
221
|
+
|
|
194
222
|
## Comparison with jsx-parser
|
|
195
223
|
|
|
196
|
-
| Feature | `astro-parser`
|
|
197
|
-
| ------------------------- |
|
|
198
|
-
| **Tokenizer** | `astro-eslint-parser`
|
|
199
|
-
| **Frontmatter** | Supported (`---...---` psblock)
|
|
200
|
-
| **Expression syntax** | `{expr}` as MustacheTag psblock
|
|
201
|
-
| **Template directives** | `class:list`, `set:html`, etc.
|
|
202
|
-
| **Namespace management** | Manual via `#updateScopeNS()`
|
|
203
|
-
| **Component detection** | `/^[A-Z]/` pattern
|
|
204
|
-
| **Self-close type** | `html+xml`
|
|
205
|
-
| **Booleanish attributes** | Not configured
|
|
206
|
-
| **Nameless fragments** | `<>...</>` supported
|
|
207
|
-
| **Spread attributes** |
|
|
224
|
+
| Feature | `astro-parser` | `jsx-parser` |
|
|
225
|
+
| ------------------------- | ------------------------------------------------ | ----------------------------------------------- |
|
|
226
|
+
| **Tokenizer** | `astro-eslint-parser` | TypeScript ESTree (`@typescript-eslint/parser`) |
|
|
227
|
+
| **Frontmatter** | Supported (`---...---` psblock) | Not applicable |
|
|
228
|
+
| **Expression syntax** | `{expr}` as MustacheTag psblock | `{expr}` as JSXExpressionContainer psblock |
|
|
229
|
+
| **Template directives** | `class:list`, `set:html`, etc. | Not applicable |
|
|
230
|
+
| **Namespace management** | Manual via `#updateScopeNS()` | Delegates to `getNamespace()` from html-parser |
|
|
231
|
+
| **Component detection** | `/^[A-Z]/` pattern | `/^[A-Z]/` pattern |
|
|
232
|
+
| **Self-close type** | `html+xml` | Default (XML-only) |
|
|
233
|
+
| **Booleanish attributes** | Not configured | `booleanish: true` |
|
|
234
|
+
| **Nameless fragments** | `<>...</>` supported | `<>...</>` supported |
|
|
235
|
+
| **Spread attributes** | Brace-aware pre-pass in `visitAttr()` (TS-aware) | Custom `visitSpreadAttr()` with IDL lookup |
|
|
208
236
|
|
|
209
237
|
## Version Compatibility
|
|
210
238
|
|
|
@@ -222,6 +250,7 @@ astro-eslint-parser → @astrojs/compiler → Astro syntax support
|
|
|
222
250
|
| ----------------- | -------------------------------------------------------------------------------------------------- |
|
|
223
251
|
| `parser.ts` | `AstroParser` class — all override methods and namespace scoping |
|
|
224
252
|
| `astro-parser.ts` | `astroParse()` wrapper — delegates to `astro-eslint-parser`, converts diagnostics to `ParserError` |
|
|
253
|
+
| `spread-attr.ts` | Brace-aware spread-attribute extractor used by `visitAttr()` (see Spread Attributes above) |
|
|
225
254
|
| `index.ts` | Public API — re-exports the singleton `parser` instance |
|
|
226
255
|
|
|
227
256
|
## Documentation Map
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
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.18.3](https://github.com/markuplint/markuplint/compare/v4.18.2...v4.18.3) (2026-05-10)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- **astro-parser:** preserve spread attributes containing TypeScript and expression-child siblings ([fe76c34](https://github.com/markuplint/markuplint/commit/fe76c34f04b6ce2559c8fbfad01df525781ef34d)), closes [#3824](https://github.com/markuplint/markuplint/issues/3824)
|
|
11
|
+
- **astro-parser:** stop surfacing non-fatal Astro diagnostics as parse errors ([3b51f6c](https://github.com/markuplint/markuplint/commit/3b51f6c34d200c04006c283a62becb5733673fac)), closes [#3823](https://github.com/markuplint/markuplint/issues/3823)
|
|
12
|
+
|
|
13
|
+
# [4.18.0](https://github.com/markuplint/markuplint/compare/v4.14.1...v4.18.0) (2026-04-22)
|
|
14
|
+
|
|
15
|
+
**Note:** Version bump only for package @markuplint/astro-parser
|
|
16
|
+
|
|
17
|
+
## [4.6.24](https://github.com/markuplint/markuplint/compare/@markuplint/astro-parser@4.6.23...@markuplint/astro-parser@4.6.24) (2026-04-21)
|
|
18
|
+
|
|
19
|
+
**Note:** Version bump only for package @markuplint/astro-parser
|
|
20
|
+
|
|
6
21
|
## [4.6.23](https://github.com/markuplint/markuplint/compare/@markuplint/astro-parser@4.6.22...@markuplint/astro-parser@4.6.23) (2026-02-10)
|
|
7
22
|
|
|
8
23
|
**Note:** Version bump only for package @markuplint/astro-parser
|
package/docs/maintenance.ja.md
CHANGED
|
@@ -112,11 +112,12 @@ expect(debugMaps).toStrictEqual([
|
|
|
112
112
|
|
|
113
113
|
上流パッケージの変更がこのパーサーに影響を与える可能性があります:
|
|
114
114
|
|
|
115
|
-
| パッケージ
|
|
116
|
-
|
|
|
117
|
-
| `@markuplint/parser-utils`
|
|
118
|
-
| `@markuplint/
|
|
119
|
-
|
|
|
115
|
+
| パッケージ | 影響 |
|
|
116
|
+
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
117
|
+
| `@markuplint/parser-utils` | 基底 `Parser` クラスの変更は全オーバーライドメソッドに影響 |
|
|
118
|
+
| `@markuplint/parser-utils/script-parser` | TS 対応と非貪欲なパースが上流に入った場合、`src/spread-attr.ts` と `visitAttr()` のプリパスは削除可能 |
|
|
119
|
+
| `@markuplint/ml-ast` | AST 型の変更は `nodeize()` の戻り値の型に影響 |
|
|
120
|
+
| `astro-eslint-parser` | パーサー出力形式の変更は `tokenize()` と `nodeize()` に影響 |
|
|
120
121
|
|
|
121
122
|
`astro-eslint-parser` を更新する場合:
|
|
122
123
|
|
|
@@ -131,6 +132,23 @@ yarn upgrade @astrojs/compiler --scope @markuplint/astro-parser --dev
|
|
|
131
132
|
yarn build --scope @markuplint/astro-parser && yarn test --scope @markuplint/astro-parser
|
|
132
133
|
```
|
|
133
134
|
|
|
135
|
+
## dev (v5 RC) からの fix port 手順
|
|
136
|
+
|
|
137
|
+
`dev` と `v4` は自動マージ不可 — 両方に当てる必要がある fix は手動で port する。最も頻発する port hazard は `Token` のフィールド名差:
|
|
138
|
+
|
|
139
|
+
| v4 (本ブランチ) | dev (v5 RC) |
|
|
140
|
+
| ---------------------------------------------- | ----------------------------------------------------------- |
|
|
141
|
+
| `token.startOffset` / `startLine` / `startCol` | `token.offset` / `line` / `col` |
|
|
142
|
+
| `token.endOffset` / `endLine` / `endCol` | `#getEndLocation()` ヘルパが `{ offset, line, col }` を返す |
|
|
143
|
+
| `MLASTText` は `parentNode` のみ | `MLASTText` は `parentNodeUuid` も持つ |
|
|
144
|
+
|
|
145
|
+
dev fix を port する手順:
|
|
146
|
+
|
|
147
|
+
1. dev のソース変更とテストを両方読む。`token.line` / `token.col` / `token.offset` で assert しているテストは v4 では `startLine` / `startCol` / `startOffset` に書き換える。
|
|
148
|
+
2. v4 のフィールド名でソース変更を適用。`yarn build` で `TS2339` が出れば見落としを fail-fast に検出できる。
|
|
149
|
+
3. dev と同じ `#NNNN` describe id を v4 でも使う — id を揃えると後から両ブランチを grep で対比しやすい。
|
|
150
|
+
4. JSDoc / `parser-class.md` / 該当パッケージの `maintenance.md` Troubleshooting に dev / v4 の Issue・PR を相互リンク。次に port する人が関係を再発見しなくて済む。
|
|
151
|
+
|
|
134
152
|
## トラブルシューティング
|
|
135
153
|
|
|
136
154
|
### フロントマターが認識されない
|
|
@@ -181,3 +199,34 @@ yarn build --scope @markuplint/astro-parser && yarn test --scope @markuplint/ast
|
|
|
181
199
|
1. 属性名の形式を確認 — 正規表現はコロンが1つだけで、両側に空でない部分が必要
|
|
182
200
|
2. `switch (lowerCaseDirectiveName)` を確認 — ディレクティブプレフィックスがケースにマッチする必要がある
|
|
183
201
|
3. 新しいディレクティブプレフィックスの場合は、新しいケースを追加(レシピ #1 参照)
|
|
202
|
+
|
|
203
|
+
### スプレッド属性が途中で切れる、または `{...EXPR}` の直後に式の子があると `Invalid tag syntax` が発生する
|
|
204
|
+
|
|
205
|
+
**症状:** 次のいずれか:
|
|
206
|
+
|
|
207
|
+
- `{...{ command: 'close' } as any}` が部分的なスプレッドと不正な `as` / `any}` 属性に分割される。
|
|
208
|
+
- `<div {...props}>{label}</div>`(または `{label}` のような式の子をスプレッド属性の直後に持つ要素)が `SyntaxError: Invalid tag syntax: ...` をスローする。
|
|
209
|
+
|
|
210
|
+
**原因:** 要素の生トークンが、`src/spread-attr.ts` の波括弧対応抽出器ではなく `parser-utils/safeScriptParser`(espree ベース)にルーティングされている。`safeScriptParser` は TypeScript 構文(`as`)を理解せず、また「valid な JS プレフィックス」をスプレッドの `}` を超えて周囲の HTML まで伸ばすことがある(`{...props}>{label}` を二項 `>` 式として解釈)。
|
|
211
|
+
|
|
212
|
+
**解決策:**
|
|
213
|
+
|
|
214
|
+
1. `src/parser.ts` の `visitAttr()` が `super.visitAttr()` の **前**に `./spread-attr.js` の `extractSpreadAttribute()` を呼んでいることを確認。順序が重要 — 基底パスにフォールスルーするとバグが発動する。
|
|
215
|
+
2. 新しいエッジケースが報告されたら、`src/spread-attr.spec.ts` で `findMatchingBrace()` のユニットテストとして再現し、波括弧マッチャに追加のエスケープルール(文字列 / テンプレート / コメント / バックスラッシュ)が必要かを判断。
|
|
216
|
+
3. 既知の制限は `src/spread-attr.ts` の JSDoc に記載 — 波括弧を含む正規表現リテラルは扱えない。新たな制限が見つかったらそこに追記。
|
|
217
|
+
|
|
218
|
+
オリジナルの報告とスプレッド属性を `parser-utils` ではなく本パッケージで処理する根拠については [#3824](https://github.com/markuplint/markuplint/issues/3824) を参照。
|
|
219
|
+
|
|
220
|
+
### `<script>` または `<style>` の本文で `Invalid tag syntax` が throw する
|
|
221
|
+
|
|
222
|
+
**症状:** `<script>` 本文の JS/TS に HTML 風の部分文字列(典型的には `/<br\s*\/?>/gi` や `/<\/?p>/gi` のような正規表現)が含まれると `ParserError: SyntaxError: Invalid tag syntax: "..."` で落ちる。`<style>` 本文中の `/* <br = */` のような CSS コメントでも同種の症状が出る。
|
|
223
|
+
|
|
224
|
+
**原因:** `AstroParser.visitElement()` は要素全体(本文を含む)の raw 文字列を `parser-utils` の `parseCodeFragment()` に渡している。raw-text 認識がないと `parseCodeFragment()` は本文を HTML として再トークナイズし、正規表現中の `<br...>` をタグ開始と解釈、続く `\s`(リテラルなバックスラッシュ)を属性名とみなして throw する。
|
|
225
|
+
|
|
226
|
+
**解決策:** raw-text の安全性は `parser-utils/parser.ts` の `parseCodeFragment()` 内に実装されている。自閉でない開始タグの `nodeName.toLowerCase()` が `rawTextElements` に含まれる場合、本文は次に現れる ASCII 大文字小文字非依存の `</tagName`(タブ/LF/FF/CR/空白/`>` /`/` のいずれかが続く)まで丸ごと消費される(HTML LS §13.2.5.1)。リグレッションが疑われたら:
|
|
227
|
+
|
|
228
|
+
1. `npx vitest run packages/@markuplint/astro-parser/src/parser.spec.ts -t "#3860"` で失敗フィクスチャがまだ再現するかを確認。
|
|
229
|
+
2. 修正は `astro-parser` 内ではない。`packages/@markuplint/parser-utils/src/parser.ts` の `parseCodeFragment()` を見て raw-text 分岐が無傷か、close-tag 正規表現が仕様の文字クラスにまだ一致するかを確認する。
|
|
230
|
+
3. 別の上流 parser が `astro-parser` と同様に要素全体の raw を `parseCodeFragment` に渡すケースが追加された場合、追加配線は不要 — 同じ parser-utils 分岐がそのままカバーする。
|
|
231
|
+
|
|
232
|
+
オリジナルの報告は [#3860](https://github.com/markuplint/markuplint/issues/3860)([#3825](https://github.com/markuplint/markuplint/issues/3825) の v4 backport)を参照。dev (v5 RC) には [#3859](https://github.com/markuplint/markuplint/pull/3859) が先行マージされている。今後 raw-text 周りの変更を port する際は両 PR を diff 比較すること。
|
package/docs/maintenance.md
CHANGED
|
@@ -112,11 +112,12 @@ The second argument `true` to `nodeListToDebugMaps` includes attribute details i
|
|
|
112
112
|
|
|
113
113
|
Changes to upstream packages can affect this parser:
|
|
114
114
|
|
|
115
|
-
| Package
|
|
116
|
-
|
|
|
117
|
-
| `@markuplint/parser-utils`
|
|
118
|
-
| `@markuplint/
|
|
119
|
-
|
|
|
115
|
+
| Package | Impact |
|
|
116
|
+
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
117
|
+
| `@markuplint/parser-utils` | Base `Parser` class changes affect all override methods |
|
|
118
|
+
| `@markuplint/parser-utils/script-parser` | If TS support and non-greedy parsing land here, `src/spread-attr.ts` and the `visitAttr()` pre-pass can be removed |
|
|
119
|
+
| `@markuplint/ml-ast` | AST type changes affect `nodeize()` return types |
|
|
120
|
+
| `astro-eslint-parser` | Parser output format changes affect `tokenize()` and `nodeize()` |
|
|
120
121
|
|
|
121
122
|
When updating `astro-eslint-parser`:
|
|
122
123
|
|
|
@@ -131,6 +132,23 @@ yarn upgrade @astrojs/compiler --scope @markuplint/astro-parser --dev
|
|
|
131
132
|
yarn build --scope @markuplint/astro-parser && yarn test --scope @markuplint/astro-parser
|
|
132
133
|
```
|
|
133
134
|
|
|
135
|
+
## Porting Fixes from dev (v5 RC)
|
|
136
|
+
|
|
137
|
+
`dev` and `v4` cannot be merged automatically — fixes that need to land on both branches are ported manually. The most common port hazard is the `Token` field rename:
|
|
138
|
+
|
|
139
|
+
| v4 (this branch) | dev (v5 RC) |
|
|
140
|
+
| ---------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
141
|
+
| `token.startOffset` / `startLine` / `startCol` | `token.offset` / `line` / `col` |
|
|
142
|
+
| `token.endOffset` / `endLine` / `endCol` | computed via `#getEndLocation()` helper that returns `{ offset, line, col }` |
|
|
143
|
+
| `MLASTText` has only `parentNode` | `MLASTText` adds `parentNodeUuid` |
|
|
144
|
+
|
|
145
|
+
Procedure when porting a dev fix:
|
|
146
|
+
|
|
147
|
+
1. Read both the dev source change and the dev tests, including any test that asserts on `token.line` / `token.col` / `token.offset` — those need to become `startLine` / `startCol` / `startOffset` on v4.
|
|
148
|
+
2. Apply the source change with the v4 field names. `yarn build` should fail fast with `TS2339` on any field you missed.
|
|
149
|
+
3. Re-run the v4 test fixtures against the same `#NNNN` describe id used on dev — keeping the id stable lets future maintainers grep across both branches.
|
|
150
|
+
4. Cross-link both Issues / PRs in the JSDoc, `parser-class.md`, and the package's `maintenance.md` Troubleshooting entry so the next porter doesn't have to re-discover the relationship.
|
|
151
|
+
|
|
134
152
|
## Troubleshooting
|
|
135
153
|
|
|
136
154
|
### Frontmatter is not recognized
|
|
@@ -181,3 +199,34 @@ yarn build --scope @markuplint/astro-parser && yarn test --scope @markuplint/ast
|
|
|
181
199
|
1. Verify the attribute name format — the regex requires exactly one colon with non-empty parts on both sides
|
|
182
200
|
2. Check the `switch (lowerCaseDirectiveName)` — the directive prefix must match a case
|
|
183
201
|
3. If it is a new directive prefix, add a new case (see Recipe #1)
|
|
202
|
+
|
|
203
|
+
### Spread attribute is truncated, or `Invalid tag syntax` is thrown for `{...EXPR}` followed by an expression child
|
|
204
|
+
|
|
205
|
+
**Symptom:** Either of the following:
|
|
206
|
+
|
|
207
|
+
- `{...{ command: 'close' } as any}` is split into a partial spread plus bogus `as` / `any}` attributes.
|
|
208
|
+
- `<div {...props}>{label}</div>` (or any element with a spread attribute followed immediately by an expression child like `{label}`) throws `SyntaxError: Invalid tag syntax: ...`.
|
|
209
|
+
|
|
210
|
+
**Cause:** The element's raw token is being routed through `parser-utils/safeScriptParser` (espree-based) instead of the brace-aware extractor in `src/spread-attr.ts`. `safeScriptParser` does not understand TypeScript syntax (`as`) and may extend a "valid JS prefix" past the spread's `}` into the surrounding HTML (interpreting `{...props}>{label}` as a binary `>` expression).
|
|
211
|
+
|
|
212
|
+
**Solution:**
|
|
213
|
+
|
|
214
|
+
1. Confirm `src/parser.ts` `visitAttr()` calls `extractSpreadAttribute()` from `./spread-attr.js` _before_ `super.visitAttr()`. The order matters — falling through to the base path is what triggers the bug.
|
|
215
|
+
2. If a new edge case is reported, reproduce it as a unit test in `src/spread-attr.spec.ts` with `findMatchingBrace()` and decide whether the brace matcher needs an additional escape rule (string / template / comment / backslash).
|
|
216
|
+
3. Known limitations are listed in `src/spread-attr.ts` JSDoc — regular-expression literals containing braces are not handled. Document any additional limitations there.
|
|
217
|
+
|
|
218
|
+
See [#3824](https://github.com/markuplint/markuplint/issues/3824) for the original report and the rationale for handling spread attributes locally instead of in `parser-utils`.
|
|
219
|
+
|
|
220
|
+
### `<script>` or `<style>` body throws `Invalid tag syntax`
|
|
221
|
+
|
|
222
|
+
**Symptom:** A `<script>` body containing JavaScript / TypeScript with HTML-like substrings (most commonly a regex such as `/<br\s*\/?>/gi` or `/<\/?p>/gi`) throws `ParserError: SyntaxError: Invalid tag syntax: "..."`. Same for `<style>` bodies that include CSS comments such as `/* <br = */`.
|
|
223
|
+
|
|
224
|
+
**Cause:** `AstroParser.visitElement()` passes the entire element source (including body) to `parser-utils`'s `parseCodeFragment()`. Without raw-text awareness, `parseCodeFragment()` would treat the body as HTML and try to tokenize the regex's `<br...>` as a start tag, hitting `\s` (literal backslash) where an attribute name is expected.
|
|
225
|
+
|
|
226
|
+
**Solution:** Raw-text safety lives in `parser-utils/parser.ts` `parseCodeFragment()` — after a non-self-closing start tag whose `nodeName.toLowerCase()` matches `rawTextElements`, the body is consumed verbatim until the next ASCII-case-insensitive `</tagName` followed by a tab/LF/FF/CR/space/`>` /`/` (HTML LS §13.2.5.1). If a regression appears here:
|
|
227
|
+
|
|
228
|
+
1. Verify the failing fixture still reproduces by running `npx vitest run packages/@markuplint/astro-parser/src/parser.spec.ts -t "#3860"`.
|
|
229
|
+
2. The fix is _not_ inside `astro-parser` — inspect `packages/@markuplint/parser-utils/src/parser.ts` `parseCodeFragment()` to confirm the raw-text branch is intact and the close-tag regex still matches the spec character class.
|
|
230
|
+
3. If a different upstream parser is added that hands the full element raw to `parseCodeFragment` (similar to `astro-parser`), no extra wiring is needed — the same parser-utils branch covers it.
|
|
231
|
+
|
|
232
|
+
See [#3860](https://github.com/markuplint/markuplint/issues/3860) (v4 backport of [#3825](https://github.com/markuplint/markuplint/issues/3825)) for the original report. The dev (v5 RC) implementation landed first via [#3859](https://github.com/markuplint/markuplint/pull/3859) — diff-compare both PRs when porting future raw-text changes.
|
package/lib/astro-parser.d.ts
CHANGED
|
@@ -2,9 +2,39 @@ import type { RootNode } from '@astrojs/compiler/types';
|
|
|
2
2
|
export type { RootNode, ElementNode, CustomElementNode, ComponentNode, FragmentNode, AttributeNode, Node, } from '@astrojs/compiler/types';
|
|
3
3
|
/**
|
|
4
4
|
* Parses an Astro component source string into the Astro compiler's root AST node.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* ## Diagnostic handling policy
|
|
7
|
+
*
|
|
8
|
+
* Only **severity=Error** Astro diagnostics surface as a `ParserError`.
|
|
9
|
+
* Warning / Information / Hint diagnostics are passed through silently
|
|
10
|
+
* because the AST is still fully populated for those levels and Astro's own
|
|
11
|
+
* tooling (the language server, `astro check`) owns the user-facing message
|
|
12
|
+
* — markuplint must not surface them as fatal `parse-error`s (#3823).
|
|
13
|
+
*
|
|
14
|
+
* Concretely, the `is:inline` Hint that Astro emits for a `<script>` tag
|
|
15
|
+
* carrying any non-`src` attribute, and the `set:html` Warning for
|
|
16
|
+
* `set:html` overwriting children, both reach this wrapper but do not
|
|
17
|
+
* abort parsing.
|
|
18
|
+
*
|
|
19
|
+
* ## Error normalization
|
|
20
|
+
*
|
|
21
|
+
* `parseTemplate()` itself throws on severity=Error diagnostics and on raw
|
|
22
|
+
* template syntax errors (unterminated comments, unclosed expressions,
|
|
23
|
+
* etc.) by raising a `SyntaxError`-derived `ParseError`. Those throws are
|
|
24
|
+
* caught and normalized to `ParserError`, so the caller sees a single
|
|
25
|
+
* Tier-3 (per-file violation) error type for every parse failure rather
|
|
26
|
+
* than a Tier-1 fatal `SyntaxError` that would abort the whole lint run.
|
|
27
|
+
*
|
|
28
|
+
* The defensive `find(severity === Error)` after `parseTemplate()` is dead
|
|
29
|
+
* code today (upstream gates internally) but is retained as a safety net
|
|
30
|
+
* if a future `astro-eslint-parser` ever stops gating severity=Error.
|
|
31
|
+
*
|
|
32
|
+
* @see https://github.com/markuplint/markuplint/issues/3823
|
|
33
|
+
* @see https://docs.astro.build/en/reference/directives-reference/#isinline
|
|
34
|
+
* @see https://docs.astro.build/en/guides/client-side-scripts/#script-processing
|
|
6
35
|
*
|
|
7
36
|
* @param code - The raw Astro component source code
|
|
8
37
|
* @returns The root AST node produced by the Astro compiler
|
|
38
|
+
* @throws {ParserError} on severity=Error Astro diagnostics or on raw upstream syntax errors
|
|
9
39
|
*/
|
|
10
40
|
export declare function astroParse(code: string): RootNode;
|
package/lib/astro-parser.js
CHANGED
|
@@ -1,19 +1,74 @@
|
|
|
1
1
|
import { ParserError } from '@markuplint/parser-utils';
|
|
2
2
|
import { parseTemplate } from 'astro-eslint-parser';
|
|
3
|
+
/**
|
|
4
|
+
* Astro tags each diagnostic with a VS Code-style severity
|
|
5
|
+
* (1=Error, 2=Warning, 3=Information, 4=Hint). The compiler exposes the
|
|
6
|
+
* enum as types-only via `@astrojs/compiler/types` — there is no runtime
|
|
7
|
+
* value to import — so the fatal level is mirrored as a literal here.
|
|
8
|
+
*
|
|
9
|
+
* @see https://github.com/withastro/compiler/blob/main/packages/compiler/shared/types.ts
|
|
10
|
+
*/
|
|
11
|
+
const ASTRO_DIAGNOSTIC_SEVERITY_ERROR = 1;
|
|
3
12
|
/**
|
|
4
13
|
* Parses an Astro component source string into the Astro compiler's root AST node.
|
|
5
|
-
*
|
|
14
|
+
*
|
|
15
|
+
* ## Diagnostic handling policy
|
|
16
|
+
*
|
|
17
|
+
* Only **severity=Error** Astro diagnostics surface as a `ParserError`.
|
|
18
|
+
* Warning / Information / Hint diagnostics are passed through silently
|
|
19
|
+
* because the AST is still fully populated for those levels and Astro's own
|
|
20
|
+
* tooling (the language server, `astro check`) owns the user-facing message
|
|
21
|
+
* — markuplint must not surface them as fatal `parse-error`s (#3823).
|
|
22
|
+
*
|
|
23
|
+
* Concretely, the `is:inline` Hint that Astro emits for a `<script>` tag
|
|
24
|
+
* carrying any non-`src` attribute, and the `set:html` Warning for
|
|
25
|
+
* `set:html` overwriting children, both reach this wrapper but do not
|
|
26
|
+
* abort parsing.
|
|
27
|
+
*
|
|
28
|
+
* ## Error normalization
|
|
29
|
+
*
|
|
30
|
+
* `parseTemplate()` itself throws on severity=Error diagnostics and on raw
|
|
31
|
+
* template syntax errors (unterminated comments, unclosed expressions,
|
|
32
|
+
* etc.) by raising a `SyntaxError`-derived `ParseError`. Those throws are
|
|
33
|
+
* caught and normalized to `ParserError`, so the caller sees a single
|
|
34
|
+
* Tier-3 (per-file violation) error type for every parse failure rather
|
|
35
|
+
* than a Tier-1 fatal `SyntaxError` that would abort the whole lint run.
|
|
36
|
+
*
|
|
37
|
+
* The defensive `find(severity === Error)` after `parseTemplate()` is dead
|
|
38
|
+
* code today (upstream gates internally) but is retained as a safety net
|
|
39
|
+
* if a future `astro-eslint-parser` ever stops gating severity=Error.
|
|
40
|
+
*
|
|
41
|
+
* @see https://github.com/markuplint/markuplint/issues/3823
|
|
42
|
+
* @see https://docs.astro.build/en/reference/directives-reference/#isinline
|
|
43
|
+
* @see https://docs.astro.build/en/guides/client-side-scripts/#script-processing
|
|
6
44
|
*
|
|
7
45
|
* @param code - The raw Astro component source code
|
|
8
46
|
* @returns The root AST node produced by the Astro compiler
|
|
47
|
+
* @throws {ParserError} on severity=Error Astro diagnostics or on raw upstream syntax errors
|
|
9
48
|
*/
|
|
10
49
|
export function astroParse(code) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
50
|
+
let result;
|
|
51
|
+
try {
|
|
52
|
+
({ result } = parseTemplate(code));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (!(error instanceof SyntaxError)) {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
const { lineNumber, column } = error;
|
|
59
|
+
throw new ParserError(error.message, {
|
|
60
|
+
line: lineNumber ?? 1,
|
|
61
|
+
col: column ?? 0,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// Defensive: if a future `astro-eslint-parser` stops gating severity=Error
|
|
65
|
+
// diagnostics internally, surface them here so they never silently leak
|
|
66
|
+
// past the wrapper as a non-fatal pass-through.
|
|
67
|
+
const fatal = result.diagnostics.find(d => d.severity === ASTRO_DIAGNOSTIC_SEVERITY_ERROR);
|
|
68
|
+
if (fatal) {
|
|
69
|
+
throw new ParserError(fatal.text, {
|
|
70
|
+
line: fatal.location.line,
|
|
71
|
+
col: fatal.location.column,
|
|
17
72
|
});
|
|
18
73
|
}
|
|
19
74
|
return result.ast;
|
package/lib/parser.d.ts
CHANGED
|
@@ -54,14 +54,14 @@ declare class AstroParser extends Parser<Node, State> {
|
|
|
54
54
|
/**
|
|
55
55
|
* Visits an attribute token, handling Astro-specific syntax including
|
|
56
56
|
* curly-brace expression values, shorthand attributes (`{name}`),
|
|
57
|
-
*
|
|
57
|
+
* spread attributes (`{...expr}`, including TypeScript and nested
|
|
58
|
+
* expressions, see #3824), and template directives (e.g., `class:list`,
|
|
59
|
+
* `set:html`).
|
|
58
60
|
*
|
|
59
61
|
* @param token - The token representing the attribute
|
|
60
62
|
* @returns The parsed attribute node with Astro-specific metadata
|
|
61
63
|
*/
|
|
62
|
-
visitAttr(token: Token):
|
|
63
|
-
__rightText?: string;
|
|
64
|
-
}) | {
|
|
64
|
+
visitAttr(token: Token): import("@markuplint/ml-ast").MLASTSpreadAttr | {
|
|
65
65
|
isDynamicValue: true | undefined;
|
|
66
66
|
isDirective: true | undefined;
|
|
67
67
|
potentialName: string | undefined;
|
package/lib/parser.js
CHANGED
|
@@ -6,6 +6,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
6
6
|
var _AstroParser_instances, _AstroParser_updateScopeNS;
|
|
7
7
|
import { AttrState, Parser, ParserError } from '@markuplint/parser-utils';
|
|
8
8
|
import { astroParse } from './astro-parser.js';
|
|
9
|
+
import { extractSpreadAttribute } from './spread-attr.js';
|
|
9
10
|
/**
|
|
10
11
|
* Parser implementation for Astro component templates.
|
|
11
12
|
* Extends the base Parser to handle Astro-specific syntax including frontmatter blocks,
|
|
@@ -201,12 +202,41 @@ class AstroParser extends Parser {
|
|
|
201
202
|
/**
|
|
202
203
|
* Visits an attribute token, handling Astro-specific syntax including
|
|
203
204
|
* curly-brace expression values, shorthand attributes (`{name}`),
|
|
204
|
-
*
|
|
205
|
+
* spread attributes (`{...expr}`, including TypeScript and nested
|
|
206
|
+
* expressions, see #3824), and template directives (e.g., `class:list`,
|
|
207
|
+
* `set:html`).
|
|
205
208
|
*
|
|
206
209
|
* @param token - The token representing the attribute
|
|
207
210
|
* @returns The parsed attribute node with Astro-specific metadata
|
|
208
211
|
*/
|
|
209
212
|
visitAttr(token) {
|
|
213
|
+
const spreadHit = extractSpreadAttribute(token.raw);
|
|
214
|
+
if (spreadHit) {
|
|
215
|
+
let spreadStartLine = token.startLine;
|
|
216
|
+
let spreadStartCol = token.startCol;
|
|
217
|
+
for (const c of spreadHit.leadingSpace) {
|
|
218
|
+
if (c === '\n') {
|
|
219
|
+
spreadStartLine++;
|
|
220
|
+
spreadStartCol = 1;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
spreadStartCol++;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const spread = super.visitSpreadAttr({
|
|
227
|
+
raw: spreadHit.spreadRaw,
|
|
228
|
+
startOffset: token.startOffset + spreadHit.leadingSpace.length,
|
|
229
|
+
startLine: spreadStartLine,
|
|
230
|
+
startCol: spreadStartCol,
|
|
231
|
+
});
|
|
232
|
+
// `extractSpreadAttribute` already validates the `{...EXPR}` shape
|
|
233
|
+
// so `super.visitSpreadAttr` is expected to return a node here.
|
|
234
|
+
// Falling through to the generic attr path is a defensive safeguard
|
|
235
|
+
// against future shape changes in the parent class.
|
|
236
|
+
if (spread) {
|
|
237
|
+
return spreadHit.leftover ? { ...spread, __rightText: spreadHit.leftover } : spread;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
210
240
|
const attr = super.visitAttr(token, {
|
|
211
241
|
quoteSet: [
|
|
212
242
|
{ start: '"', end: '"', type: 'string' },
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locates the matching closing brace for a JS-like expression that starts at
|
|
3
|
+
* `raw[start]` (which must be `{`), respecting string literals (`'`, `"`),
|
|
4
|
+
* template literals with `${}` interpolation, and line/block comments.
|
|
5
|
+
*
|
|
6
|
+
* Returns the index of the matching `}`, or -1 if no match is found.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists: `safeScriptParser` (espree-based) does not understand
|
|
9
|
+
* TypeScript syntax such as `{...x as any}` and may also extend a "valid JS
|
|
10
|
+
* prefix" past the spread's closing brace into surrounding HTML
|
|
11
|
+
* (e.g. `{...props}>{label}` is parsed as a binary `>` expression),
|
|
12
|
+
* misclassifying both the spread end and any expression-child siblings.
|
|
13
|
+
* See https://github.com/markuplint/markuplint/issues/3824.
|
|
14
|
+
*
|
|
15
|
+
* Known limitation: regular-expression literals containing braces
|
|
16
|
+
* (e.g. `{...x.match(/}/) ? a : b}`) are not recognised — `/` is always
|
|
17
|
+
* treated as a division operator. Such patterns are vanishingly rare in
|
|
18
|
+
* Astro spread attributes; rewrite via a variable indirection if needed.
|
|
19
|
+
*/
|
|
20
|
+
export declare function findMatchingBrace(raw: string, start: number): number;
|
|
21
|
+
/**
|
|
22
|
+
* Detects whether the given attribute token starts with an Astro spread
|
|
23
|
+
* attribute (`{...EXPR}`) and, if so, returns the spread's exact slice plus
|
|
24
|
+
* the leading whitespace and any leftover trailing text after the spread.
|
|
25
|
+
*
|
|
26
|
+
* Returns null when the token is not a spread attribute.
|
|
27
|
+
*
|
|
28
|
+
* Exported so the brace-matching logic can be unit-tested independently of
|
|
29
|
+
* the parser pipeline.
|
|
30
|
+
*/
|
|
31
|
+
export declare function extractSpreadAttribute(raw: string): {
|
|
32
|
+
leadingSpace: string;
|
|
33
|
+
spreadRaw: string;
|
|
34
|
+
leftover: string;
|
|
35
|
+
} | null;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts consecutive backslashes immediately before position `i` in `raw`.
|
|
3
|
+
* An odd count means the character at `i` is escaped; an even count means
|
|
4
|
+
* the preceding backslashes are themselves paired escapes.
|
|
5
|
+
*/
|
|
6
|
+
function countPrecedingBackslashes(raw, i) {
|
|
7
|
+
let n = 0;
|
|
8
|
+
let j = i - 1;
|
|
9
|
+
while (j >= 0 && raw[j] === '\\') {
|
|
10
|
+
n++;
|
|
11
|
+
j--;
|
|
12
|
+
}
|
|
13
|
+
return n;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Locates the matching closing brace for a JS-like expression that starts at
|
|
17
|
+
* `raw[start]` (which must be `{`), respecting string literals (`'`, `"`),
|
|
18
|
+
* template literals with `${}` interpolation, and line/block comments.
|
|
19
|
+
*
|
|
20
|
+
* Returns the index of the matching `}`, or -1 if no match is found.
|
|
21
|
+
*
|
|
22
|
+
* Why this exists: `safeScriptParser` (espree-based) does not understand
|
|
23
|
+
* TypeScript syntax such as `{...x as any}` and may also extend a "valid JS
|
|
24
|
+
* prefix" past the spread's closing brace into surrounding HTML
|
|
25
|
+
* (e.g. `{...props}>{label}` is parsed as a binary `>` expression),
|
|
26
|
+
* misclassifying both the spread end and any expression-child siblings.
|
|
27
|
+
* See https://github.com/markuplint/markuplint/issues/3824.
|
|
28
|
+
*
|
|
29
|
+
* Known limitation: regular-expression literals containing braces
|
|
30
|
+
* (e.g. `{...x.match(/}/) ? a : b}`) are not recognised — `/` is always
|
|
31
|
+
* treated as a division operator. Such patterns are vanishingly rare in
|
|
32
|
+
* Astro spread attributes; rewrite via a variable indirection if needed.
|
|
33
|
+
*/
|
|
34
|
+
export function findMatchingBrace(raw, start) {
|
|
35
|
+
if (raw[start] !== '{')
|
|
36
|
+
return -1;
|
|
37
|
+
let depth = 0;
|
|
38
|
+
let inString = null;
|
|
39
|
+
const templateBraceStack = [];
|
|
40
|
+
for (let i = start; i < raw.length; i++) {
|
|
41
|
+
const c = raw[i];
|
|
42
|
+
if (inString) {
|
|
43
|
+
if (inString === '`') {
|
|
44
|
+
if (c === '`' && countPrecedingBackslashes(raw, i) % 2 === 0) {
|
|
45
|
+
inString = null;
|
|
46
|
+
}
|
|
47
|
+
else if (c === '$' && raw[i + 1] === '{') {
|
|
48
|
+
templateBraceStack.push(depth);
|
|
49
|
+
inString = null;
|
|
50
|
+
depth++;
|
|
51
|
+
i++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else if (c === inString && countPrecedingBackslashes(raw, i) % 2 === 0) {
|
|
55
|
+
inString = null;
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (c === '/' && raw[i + 1] === '/') {
|
|
60
|
+
while (i < raw.length && raw[i] !== '\n')
|
|
61
|
+
i++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (c === '/' && raw[i + 1] === '*') {
|
|
65
|
+
i += 2;
|
|
66
|
+
while (i < raw.length - 1 && !(raw[i] === '*' && raw[i + 1] === '/'))
|
|
67
|
+
i++;
|
|
68
|
+
i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
72
|
+
inString = c;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (c === '{') {
|
|
76
|
+
depth++;
|
|
77
|
+
}
|
|
78
|
+
else if (c === '}') {
|
|
79
|
+
depth--;
|
|
80
|
+
if (depth === 0)
|
|
81
|
+
return i;
|
|
82
|
+
if (templateBraceStack.length > 0 && depth === templateBraceStack.at(-1)) {
|
|
83
|
+
templateBraceStack.pop();
|
|
84
|
+
inString = '`';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return -1;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Detects whether the given attribute token starts with an Astro spread
|
|
92
|
+
* attribute (`{...EXPR}`) and, if so, returns the spread's exact slice plus
|
|
93
|
+
* the leading whitespace and any leftover trailing text after the spread.
|
|
94
|
+
*
|
|
95
|
+
* Returns null when the token is not a spread attribute.
|
|
96
|
+
*
|
|
97
|
+
* Exported so the brace-matching logic can be unit-tested independently of
|
|
98
|
+
* the parser pipeline.
|
|
99
|
+
*/
|
|
100
|
+
export function extractSpreadAttribute(raw) {
|
|
101
|
+
const leadingMatch = /^\s*/.exec(raw);
|
|
102
|
+
const leadingSpace = leadingMatch?.[0] ?? '';
|
|
103
|
+
const remaining = raw.slice(leadingSpace.length);
|
|
104
|
+
// eslint-disable-next-line regexp/strict
|
|
105
|
+
if (!/^{\s*\.{3}[^.]/.test(remaining)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const end = findMatchingBrace(remaining, 0);
|
|
109
|
+
if (end < 0)
|
|
110
|
+
return null;
|
|
111
|
+
return {
|
|
112
|
+
leadingSpace,
|
|
113
|
+
spreadRaw: remaining.slice(0, end + 1),
|
|
114
|
+
leftover: remaining.slice(end + 1),
|
|
115
|
+
};
|
|
116
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markuplint/astro-parser",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.18.3",
|
|
4
4
|
"description": "astro parser for markuplint",
|
|
5
5
|
"repository": "git@github.com:markuplint/markuplint.git",
|
|
6
6
|
"author": "Yusuke Hirao <yusukehirao@me.com>",
|
|
@@ -21,12 +21,12 @@
|
|
|
21
21
|
"clean": "tsc --build --clean tsconfig.build.json"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@markuplint/ml-ast": "4.
|
|
25
|
-
"@markuplint/parser-utils": "4.
|
|
26
|
-
"astro-eslint-parser": "1.
|
|
24
|
+
"@markuplint/ml-ast": "4.18.0",
|
|
25
|
+
"@markuplint/parser-utils": "4.18.3",
|
|
26
|
+
"astro-eslint-parser": "1.4.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@astrojs/compiler": "
|
|
29
|
+
"@astrojs/compiler": "3.0.1"
|
|
30
30
|
},
|
|
31
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "c53026c2c600bb304f2088589f67303479e92e62"
|
|
32
32
|
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export function detectBlockBehavior(raw) {
|
|
2
|
-
const re = /\.+\s*(?<type>map|filter)\s*\((?:function\s*\(.[^\n\r{\u2028\u2029]*\{.*return\s*$|.+=>\s*\(?\s*)/;
|
|
3
|
-
const match = raw.match(re);
|
|
4
|
-
if (!match) {
|
|
5
|
-
return null;
|
|
6
|
-
}
|
|
7
|
-
const type = match.groups?.type === 'map' ? 'each' : 'if';
|
|
8
|
-
return {
|
|
9
|
-
type,
|
|
10
|
-
expression: raw,
|
|
11
|
-
};
|
|
12
|
-
}
|