Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-vue-colon-attribute-parsing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Fixed [#9161](https://github.com/biomejs/biome/issues/9161): The Vue parser now correctly handles colon attributes like `xlink:href` and `xmlns:xlink` by parsing them as single attributes instead of splitting them into separate tokens.
10 changes: 9 additions & 1 deletion crates/biome_html_parser/src/lexer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,11 @@ impl<'src> HtmlLexer<'src> {
}
}
IdentifierContext::Vue => {
if is_attribute_name_byte_vue(byte) {
if byte == b':' && is_vue_directive_prefix_bytes(&buffer[..len]) {
break;
}

if is_attribute_name_byte_vue(byte) || byte == b':' {
if len < BUFFER_SIZE {
buffer[len] = byte;
len += 1;
Expand Down Expand Up @@ -1430,6 +1434,10 @@ fn is_astro_directive_keyword_bytes(bytes: &[u8]) -> bool {
)
}

fn is_vue_directive_prefix_bytes(bytes: &[u8]) -> bool {
bytes.starts_with(b"v-")
}

/// Identifiers can contain letters, numbers and `_`
fn is_at_continue_identifier(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || byte == b'_'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<template>
<AvatarPrimitive.Fallback
bind:ref
class="something nice"
/>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ expression: snapshot
```vue
<template>
<AvatarPrimitive.Fallback
bind:ref
class="something nice"
/>
</template>
Expand Down Expand Up @@ -48,101 +47,74 @@ HtmlRoot {
attributes: HtmlAttributeList [
HtmlAttribute {
name: HtmlAttributeName {
value_token: HTML_LITERAL@38..47 "bind" [Newline("\n"), Whitespace(" ")] [],
},
initializer: missing (optional),
},
VueVBindShorthandDirective {
arg: VueDirectiveArgument {
colon_token: COLON@47..48 ":" [] [],
arg: VueStaticArgument {
name_token: HTML_LITERAL@48..51 "ref" [] [],
},
},
modifiers: VueModifierList [],
initializer: missing (optional),
},
HtmlAttribute {
name: HtmlAttributeName {
value_token: HTML_LITERAL@51..61 "class" [Newline("\n"), Whitespace(" ")] [],
value_token: HTML_LITERAL@38..48 "class" [Newline("\n"), Whitespace(" ")] [],
},
initializer: HtmlAttributeInitializerClause {
eq_token: EQ@61..62 "=" [] [],
eq_token: EQ@48..49 "=" [] [],
value: HtmlString {
value_token: HTML_STRING_LITERAL@62..78 "\"something nice\"" [] [],
value_token: HTML_STRING_LITERAL@49..65 "\"something nice\"" [] [],
},
},
},
],
slash_token: SLASH@78..82 "/" [Newline("\n"), Whitespace(" ")] [],
r_angle_token: R_ANGLE@82..83 ">" [] [],
slash_token: SLASH@65..69 "/" [Newline("\n"), Whitespace(" ")] [],
r_angle_token: R_ANGLE@69..70 ">" [] [],
},
],
closing_element: HtmlClosingElement {
l_angle_token: L_ANGLE@83..85 "<" [Newline("\n")] [],
slash_token: SLASH@85..86 "/" [] [],
l_angle_token: L_ANGLE@70..72 "<" [Newline("\n")] [],
slash_token: SLASH@72..73 "/" [] [],
name: HtmlTagName {
value_token: HTML_LITERAL@86..94 "template" [] [],
value_token: HTML_LITERAL@73..81 "template" [] [],
},
r_angle_token: R_ANGLE@94..95 ">" [] [],
r_angle_token: R_ANGLE@81..82 ">" [] [],
},
},
],
eof_token: EOF@95..96 "" [Newline("\n")] [],
eof_token: EOF@82..83 "" [Newline("\n")] [],
}
```

## CST

```
0: HTML_ROOT@0..96
0: HTML_ROOT@0..83
0: (empty)
1: (empty)
2: (empty)
3: HTML_ELEMENT_LIST@0..95
0: HTML_ELEMENT@0..95
3: HTML_ELEMENT_LIST@0..82
0: HTML_ELEMENT@0..82
0: HTML_OPENING_ELEMENT@0..10
0: L_ANGLE@0..1 "<" [] []
1: HTML_TAG_NAME@1..9
0: HTML_LITERAL@1..9 "template" [] []
2: HTML_ATTRIBUTE_LIST@9..9
3: R_ANGLE@9..10 ">" [] []
1: HTML_ELEMENT_LIST@10..83
0: HTML_SELF_CLOSING_ELEMENT@10..83
1: HTML_ELEMENT_LIST@10..70
0: HTML_SELF_CLOSING_ELEMENT@10..70
0: L_ANGLE@10..14 "<" [Newline("\n"), Whitespace(" ")] []
1: HTML_MEMBER_NAME@14..38
0: HTML_COMPONENT_NAME@14..29
0: HTML_LITERAL@14..29 "AvatarPrimitive" [] []
1: DOT@29..30 "." [] []
2: HTML_TAG_NAME@30..38
0: HTML_LITERAL@30..38 "Fallback" [] []
2: HTML_ATTRIBUTE_LIST@38..78
0: HTML_ATTRIBUTE@38..47
0: HTML_ATTRIBUTE_NAME@38..47
0: HTML_LITERAL@38..47 "bind" [Newline("\n"), Whitespace(" ")] []
1: (empty)
1: VUE_V_BIND_SHORTHAND_DIRECTIVE@47..51
0: VUE_DIRECTIVE_ARGUMENT@47..51
0: COLON@47..48 ":" [] []
1: VUE_STATIC_ARGUMENT@48..51
0: HTML_LITERAL@48..51 "ref" [] []
1: VUE_MODIFIER_LIST@51..51
2: (empty)
2: HTML_ATTRIBUTE@51..78
0: HTML_ATTRIBUTE_NAME@51..61
0: HTML_LITERAL@51..61 "class" [Newline("\n"), Whitespace(" ")] []
1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@61..78
0: EQ@61..62 "=" [] []
1: HTML_STRING@62..78
0: HTML_STRING_LITERAL@62..78 "\"something nice\"" [] []
3: SLASH@78..82 "/" [Newline("\n"), Whitespace(" ")] []
4: R_ANGLE@82..83 ">" [] []
2: HTML_CLOSING_ELEMENT@83..95
0: L_ANGLE@83..85 "<" [Newline("\n")] []
1: SLASH@85..86 "/" [] []
2: HTML_TAG_NAME@86..94
0: HTML_LITERAL@86..94 "template" [] []
3: R_ANGLE@94..95 ">" [] []
4: EOF@95..96 "" [Newline("\n")] []
2: HTML_ATTRIBUTE_LIST@38..65
0: HTML_ATTRIBUTE@38..65
0: HTML_ATTRIBUTE_NAME@38..48
0: HTML_LITERAL@38..48 "class" [Newline("\n"), Whitespace(" ")] []
1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@48..65
0: EQ@48..49 "=" [] []
1: HTML_STRING@49..65
0: HTML_STRING_LITERAL@49..65 "\"something nice\"" [] []
3: SLASH@65..69 "/" [Newline("\n"), Whitespace(" ")] []
4: R_ANGLE@69..70 ">" [] []
2: HTML_CLOSING_ELEMENT@70..82
0: L_ANGLE@70..72 "<" [Newline("\n")] []
1: SLASH@72..73 "/" [] []
2: HTML_TAG_NAME@73..81
0: HTML_LITERAL@73..81 "template" [] []
3: R_ANGLE@81..82 ">" [] []
4: EOF@82..83 "" [Newline("\n")] []

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<svg width="200" height="150"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<circle id="myCircle" cx="0" cy="0" r="30" fill="orange" />
</defs>

<!-- xlink:href should be parsed as a single attribute -->
<use xlink:href="#myCircle" x="50" y="50" />
<use xlink:href="#myCircle" x="150" y="100" fill="steelblue" />

</svg>
Loading
Loading