Skip to content

Commit 96ca7b6

Browse files
committed
refactor: consider all attributes that start with v- to be vue directives
1 parent d3011ba commit 96ca7b6

31 files changed

+9076
-53
lines changed

crates/biome_html_factory/src/generated/syntax_factory.rs

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_html_parser/src/syntax/mod.rs

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ impl SyntaxFeature for HtmlSyntaxFeatures {
5656
const RECOVER_ATTRIBUTE_LIST: TokenSet<HtmlSyntaxKind> = token_set!(T![>], T![<], T![/]);
5757
const RECOVER_TEXT_EXPRESSION_LIST: TokenSet<HtmlSyntaxKind> =
5858
token_set!(T![<], T![>], T!['}'], T!["}}"]);
59-
const VUE_DIRECTIVE_CHARS: TokenSet<HtmlSyntaxKind> = token_set![T![@], T![.], T![:]];
6059

6160
/// These elements are effectively always self-closing. They should not have a closing tag (if they do, it should be a parsing error). They might not contain a `/` like in `<img />`.
6261
static VOID_ELEMENTS: &[&str] = &[
@@ -315,7 +314,7 @@ impl ParseNodeList for AttributeList {
315314
}
316315

317316
fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool {
318-
p.at(T![>]) || p.at(T![/]) || p.at(EOF) || p.at(T!['}'])
317+
p.at(T![>]) || p.at(T![/]) || p.at(T!['}'])
319318
}
320319

321320
fn recover(
@@ -336,7 +335,6 @@ fn parse_attribute(p: &mut HtmlParser) -> ParsedSyntax {
336335
return Absent;
337336
}
338337

339-
let chpt = p.checkpoint();
340338
match p.cur() {
341339
T!["{{"] => {
342340
let m = p.start();
@@ -381,20 +379,13 @@ fn parse_attribute(p: &mut HtmlParser) -> ParsedSyntax {
381379
|p| parse_attach_attribute(p),
382380
|p: &HtmlParser<'_>, m: &CompletedMarker| disabled_svelte_prop(p, m.range(p)),
383381
),
382+
_ if p.cur_text().starts_with("v-") => {
383+
HtmlSyntaxFeatures::Vue
384+
.parse_exclusive_syntax(p, parse_vue_directive, |p, m| disabled_vue(p, m.range(p)))
385+
}
384386
_ => {
385387
let m = p.start();
386388
parse_literal(p, HTML_ATTRIBUTE_NAME).or_add_diagnostic(p, expected_attribute);
387-
// Here we harness cases where we parse an attribute like: <i class.bind="icon">
388-
// The parser correctly reads class, but then we find `.`, which we know to be a Vue syntax
389-
if p.at_ts(VUE_DIRECTIVE_CHARS) {
390-
m.abandon(p);
391-
p.rewind(chpt);
392-
return HtmlSyntaxFeatures::Vue.parse_exclusive_syntax(
393-
p,
394-
parse_vue_directive,
395-
|p, m| disabled_vue(p, m.range(p)),
396-
);
397-
}
398389

399390
if p.at(T![=]) {
400391
parse_attribute_initializer(p).ok();

crates/biome_html_parser/src/syntax/vue.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@ use biome_parser::parse_recovery::ParseRecoveryTokenSet;
1212
use biome_parser::parsed_syntax::ParsedSyntax::{Absent, Present};
1313
use biome_parser::prelude::*;
1414

15-
pub fn parse_vue_directive(p: &mut HtmlParser) -> ParsedSyntax {
15+
pub(crate) fn parse_vue_directive(p: &mut HtmlParser) -> ParsedSyntax {
16+
if !p.at(HTML_LITERAL) {
17+
return Absent;
18+
}
19+
1620
let m = p.start();
1721

18-
p.bump_with_context(HTML_LITERAL, HtmlLexContext::InsideTagVue);
22+
// FIXME: Ideally, the lexer would just lex VUE_IDENT directly
23+
p.bump_remap_with_context(VUE_IDENT, HtmlLexContext::InsideTagVue);
1924
if p.at(T![:]) {
2025
parse_vue_directive_argument(p).ok();
2126
}
@@ -27,7 +32,7 @@ pub fn parse_vue_directive(p: &mut HtmlParser) -> ParsedSyntax {
2732
Present(m.complete(p, VUE_DIRECTIVE))
2833
}
2934

30-
pub fn parse_vue_v_bind_shorthand_directive(p: &mut HtmlParser) -> ParsedSyntax {
35+
pub(crate) fn parse_vue_v_bind_shorthand_directive(p: &mut HtmlParser) -> ParsedSyntax {
3136
if !p.at(T![:]) {
3237
return Absent;
3338
}
@@ -49,7 +54,7 @@ pub fn parse_vue_v_bind_shorthand_directive(p: &mut HtmlParser) -> ParsedSyntax
4954
Present(m.complete(p, VUE_V_BIND_SHORTHAND_DIRECTIVE))
5055
}
5156

52-
pub fn parse_vue_v_on_shorthand_directive(p: &mut HtmlParser) -> ParsedSyntax {
57+
pub(crate) fn parse_vue_v_on_shorthand_directive(p: &mut HtmlParser) -> ParsedSyntax {
5358
if !p.at(T![@]) {
5459
return Absent;
5560
}
@@ -98,7 +103,7 @@ fn parse_vue_dynamic_argument(p: &mut HtmlParser) -> ParsedSyntax {
98103

99104
let m = p.start();
100105

101-
p.expect_with_context(T!['['], HtmlLexContext::InsideTagVue);
106+
p.bump_with_context(T!['['], HtmlLexContext::InsideTagVue);
102107
p.expect_with_context(HTML_LITERAL, HtmlLexContext::InsideTagVue);
103108
p.expect_with_context(T![']'], HtmlLexContext::InsideTagVue);
104109

@@ -117,7 +122,7 @@ impl ParseNodeList for VueModifierList {
117122
}
118123

119124
fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool {
120-
p.at(T![=])
125+
p.at(T![=]) || p.at(T![>]) || p.at(T![/]) || p.at(T!['}'])
121126
}
122127

123128
fn recover(
@@ -127,7 +132,10 @@ impl ParseNodeList for VueModifierList {
127132
) -> biome_parser::parse_recovery::RecoveryResult {
128133
parsed_element.or_recover_with_token_set(
129134
p,
130-
&ParseRecoveryTokenSet::new(VUE_BOGUS_DIRECTIVE, token_set![T![.], T![>]]),
135+
&ParseRecoveryTokenSet::new(
136+
VUE_BOGUS_DIRECTIVE,
137+
token_set![T![.], T![>], T![/], T!['}']],
138+
),
131139
expected_attribute,
132140
)
133141
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
<template>
22
<div v-bind:bar.lower="'FOO'"></div>
3-
</template>>
3+
</template>

crates/biome_html_parser/tests/html_specs/ok/vue/modifier.vue.snap

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ expression: snapshot
77
```vue
88
<template>
99
<div v-bind:bar.lower="'FOO'"></div>
10-
</template>>
11-
10+
</template>
1211
```
1312
1413
@@ -38,7 +37,7 @@ HtmlRoot {
3837
},
3938
attributes: HtmlAttributeList [
4039
VueDirective {
41-
name_token: HTML_LITERAL@17..23 "v-bind" [] [],
40+
name_token: VUE_IDENT@17..23 "v-bind" [] [],
4241
arg: VueDirectiveArgument {
4342
colon_token: COLON@23..24 ":" [] [],
4443
arg: VueStaticArgument {
@@ -81,22 +80,19 @@ HtmlRoot {
8180
r_angle_token: R_ANGLE@59..60 ">" [] [],
8281
},
8382
},
84-
HtmlContent {
85-
value_token: HTML_LITERAL@60..61 ">" [] [],
86-
},
8783
],
88-
eof_token: EOF@61..62 "" [Newline("\n")] [],
84+
eof_token: EOF@60..60 "" [] [],
8985
}
9086
```
9187
9288
## CST
9389
9490
```
95-
0: HTML_ROOT@0..62
91+
0: HTML_ROOT@0..60
9692
0: (empty)
9793
1: (empty)
9894
2: (empty)
99-
3: HTML_ELEMENT_LIST@0..61
95+
3: HTML_ELEMENT_LIST@0..60
10096
10197
10298
0: [email protected] "<" [] []
@@ -112,7 +108,7 @@ HtmlRoot {
112108
0: [email protected] "div" [] [Whitespace(" ")]
113109
114110
115-
0: HTML_LITERAL@17..23 "v-bind" [] []
111+
0: VUE_IDENT@17..23 "v-bind" [] []
116112
117113
0: [email protected] ":" [] []
118114
@@ -139,8 +135,6 @@ HtmlRoot {
139135
140136
0: [email protected] "template" [] []
141137
3: [email protected] ">" [] []
142-
143-
0: [email protected] ">" [] []
144-
4: [email protected] "" [Newline("\n")] []
138+
4: [email protected] "" [] []
145139
146140
```

crates/biome_html_parser/tests/html_specs/ok/vue/modifiers-all-variants.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
<div v-foo:bar.baz="5"></div>
33
<div :bar.baz="5"></div>
44
<div @bar.baz="5"></div>
5-
</template>>
5+
</template>

crates/biome_html_parser/tests/html_specs/ok/vue/modifiers-all-variants.vue.snap

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ expression: snapshot
99
<div v-foo:bar.baz="5"></div>
1010
<div :bar.baz="5"></div>
1111
<div @bar.baz="5"></div>
12-
</template>>
13-
12+
</template>
1413
```
1514
1615
@@ -40,7 +39,7 @@ HtmlRoot {
4039
},
4140
attributes: HtmlAttributeList [
4241
VueDirective {
43-
name_token: HTML_LITERAL@17..22 "v-foo" [] [],
42+
name_token: VUE_IDENT@17..22 "v-foo" [] [],
4443
arg: VueDirectiveArgument {
4544
colon_token: COLON@22..23 ":" [] [],
4645
arg: VueStaticArgument {
@@ -161,22 +160,19 @@ HtmlRoot {
161160
r_angle_token: R_ANGLE@104..105 ">" [] [],
162161
},
163162
},
164-
HtmlContent {
165-
value_token: HTML_LITERAL@105..106 ">" [] [],
166-
},
167163
],
168-
eof_token: EOF@106..107 "" [Newline("\n")] [],
164+
eof_token: EOF@105..105 "" [] [],
169165
}
170166
```
171167
172168
## CST
173169
174170
```
175-
0: HTML_ROOT@0..107
171+
0: HTML_ROOT@0..105
176172
0: (empty)
177173
1: (empty)
178174
2: (empty)
179-
3: HTML_ELEMENT_LIST@0..106
175+
3: HTML_ELEMENT_LIST@0..105
180176
181177
182178
0: [email protected] "<" [] []
@@ -192,7 +188,7 @@ HtmlRoot {
192188
0: [email protected] "div" [] [Whitespace(" ")]
193189
194190
195-
0: HTML_LITERAL@17..22 "v-foo" [] []
191+
0: VUE_IDENT@17..22 "v-foo" [] []
196192
197193
0: [email protected] ":" [] []
198194
@@ -272,8 +268,6 @@ HtmlRoot {
272268
273269
0: [email protected] "template" [] []
274270
3: [email protected] ">" [] []
275-
276-
0: [email protected] ">" [] []
277-
4: [email protected] "" [Newline("\n")] []
271+
4: [email protected] "" [] []
278272
279273
```
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
<Foo :[key]="foo" />
3+
</template>
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
---
2+
source: crates/biome_html_parser/tests/spec_test.rs
3+
expression: snapshot
4+
---
5+
## Input
6+
7+
```vue
8+
<template>
9+
<Foo :[key]="foo" />
10+
</template>
11+
12+
```
13+
14+
15+
## AST
16+
17+
```
18+
HtmlRoot {
19+
bom_token: missing (optional),
20+
frontmatter: missing (optional),
21+
directive: missing (optional),
22+
html: HtmlElementList [
23+
HtmlElement {
24+
opening_element: HtmlOpeningElement {
25+
l_angle_token: L_ANGLE@0..1 "<" [] [],
26+
name: HtmlTagName {
27+
value_token: HTML_LITERAL@1..9 "template" [] [],
28+
},
29+
attributes: HtmlAttributeList [],
30+
r_angle_token: R_ANGLE@9..10 ">" [] [],
31+
},
32+
children: HtmlElementList [
33+
HtmlSelfClosingElement {
34+
l_angle_token: L_ANGLE@10..13 "<" [Newline("\n"), Whitespace("\t")] [],
35+
name: HtmlTagName {
36+
value_token: HTML_LITERAL@13..17 "Foo" [] [Whitespace(" ")],
37+
},
38+
attributes: HtmlAttributeList [
39+
VueVBindShorthandDirective {
40+
arg: VueDirectiveArgument {
41+
colon_token: COLON@17..18 ":" [] [],
42+
arg: VueStaticArgument {
43+
name_token: HTML_LITERAL@18..23 "[key]" [] [],
44+
},
45+
},
46+
modifiers: VueModifierList [],
47+
initializer: HtmlAttributeInitializerClause {
48+
eq_token: EQ@23..24 "=" [] [],
49+
value: HtmlString {
50+
value_token: HTML_STRING_LITERAL@24..30 "\"foo\"" [] [Whitespace(" ")],
51+
},
52+
},
53+
},
54+
],
55+
slash_token: SLASH@30..31 "/" [] [],
56+
r_angle_token: R_ANGLE@31..32 ">" [] [],
57+
},
58+
],
59+
closing_element: HtmlClosingElement {
60+
l_angle_token: L_ANGLE@32..34 "<" [Newline("\n")] [],
61+
slash_token: SLASH@34..35 "/" [] [],
62+
name: HtmlTagName {
63+
value_token: HTML_LITERAL@35..43 "template" [] [],
64+
},
65+
r_angle_token: R_ANGLE@43..44 ">" [] [],
66+
},
67+
},
68+
],
69+
eof_token: EOF@44..45 "" [Newline("\n")] [],
70+
}
71+
```
72+
73+
## CST
74+
75+
```
76+
77+
0: (empty)
78+
1: (empty)
79+
2: (empty)
80+
81+
82+
83+
0: [email protected] "<" [] []
84+
85+
0: [email protected] "template" [] []
86+
87+
3: [email protected] ">" [] []
88+
89+
90+
0: [email protected] "<" [Newline("\n"), Whitespace("\t")] []
91+
92+
0: [email protected] "Foo" [] [Whitespace(" ")]
93+
94+
95+
96+
0: [email protected] ":" [] []
97+
98+
0: [email protected] "[key]" [] []
99+
100+
101+
0: [email protected] "=" [] []
102+
103+
0: [email protected] "\"foo\"" [] [Whitespace(" ")]
104+
3: [email protected] "/" [] []
105+
4: [email protected] ">" [] []
106+
107+
0: [email protected] "<" [Newline("\n")] []
108+
1: [email protected] "/" [] []
109+
110+
0: [email protected] "template" [] []
111+
3: [email protected] ">" [] []
112+
4: [email protected] "" [Newline("\n")] []
113+
114+
```

0 commit comments

Comments
 (0)