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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<AlertDialogPrimitive bind:value />
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
source: crates/biome_formatter_test/src/snapshot_builder.rs
info: svelte/bind_component.svelte
---
# Input

```svelte
<AlertDialogPrimitive bind:value />

```


=============================

# Outputs

## Output 1

-----
Indent style: Tab
Indent width: 2
Line ending: LF
Line width: 80
Attribute Position: Auto
Bracket same line: false
Whitespace sensitivity: css
Indent script and style: false
Self close void elements: never
Trailing newline: true
-----

```svelte
<AlertDialogPrimitive bind:value />

```
11 changes: 7 additions & 4 deletions crates/biome_html_parser/src/lexer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ impl<'src> HtmlLexer<'src> {
}
}

/// Consume a token in the [HtmlLexContext::InsideTagVue] context.
/// Consume a token in the [HtmlLexContext::InsideTagWithDirectives] context.
/// This context is used for Vue files with Vue-specific directives.
fn consume_token_inside_tag_vue(&mut self, current: u8) -> HtmlSyntaxKind {
fn consume_token_inside_tag_directives(&mut self, current: u8) -> HtmlSyntaxKind {
let dispatched = lookup_byte(current);

match dispatched {
Expand All @@ -135,9 +135,10 @@ impl<'src> HtmlLexer<'src> {
self.consume_byte(T!['}'])
}
}
// Vue and Svelte directives
COL => self.consume_byte(T![:]),

// these are used in Vue directives
COL => self.consume_byte(T![:]),
AT_ => self.consume_byte(T![@]),
PRD => self.consume_byte(T![.]),
BTO => self.consume_byte(T!['[']),
Expand Down Expand Up @@ -1180,7 +1181,9 @@ impl<'src> Lexer<'src> for HtmlLexer<'src> {
Some(current) => match context {
HtmlLexContext::Regular => self.consume_token(current),
HtmlLexContext::InsideTag => self.consume_token_inside_tag(current),
HtmlLexContext::InsideTagVue => self.consume_token_inside_tag_vue(current),
HtmlLexContext::InsideTagWithDirectives => {
self.consume_token_inside_tag_directives(current)
}
HtmlLexContext::VueDirectiveArgument => {
self.consume_token_vue_directive_argument()
}
Expand Down
38 changes: 18 additions & 20 deletions crates/biome_html_parser/src/syntax/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod svelte;
mod vue;

use crate::parser::HtmlParser;
use crate::syntax::HtmlSyntaxFeatures::{Astro, DoubleTextExpressions, SingleTextExpressions, Vue};
use crate::syntax::HtmlSyntaxFeatures::{Astro, DoubleTextExpressions, Svelte, Vue};
use crate::syntax::astro::{parse_astro_fence, parse_astro_spread_or_expression};
use crate::syntax::parse_error::*;
use crate::syntax::svelte::{
Expand Down Expand Up @@ -34,7 +34,7 @@ pub(crate) enum HtmlSyntaxFeatures {
/// Exclusive to those documents that support text expressions with {{ }}
DoubleTextExpressions,
/// Exclusive to those documents that support text expressions with { }
SingleTextExpressions,
Svelte,
/// Exclusive to those documents that support Vue
Vue,
}
Expand All @@ -44,14 +44,12 @@ impl SyntaxFeature for HtmlSyntaxFeatures {

fn is_supported(&self, p: &HtmlParser) -> bool {
match self {
Self::Astro => p.options().frontmatter,
Self::DoubleTextExpressions => {
Astro => p.options().frontmatter,
DoubleTextExpressions => {
p.options().text_expression == Some(TextExpressionKind::Double)
}
Self::SingleTextExpressions => {
p.options().text_expression == Some(TextExpressionKind::Single)
}
Self::Vue => p.options().vue,
Svelte => p.options().text_expression == Some(TextExpressionKind::Single),
Vue => p.options().vue,
}
}
}
Expand Down Expand Up @@ -136,7 +134,7 @@ fn inside_tag_context(p: &HtmlParser) -> HtmlLexContext {
// Only Vue files use InsideTagVue context, which has Vue-specific directive parsing (v-bind, :, @, etc.)
// Svelte and Astro use regular InsideTag context as they have different directive syntax
if HtmlSyntaxFeatures::Vue.is_supported(p) {
HtmlLexContext::InsideTagVue
HtmlLexContext::InsideTagWithDirectives
} else {
HtmlLexContext::InsideTag
}
Expand All @@ -152,15 +150,15 @@ fn is_possible_component(p: &HtmlParser, tag_name: &str) -> bool {

/// Returns the lexer context to use when parsing component names and member expressions.
/// This allows `.` to be lexed as a token for member expressions like Component.Member
/// We reuse InsideTagVue context because it supports `.` lexing, but this is ONLY used
/// We reuse [HtmlLexContext::InsideTagWithDirectives] context because it supports `.` lexing, but this is ONLY used
/// for parsing component names, not for parsing attributes.
#[inline(always)]
fn component_name_context(p: &HtmlParser) -> HtmlLexContext {
if Vue.is_supported(p) || Astro.is_supported(p) || SingleTextExpressions.is_supported(p) {
// Use InsideTagVue for all component-supporting files when parsing component names
if Vue.is_supported(p) || Svelte.is_supported(p) {
// Use HtmlLexContext::InsideTagWithDirectives for all component-supporting files when parsing component names
// This allows `.` to be lexed properly for member expressions
// Note: This is safe because we only use this context for tag names, not attributes
HtmlLexContext::InsideTagVue
HtmlLexContext::InsideTagWithDirectives
} else {
HtmlLexContext::InsideTag
}
Expand Down Expand Up @@ -462,15 +460,15 @@ fn parse_attribute(p: &mut HtmlParser) -> ParsedSyntax {
parse_vue_v_slot_shorthand_directive,
|p, m| disabled_vue(p, m.range(p)),
),
T!['{'] if SingleTextExpressions.is_supported(p) => parse_svelte_spread_or_expression(p),
T!['{'] if Svelte.is_supported(p) => parse_svelte_spread_or_expression(p),
T!['{'] if Astro.is_supported(p) => parse_astro_spread_or_expression(p),
// Keep previous behaviour so that invalid documents are still parsed.
T!['{'] => SingleTextExpressions.parse_exclusive_syntax(
T!['{'] => Svelte.parse_exclusive_syntax(
p,
|p| parse_svelte_spread_or_expression(p),
|p: &HtmlParser<'_>, m: &CompletedMarker| disabled_svelte(p, m.range(p)),
),
T!["{@"] => SingleTextExpressions.parse_exclusive_syntax(
T!["{@"] => Svelte.parse_exclusive_syntax(
p,
|p| parse_attach_attribute(p),
|p: &HtmlParser<'_>, m: &CompletedMarker| disabled_svelte(p, m.range(p)),
Expand All @@ -479,7 +477,7 @@ fn parse_attribute(p: &mut HtmlParser) -> ParsedSyntax {
Vue.parse_exclusive_syntax(p, parse_vue_directive, |p, m| disabled_vue(p, m.range(p)))
}
_ if is_at_svelte_directive_start(p) => {
SingleTextExpressions.parse_exclusive_syntax(p, parse_svelte_directive, |p, m| {
Svelte.parse_exclusive_syntax(p, parse_svelte_directive, |p, m| {
disabled_svelte(p, m.range(p))
})
}
Expand All @@ -503,7 +501,7 @@ fn is_at_attribute_start(p: &mut HtmlParser) -> bool {
T![:],
T![@],
T![#],
]) || (SingleTextExpressions.is_supported(p) && p.at(T!["{@"]))
]) || (Svelte.is_supported(p) && p.at(T!["{@"]))
}

fn parse_literal(p: &mut HtmlParser, kind: HtmlSyntaxKind) -> ParsedSyntax {
Expand Down Expand Up @@ -572,7 +570,7 @@ fn parse_attribute_initializer(p: &mut HtmlParser) -> ParsedSyntax {
let m = p.start();
p.bump_with_context(T![=], HtmlLexContext::AttributeValue);
if p.at(T!['{']) {
HtmlSyntaxFeatures::SingleTextExpressions
HtmlSyntaxFeatures::Svelte
.parse_exclusive_syntax(
p,
|p| parse_single_text_expression(p, inside_tag_context(p)),
Expand Down Expand Up @@ -670,7 +668,7 @@ pub(crate) fn parse_single_text_expression(
p: &mut HtmlParser,
context: HtmlLexContext,
) -> ParsedSyntax {
if !SingleTextExpressions.is_supported(p) {
if !Svelte.is_supported(p) {
return Absent;
}

Expand Down
42 changes: 30 additions & 12 deletions crates/biome_html_parser/src/syntax/svelte.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::parser::HtmlParser;
use crate::syntax::HtmlSyntaxFeatures::SingleTextExpressions;
use crate::syntax::HtmlSyntaxFeatures::Svelte;
use crate::syntax::parse_error::{
expected_child_or_block, expected_expression, expected_name, expected_svelte_closing_block,
expected_svelte_property, expected_text_expression, expected_valid_directive,
Expand Down Expand Up @@ -358,7 +358,7 @@ fn parse_each_opening_block(p: &mut HtmlParser, parent_marker: Marker) -> (Parse

/// Parses a spread attribute or a single text expression.
pub(crate) fn parse_svelte_spread_or_expression(p: &mut HtmlParser) -> ParsedSyntax {
if !SingleTextExpressions.is_supported(p) {
if !Svelte.is_supported(p) {
return Absent;
}

Expand Down Expand Up @@ -1269,6 +1269,20 @@ pub(crate) fn is_at_svelte_keyword(p: &HtmlParser) -> bool {
)
}

fn is_at_svelte_directive_keyword(token: HtmlSyntaxKind) -> bool {
matches!(
token,
T![bind]
| T![transition]
| T![in]
| T![out]
| T![class]
| T![style]
| T![use]
| T![animate]
)
}

fn is_at_else_opening_block(p: &mut HtmlParser) -> bool {
p.at(T!["{:"]) && p.nth_at(1, T![else])
}
Expand All @@ -1277,16 +1291,20 @@ fn is_at_then_or_catch_block(p: &mut HtmlParser) -> bool {
p.at(T!["{:"]) && (p.nth_at(1, T![then]) || p.nth_at(1, T![catch]))
}

pub(crate) fn is_at_svelte_directive_start(p: &HtmlParser) -> bool {
let text = p.cur_text();
text.starts_with("bind:")
|| text.starts_with("transition:")
|| text.starts_with("in:")
|| text.starts_with("out:")
|| text.starts_with("animate:")
|| text.starts_with("use:")
|| text.starts_with("style:")
|| text.starts_with("class:")
pub(crate) fn is_at_svelte_directive_start(p: &mut HtmlParser) -> bool {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ugly, but it was the only way to catch bind: and friends without possible false positives

if Svelte.is_unsupported(p) {
return false;
}
let checkpoint = p.checkpoint();
p.re_lex(HtmlReLexContext::Svelte);
let first_token = p.cur();

p.bump_any_with_context(HtmlLexContext::Svelte);
let second_token = p.cur();

p.rewind(checkpoint);

second_token == T![:] && is_at_svelte_directive_keyword(first_token)
}

// #endregion
20 changes: 10 additions & 10 deletions crates/biome_html_parser/src/syntax/vue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub(crate) fn parse_vue_directive(p: &mut HtmlParser) -> ParsedSyntax {

let pos = p.source().position();
// FIXME: Ideally, the lexer would just lex IDENT directly
p.bump_remap_with_context(IDENT, HtmlLexContext::InsideTagVue);
p.bump_remap_with_context(IDENT, HtmlLexContext::InsideTagWithDirectives);
if p.at(T![:]) {
// is there any trivia after the directive name and before the colon?
if let Some(last_trivia) = p.source().trivia_list.last()
Expand Down Expand Up @@ -71,7 +71,7 @@ pub(crate) fn parse_vue_v_on_shorthand_directive(p: &mut HtmlParser) -> ParsedSy
let m = p.start();

let pos = p.source().position();
p.bump_with_context(T![@], HtmlLexContext::InsideTagVue);
p.bump_with_context(T![@], HtmlLexContext::InsideTagWithDirectives);
// is there any trivia after the @ and before argument?
if let Some(last_trivia) = p.source().trivia_list.last()
&& pos < last_trivia.text_range().start()
Expand Down Expand Up @@ -100,7 +100,7 @@ pub(crate) fn parse_vue_v_slot_shorthand_directive(p: &mut HtmlParser) -> Parsed
let m = p.start();

let pos = p.source().position();
p.bump_with_context(T![#], HtmlLexContext::InsideTagVue);
p.bump_with_context(T![#], HtmlLexContext::InsideTagWithDirectives);
// is there any trivia after the hash and before argument?
if let Some(last_trivia) = p.source().trivia_list.last()
&& pos < last_trivia.text_range().start()
Expand Down Expand Up @@ -129,7 +129,7 @@ fn parse_vue_directive_argument(p: &mut HtmlParser) -> ParsedSyntax {
let m = p.start();

let pos = p.source().position();
p.bump_with_context(T![:], HtmlLexContext::InsideTagVue);
p.bump_with_context(T![:], HtmlLexContext::InsideTagWithDirectives);
// is there any trivia after the colon and before argument?
if let Some(last_trivia) = p.source().trivia_list.last()
&& pos < last_trivia.text_range().start()
Expand All @@ -149,7 +149,7 @@ fn parse_vue_directive_argument(p: &mut HtmlParser) -> ParsedSyntax {
fn parse_vue_static_argument(p: &mut HtmlParser) -> ParsedSyntax {
let m = p.start();

p.expect_with_context(HTML_LITERAL, HtmlLexContext::InsideTagVue);
p.expect_with_context(HTML_LITERAL, HtmlLexContext::InsideTagWithDirectives);

Present(m.complete(p, VUE_STATIC_ARGUMENT))
}
Expand All @@ -162,8 +162,8 @@ fn parse_vue_dynamic_argument(p: &mut HtmlParser) -> ParsedSyntax {
let m = p.start();

p.bump_with_context(T!['['], HtmlLexContext::VueDirectiveArgument);
p.expect_with_context(HTML_LITERAL, HtmlLexContext::InsideTagVue);
p.expect_with_context(T![']'], HtmlLexContext::InsideTagVue);
p.expect_with_context(HTML_LITERAL, HtmlLexContext::InsideTagWithDirectives);
p.expect_with_context(T![']'], HtmlLexContext::InsideTagWithDirectives);

Present(m.complete(p, VUE_DYNAMIC_ARGUMENT))
}
Expand Down Expand Up @@ -206,12 +206,12 @@ fn parse_vue_modifier(p: &mut HtmlParser) -> ParsedSyntax {

let m = p.start();

p.bump_with_context(T![.], HtmlLexContext::InsideTagVue);
p.bump_with_context(T![.], HtmlLexContext::InsideTagWithDirectives);
if p.at(T![:]) {
// `:` is actually a valid modifier, for example `@keydown.:`
p.bump_remap_with_context(HTML_LITERAL, HtmlLexContext::InsideTagVue);
p.bump_remap_with_context(HTML_LITERAL, HtmlLexContext::InsideTagWithDirectives);
} else {
p.expect_with_context(HTML_LITERAL, HtmlLexContext::InsideTagVue);
p.expect_with_context(HTML_LITERAL, HtmlLexContext::InsideTagWithDirectives);
}

Present(m.complete(p, VUE_MODIFIER))
Expand Down
4 changes: 2 additions & 2 deletions crates/biome_html_parser/src/token_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ pub(crate) enum HtmlLexContext {
/// When the lexer is inside a tag, special characters are lexed as tag tokens.
InsideTag,
/// Like [InsideTag], but with Vue-specific tokens enabled.
/// This enables parsing of Vue directives (v-bind, :, @, #, etc.)
InsideTagVue,
/// This enables parsing of Component directives (v-bind, :, @, #, etc.)
InsideTagWithDirectives,
/// Lexes Vue directive arguments inside `[]`.
VueDirectiveArgument,
/// When the parser encounters a `=` token (the beginning of the attribute initializer clause), it switches to this context.
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
<input bind:value />
<AlertDialogPrimitive bind:value />
Loading