Skip to content

Commit 5b85e3f

Browse files
committed
fix(core): text expression parsing and LSP file opening
1 parent d0dcc7e commit 5b85e3f

File tree

15 files changed

+690
-19
lines changed

15 files changed

+690
-19
lines changed

.changeset/proud-peaches-hide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed [#7837](https://github.com/biomejs/biome/issues/7837), where Biome couldn't properly parse text expressions that contained nested curly brackets. This was breaking parsing in Astro and Svelte files.

.changeset/wild-eels-send.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed [#7837](https://github.com/biomejs/biome/issues/7837), where Biome Language Server paniced when opening HTML-ish files when the experimental full support is enabled.

crates/biome_html_parser/src/lexer/mod.rs

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -221,22 +221,33 @@ impl<'src> HtmlLexer<'src> {
221221
}
222222
}
223223

224-
fn consume_single_text_expression(&mut self, current: u8) -> HtmlSyntaxKind {
225-
match current {
226-
b'}' if !self.at_closing_double_text_expression() => self.consume_byte(T!['}']),
227-
b'<' => self.consume_byte(T![<]),
228-
_ => {
229-
while let Some(current) = self.current_byte() {
230-
match current {
231-
b'}' if !self.at_closing_double_text_expression() => break,
232-
_ => {
233-
self.advance(1);
234-
}
224+
fn consume_single_text_expression(&mut self) -> HtmlSyntaxKind {
225+
let mut brackets_stack = Vec::<()>::new();
226+
if self.previous_byte() == Some(b'{') {
227+
brackets_stack.push(());
228+
}
229+
while let Some(current) = self.current_byte() {
230+
match current {
231+
b'}' => {
232+
brackets_stack.pop();
233+
if brackets_stack.is_empty() {
234+
break;
235+
} else {
236+
self.advance(1);
235237
}
236238
}
237-
HTML_LITERAL
239+
b'{' => {
240+
brackets_stack.push(());
241+
self.advance(1);
242+
}
243+
244+
_ => {
245+
self.advance(1);
246+
}
238247
}
239248
}
249+
250+
HTML_LITERAL
240251
}
241252

242253
fn consume_comment(&mut self) -> HtmlSyntaxKind {
@@ -767,7 +778,7 @@ impl<'src> Lexer<'src> for HtmlLexer<'src> {
767778
}
768779
HtmlLexContext::TextExpression(kind) => match kind {
769780
TextExpressionKind::Double => self.consume_double_text_expression(current),
770-
TextExpressionKind::Single => self.consume_single_text_expression(current),
781+
TextExpressionKind::Single => self.consume_single_text_expression(),
771782
},
772783
HtmlLexContext::CdataSection => self.consume_inside_cdata(current),
773784
HtmlLexContext::AstroFencedCodeBlock => {

crates/biome_html_parser/src/syntax/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ impl TextExpression {
570570
HtmlLexContext::TextExpression(self.kind),
571571
);
572572
} else {
573-
p.bump_remap_any_with_context(HtmlLexContext::TextExpression(self.kind));
573+
p.bump_remap_with_context(HTML_LITERAL, HtmlLexContext::InsideTag);
574574
}
575575
}
576576
TextExpressionKind::Double => {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
const items = [1, 2, 3];
3+
---
4+
5+
{items.map(item => <div>{item}</div>)
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
---
2+
source: crates/biome_html_parser/tests/spec_test.rs
3+
expression: snapshot
4+
---
5+
## Input
6+
7+
```astro
8+
---
9+
const items = [1, 2, 3];
10+
---
11+
12+
{items.map(item => <div>{item}</div>)
13+
14+
```
15+
16+
17+
## AST
18+
19+
```
20+
HtmlRoot {
21+
bom_token: missing (optional),
22+
frontmatter: AstroFrontmatterElement {
23+
l_fence_token: FENCE@0..3 "---" [] [],
24+
content: AstroEmbeddedContent {
25+
content_token: HTML_LITERAL@3..29 "const items = [1, 2, 3];\n" [Newline("\n")] [],
26+
},
27+
r_fence_token: FENCE@29..32 "---" [] [],
28+
},
29+
directive: missing (optional),
30+
html: HtmlElementList [
31+
HtmlBogusTextExpression {
32+
items: [
33+
L_CURLY@32..35 "{" [Newline("\n"), Newline("\n")] [],
34+
HTML_LITERAL@35..53 "items.map(item =>" [] [Whitespace(" ")],
35+
],
36+
},
37+
HtmlElement {
38+
opening_element: HtmlOpeningElement {
39+
l_angle_token: L_ANGLE@53..54 "<" [] [],
40+
name: HtmlTagName {
41+
value_token: HTML_LITERAL@54..57 "div" [] [],
42+
},
43+
attributes: HtmlAttributeList [],
44+
r_angle_token: R_ANGLE@57..58 ">" [] [],
45+
},
46+
children: HtmlElementList [
47+
HtmlSingleTextExpression {
48+
l_curly_token: L_CURLY@58..59 "{" [] [],
49+
expression: HtmlTextExpression {
50+
html_literal_token: HTML_LITERAL@59..63 "item" [] [],
51+
},
52+
r_curly_token: R_CURLY@63..64 "}" [] [],
53+
},
54+
],
55+
closing_element: HtmlClosingElement {
56+
l_angle_token: L_ANGLE@64..65 "<" [] [],
57+
slash_token: SLASH@65..66 "/" [] [],
58+
name: HtmlTagName {
59+
value_token: HTML_LITERAL@66..69 "div" [] [],
60+
},
61+
r_angle_token: R_ANGLE@69..70 ">" [] [],
62+
},
63+
},
64+
HtmlContent {
65+
value_token: HTML_LITERAL@70..71 ")" [] [],
66+
},
67+
],
68+
eof_token: EOF@71..72 "" [Newline("\n")] [],
69+
}
70+
```
71+
72+
## CST
73+
74+
```
75+
0: HTML_ROOT@0..72
76+
0: (empty)
77+
1: ASTRO_FRONTMATTER_ELEMENT@0..32
78+
0: FENCE@0..3 "---" [] []
79+
1: ASTRO_EMBEDDED_CONTENT@3..29
80+
0: HTML_LITERAL@3..29 "const items = [1, 2, 3];\n" [Newline("\n")] []
81+
2: FENCE@29..32 "---" [] []
82+
2: (empty)
83+
3: HTML_ELEMENT_LIST@32..71
84+
0: HTML_BOGUS_TEXT_EXPRESSION@32..53
85+
0: L_CURLY@32..35 "{" [Newline("\n"), Newline("\n")] []
86+
1: HTML_LITERAL@35..53 "items.map(item =>" [] [Whitespace(" ")]
87+
1: HTML_ELEMENT@53..70
88+
0: HTML_OPENING_ELEMENT@53..58
89+
0: L_ANGLE@53..54 "<" [] []
90+
1: HTML_TAG_NAME@54..57
91+
0: HTML_LITERAL@54..57 "div" [] []
92+
2: HTML_ATTRIBUTE_LIST@57..57
93+
3: R_ANGLE@57..58 ">" [] []
94+
1: HTML_ELEMENT_LIST@58..64
95+
0: HTML_SINGLE_TEXT_EXPRESSION@58..64
96+
0: L_CURLY@58..59 "{" [] []
97+
1: HTML_TEXT_EXPRESSION@59..63
98+
0: HTML_LITERAL@59..63 "item" [] []
99+
2: R_CURLY@63..64 "}" [] []
100+
2: HTML_CLOSING_ELEMENT@64..70
101+
0: L_ANGLE@64..65 "<" [] []
102+
1: SLASH@65..66 "/" [] []
103+
2: HTML_TAG_NAME@66..69
104+
0: HTML_LITERAL@66..69 "div" [] []
105+
3: R_ANGLE@69..70 ">" [] []
106+
2: HTML_CONTENT@70..71
107+
0: HTML_LITERAL@70..71 ")" [] []
108+
4: EOF@71..72 "" [Newline("\n")] []
109+
110+
```
111+
112+
## Diagnostics
113+
114+
```
115+
nested_expression.astro:5:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
116+
117+
× Found a text expression that doesn't have the closing expression:
118+
119+
3---
120+
4
121+
> 5 │ {items.map(item => <div>{item}</div>)
122+
│ ^^^^^^^^^^^^^^^^^^
123+
6 │
124+
125+
i This is where the opening expression was found:
126+
127+
3 │ ---
128+
4 │
129+
> 5 │ {items.map(item => <div>{item}</div>)
130+
^
131+
6
132+
133+
```
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
import { Counter } from '@/components/counter'
3+
import PageLayout from '@/components/page-layout.astro'
4+
5+
// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build
6+
// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh.
7+
---
8+
<PageLayout>
9+
<main class="flex h-screen w-screen flex-col items-center justify-center">
10+
<Counter client:load>
11+
12+
13+
<span>--</span>
14+
</Counter>
15+
</main>
16+
</PageLayout>

0 commit comments

Comments
 (0)