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/fair-ligers-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Fixed [#8710](https://github.com/biomejs/biome/issues/8710): Biome now parses Vue dynamic slot shorthand arguments that use template literals in `[]`.
39 changes: 39 additions & 0 deletions crates/biome_html_parser/src/lexer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,42 @@ impl<'src> HtmlLexer<'src> {
}
}

/// Consume a token in the [HtmlLexContext::VueDirectiveArgument] context.
fn consume_token_vue_directive_argument(&mut self) -> HtmlSyntaxKind {
let start = self.text_position();
let mut brackets_stack = 0;
let mut quotes_seen = QuotesSeen::new();

while let Some(byte) = self.current_byte() {
quotes_seen.check_byte(byte);
let char = biome_unicode_table::lookup_byte(byte);
use biome_unicode_table::Dispatch::*;

if quotes_seen.is_empty() {
match char {
BTO => {
brackets_stack += 1;
}
BTC => {
if brackets_stack == 0 {
break;
}
brackets_stack -= 1;
}
_ => {}
}
}

self.advance_byte_or_char(byte);
}

if self.text_position() != start {
HTML_LITERAL
} else {
ERROR_TOKEN
}
}
Comment on lines +158 to +192
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

❓ Verification inconclusive

Script executed:

# Find and inspect the QuotesSeen implementation
rg -A 10 "struct QuotesSeen" crates/biome_html_parser/src/

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 0

stdout:

crates/biome_html_parser/src/lexer/mod.rs:struct QuotesSeen {
crates/biome_html_parser/src/lexer/mod.rs-    single: u16,
crates/biome_html_parser/src/lexer/mod.rs-    double: u16,
crates/biome_html_parser/src/lexer/mod.rs-    template: u16,
crates/biome_html_parser/src/lexer/mod.rs-    inside_comment: bool,
crates/biome_html_parser/src/lexer/mod.rs-    prev_byte: Option<u8>,
crates/biome_html_parser/src/lexer/mod.rs-}
crates/biome_html_parser/src/lexer/mod.rs-
crates/biome_html_parser/src/lexer/mod.rs-impl QuotesSeen {
crates/biome_html_parser/src/lexer/mod.rs-    fn new() -> Self {
crates/biome_html_parser/src/lexer/mod.rs-        Self {

Script executed:

# Check the full consume_token_vue_directive_argument function and surrounding context
cat -n crates/biome_html_parser/src/lexer/mod.rs | sed -n '150,200p'

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 0

stdout:

   150	            }
   151	            _ if (self.current_kind != T![<] && is_attribute_name_byte_vue(current)) => {
   152	                self.consume_identifier(current, IdentifierContext::Vue)
   153	            }
   154	            _ => self.consume_unexpected_character(),
   155	        }
   156	    }
   157	
   158	    /// Consume a token in the [HtmlLexContext::VueDirectiveArgument] context.
   159	    fn consume_token_vue_directive_argument(&mut self) -> HtmlSyntaxKind {
   160	        let start = self.text_position();
   161	        let mut brackets_stack = 0;
   162	        let mut quotes_seen = QuotesSeen::new();
   163	
   164	        while let Some(byte) = self.current_byte() {
   165	            quotes_seen.check_byte(byte);
   166	            let char = biome_unicode_table::lookup_byte(byte);
   167	            use biome_unicode_table::Dispatch::*;
   168	
   169	            if quotes_seen.is_empty() {
   170	                match char {
   171	                    BTO => {
   172	                        brackets_stack += 1;
   173	                    }
   174	                    BTC => {
   175	                        if brackets_stack == 0 {
   176	                            break;
   177	                        }
   178	                        brackets_stack -= 1;
   179	                    }
   180	                    _ => {}
   181	                }
   182	            }
   183	
   184	            self.advance_byte_or_char(byte);
   185	        }
   186	
   187	        if self.text_position() != start {
   188	            HTML_LITERAL
   189	        } else {
   190	            ERROR_TOKEN
   191	        }
   192	    }
   193	
   194	    /// Consume a token in the [HtmlLexContext::Regular] context.
   195	    fn consume_token(&mut self, current: u8) -> HtmlSyntaxKind {
   196	        match current {
   197	            b'\n' | b'\r' | b'\t' | b' ' => self.consume_newline_or_whitespaces(),
   198	            b'!' if self.current() == T![<] => self.consume_byte(T![!]),
   199	            b'/' if self.current() == T![<] => self.consume_byte(T![/]),
   200	            b',' if self.current() == T![<] => self.consume_byte(T![,]),

Script executed:

# Search for tests related to Vue directives
find crates/biome_html_parser -name "*.rs" -type f | xargs grep -l "vue_directive\|VueDirective" | head -10

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 0

stdout:

crates/biome_html_parser/src/lexer/mod.rs
crates/biome_html_parser/src/syntax/mod.rs
crates/biome_html_parser/src/syntax/parse_error.rs
crates/biome_html_parser/src/syntax/vue.rs
crates/biome_html_parser/src/token_source.rs

Script executed:

# Look for escape handling in the lexer
rg -i "escape\|backslash\|\\\\" crates/biome_html_parser/src/lexer/ -A 2 -B 2

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 1


Script executed:

# Get the full QuotesSeen impl block, especially check_byte method
rg -A 50 "impl QuotesSeen" crates/biome_html_parser/src/lexer/mod.rs | head -80

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 0

stdout:

impl QuotesSeen {
    fn new() -> Self {
        Self {
            single: 0,
            double: 0,
            template: 0,
            inside_comment: false,
            prev_byte: None,
        }
    }

    /// It checks the given byte. If it's a quote, it's tracked
    fn check_byte(&mut self, byte: u8) {
        // Check for comment exit first
        if self.inside_comment {
            if byte == b'\n' {
                // Exit single-line comment
                self.inside_comment = false;
            } else if self.prev_byte == Some(b'*') && byte == b'/' {
                // Exit multi-line comment
                self.inside_comment = false;
            }
            self.prev_byte = Some(byte);
            return; // Don't track quotes inside comments
        }

        // Check for comment entry - but only if we're not inside quotes
        if self.prev_byte == Some(b'/')
            && (byte == b'/' || byte == b'*')
            && self.single == 0
            && self.double == 0
            && self.template == 0
        {
            self.inside_comment = true;
            self.prev_byte = Some(byte);
            return;
        }

        // Normal quote tracking
        match byte {
            b'"' => self.track_double(),
            b'\'' => self.track_single(),
            b'`' => self.track_template(),
            _ => {}
        }

        self.prev_byte = Some(byte);
    }

    /// It adds a single quote if single quotes are zero and the others are greater than zero. It removes it otherwise
    fn track_single(&mut self) {

Script executed:

# Search for Vue directive tests
find crates/biome_html_parser/tests -type f -name "*.rs" 2>/dev/null | xargs grep -l "vue\|directive" 2>/dev/null || find crates -name "*vue*.rs" -path "*/tests/*"

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 0

stdout:

crates/biome_html_parser/tests/spec_test.rs
crates/biome_html_parser/tests/spec_tests.rs

Script executed:

# Also check if there are snapshot/fixture tests for Vue
find crates/biome_html_parser -name "*.html" -o -name "*vue*" | grep -i test

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 0

stdout:

crates/biome_html_parser/tests/html_specs/error/attributes/invalid-unquoted-value1.html
crates/biome_html_parser/tests/html_specs/error/attributes/invalid-unquoted-value2.html
crates/biome_html_parser/tests/html_specs/error/attributes/missing-initializer.html
crates/biome_html_parser/tests/html_specs/error/element/br-with-end.html
crates/biome_html_parser/tests/html_specs/error/element/child-no-tag-name.html
crates/biome_html_parser/tests/html_specs/error/element/missing-close-tag-2.html
crates/biome_html_parser/tests/html_specs/error/element/missing-close-tag.html
crates/biome_html_parser/tests/html_specs/error/element/missing-element-name.html
crates/biome_html_parser/tests/html_specs/error/element/solo-no-tag-name.html
crates/biome_html_parser/tests/html_specs/error/frontmatter.html
crates/biome_html_parser/tests/html_specs/error/frontmatter_bogus.html
crates/biome_html_parser/tests/html_specs/error/interpolation-attributes.html
crates/biome_html_parser/tests/html_specs/error/interpolation.html
crates/biome_html_parser/tests/html_specs/error/template-langs/django/issue-5450.html
crates/biome_html_parser/tests/html_specs/error/vue
crates/biome_html_parser/tests/html_specs/error/vue/invalid-v-bind-shorthand.vue
crates/biome_html_parser/tests/html_specs/error/vue/invalid-v-bind-shorthand.vue.snap
crates/biome_html_parser/tests/html_specs/error/vue/invalid-v-on-shorthand.vue
crates/biome_html_parser/tests/html_specs/error/vue/invalid-v-on-shorthand.vue.snap
crates/biome_html_parser/tests/html_specs/error/vue/invalid-v-slot-shorthand.vue
crates/biome_html_parser/tests/html_specs/error/vue/invalid-v-slot-shorthand.vue.snap
crates/biome_html_parser/tests/html_specs/error/vue/vue_unclosed_expression.vue
crates/biome_html_parser/tests/html_specs/error/vue/vue_unclosed_expression.vue.snap
crates/biome_html_parser/tests/html_specs/ok/attributes/attributes-unquoted.html
crates/biome_html_parser/tests/html_specs/ok/attributes/attributes.html
crates/biome_html_parser/tests/html_specs/ok/attributes/multiline-attribute-value.html
crates/biome_html_parser/tests/html_specs/ok/attributes/multiline-attributes.html
crates/biome_html_parser/tests/html_specs/ok/bom.html
crates/biome_html_parser/tests/html_specs/ok/cdata.html
crates/biome_html_parser/tests/html_specs/ok/comment-inline.html
crates/biome_html_parser/tests/html_specs/ok/comment.html
crates/biome_html_parser/tests/html_specs/ok/comment2.html
crates/biome_html_parser/tests/html_specs/ok/compat/non-spec-compliant-tag-name.html
crates/biome_html_parser/tests/html_specs/ok/content-blocks.html
crates/biome_html_parser/tests/html_specs/ok/doctype/basic.html
crates/biome_html_parser/tests/html_specs/ok/doctype/long-legacy1.html
crates/biome_html_parser/tests/html_specs/ok/doctype/long-legacy2.html
crates/biome_html_parser/tests/html_specs/ok/doctype/long-legacy3.html
crates/biome_html_parser/tests/html_specs/ok/doctype/minimal.html
crates/biome_html_parser/tests/html_specs/ok/element.html
crates/biome_html_parser/tests/html_specs/ok/element_list.html
crates/biome_html_parser/tests/html_specs/ok/element_list2.html
crates/biome_html_parser/tests/html_specs/ok/embedded-languages/script-basic.html
crates/biome_html_parser/tests/html_specs/ok/embedded-languages/script-empty.html
crates/biome_html_parser/tests/html_specs/ok/embedded-languages/script-in-string.html
crates/biome_html_parser/tests/html_specs/ok/embedded-languages/script-less-than.html
crates/biome_html_parser/tests/html_specs/ok/hello-world.html
crates/biome_html_parser/tests/html_specs/ok/no-end-tags/br-in-span.html
crates/biome_html_parser/tests/html_specs/ok/no-end-tags/br.html
crates/biome_html_parser/tests/html_specs/ok/no-end-tags/casing.html
crates/biome_html_parser/tests/html_specs/ok/no-end-tags/meta.html
crates/biome_html_parser/tests/html_specs/ok/no-end-tags/param.html
crates/biome_html_parser/tests/html_specs/ok/no-end-tags/wbr.html
crates/biome_html_parser/tests/html_specs/ok/ok.html
crates/biome_html_parser/tests/html_specs/ok/ok2.html
crates/biome_html_parser/tests/html_specs/ok/quote-in-child.html
crates/biome_html_parser/tests/html_specs/ok/special-chars.html
crates/biome_html_parser/tests/html_specs/ok/text_keywords.html
crates/biome_html_parser/tests/html_specs/ok/vue
crates/biome_html_parser/tests/html_specs/ok/vue/component.vue
crates/biome_html_parser/tests/html_specs/ok/vue/component.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/event-with-colon.vue
crates/biome_html_parser/tests/html_specs/ok/vue/event-with-colon.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/generic-directives/arg-modifiers-no-value.vue
crates/biome_html_parser/tests/html_specs/ok/vue/generic-directives/arg-modifiers-no-value.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/generic-directives/arg-no-modifiers-no-value.vue
crates/biome_html_parser/tests/html_specs/ok/vue/generic-directives/arg-no-modifiers-no-value.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/generic-directives/directive-only.vue
crates/biome_html_parser/tests/html_specs/ok/vue/generic-directives/directive-only.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/generic-directives/modifiers-no-arg-no-value.vue
crates/biome_html_parser/tests/html_specs/ok/vue/generic-directives/modifiers-no-arg-no-value.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/interpolation.vue
crates/biome_html_parser/tests/html_specs/ok/vue/interpolation.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/issue-8174.vue
crates/biome_html_parser/tests/html_specs/ok/vue/issue-8174.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/issue-8765.vue
crates/biome_html_parser/tests/html_specs/ok/vue/issue-8765.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/modifier.vue
crates/biome_html_parser/tests/html_specs/ok/vue/modifier.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/modifiers-all-variants.vue
crates/biome_html_parser/tests/html_specs/ok/vue/modifiers-all-variants.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/multiple_expressions.vue
crates/biome_html_parser/tests/html_specs/ok/vue/multiple_expressions.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-dynamic.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-dynamic.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-mixed.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-mixed.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-shorthand.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-shorthand.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-bind.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-bind.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-dynamic-chains.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-dynamic-chains.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-else-if.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-else-if.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-else.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-else.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-for.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-for.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-html-text.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-html-text.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-if.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-if.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-mixed-complex.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-mixed-complex.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-model-mixed.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-model-mixed.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-on-mixed.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-on-mixed.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-on-shorthand.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-on-shorthand.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/v-slot-shorthand.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-slot-shorthand.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/vue_expressions.vue
crates/biome_html_parser/tests/html_specs/ok/vue/vue_expressions.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/vue_expressions_escaped.vue
crates/biome_html_parser/tests/html_specs/ok/vue/vue_expressions_escaped.vue.snap
crates/biome_html_parser/tests/html_specs/ok/vue/dynamic-slot-arg.vue
crates/biome_html_parser/tests/html_specs/ok/vue/dynamic-slot-arg.vue.snap

Script executed:

# Check the vue_expressions_escaped.vue test
cat crates/biome_html_parser/tests/html_specs/ok/vue/vue_expressions_escaped.vue

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 0

stdout:

<template>
	\{\{ xyz \}\}
</template>

Script executed:

# Check the full QuotesSeen implementation, specifically the tracking methods
rg -A 100 "fn track_single" crates/biome_html_parser/src/lexer/mod.rs | head -60

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 0

stdout:

    fn track_single(&mut self) {
        if (self.single == 0 && (self.double > 0 || self.template > 0))
            || (self.single == 0 && self.double == 0 && self.template == 0)
        {
            self.single.add_assign(1);
        } else {
            self.single = self.single.saturating_sub(1);
        }
    }
    /// It adds a double quote if double quotes are zero and the others are greater than zero. It removes it otherwise
    fn track_double(&mut self) {
        if (self.double == 0 && (self.single > 0 || self.template > 0))
            || (self.double == 0 && self.single == 0 && self.template == 0)
        {
            self.double.add_assign(1);
        } else {
            self.double = self.double.saturating_sub(1);
        }
    }

    /// It adds a template quote if template quotes are zero and the others are greater than zero. It removes it otherwise
    fn track_template(&mut self) {
        if (self.template == 0 && (self.single > 0 || self.double > 0))
            || (self.template == 0 && self.single == 0 && self.double == 0)
        {
            self.template.add_assign(1);
        } else {
            self.template = self.template.saturating_sub(1);
        }
    }

    fn is_empty(&self) -> bool {
        self.single == 0 && self.double == 0 && self.template == 0
    }
}

#[cfg(test)]
mod quotes_seen {
    use crate::lexer::QuotesSeen;

    fn track(source: &str, quotes_seen: &mut QuotesSeen) {
        for char in source.as_bytes() {
            quotes_seen.check_byte(*char);
        }
    }

    #[test]
    fn is_not_empty() {
        let source = r#"'"`"#;
        let mut quotes_seen = QuotesSeen::new();
        track(source, &mut quotes_seen);
        assert!(!quotes_seen.is_empty());

        let source = r#"`'""'"#;
        let mut quotes_seen = QuotesSeen::new();
        track(source, &mut quotes_seen);
        assert!(!quotes_seen.is_empty());
    }
    #[test]
    fn is_empty() {

Script executed:

# Look for any escape handling elsewhere in consume_token_vue_directive_argument or related code
rg -B 5 -A 15 "consume_token_vue_directive_argument" crates/biome_html_parser/src/lexer/mod.rs | tail -40

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 0

stdout:


    /// Consume a token in the [HtmlLexContext::VueDirectiveArgument] context.
    fn consume_token_vue_directive_argument(&mut self) -> HtmlSyntaxKind {
        let start = self.text_position();
        let mut brackets_stack = 0;
        let mut quotes_seen = QuotesSeen::new();

        while let Some(byte) = self.current_byte() {
            quotes_seen.check_byte(byte);
            let char = biome_unicode_table::lookup_byte(byte);
            use biome_unicode_table::Dispatch::*;

            if quotes_seen.is_empty() {
                match char {
                    BTO => {
                        brackets_stack += 1;
                    }
                    BTC => {
--
                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::VueDirectiveArgument => {
                        self.consume_token_vue_directive_argument()
                    }
                    HtmlLexContext::AttributeValue => self.consume_token_attribute_value(current),
                    HtmlLexContext::Doctype => self.consume_token_doctype(current),
                    HtmlLexContext::EmbeddedLanguage(lang) => {
                        self.consume_token_embedded_language(current, lang, context)
                    }
                    HtmlLexContext::TextExpression(kind) => match kind {
                        TextExpressionKind::Double => self.consume_double_text_expression(current),
                        TextExpressionKind::Single => self.consume_single_text_expression(),
                    },
                    HtmlLexContext::RestrictedSingleExpression(kind) => {
                        self.consume_restricted_single_text_expression(kind)
                    }
                    HtmlLexContext::CdataSection => self.consume_inside_cdata(current),
                    HtmlLexContext::AstroFencedCodeBlock => {

Script executed:

# Look for Vue directive argument tests with special characters
find crates/biome_html_parser/tests/html_specs -name "*.vue" -type f -exec grep -l "\[" {} \; | head -5

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 0

stdout:

crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-dynamic.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-mixed.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-dynamic-chains.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-else-if.vue
crates/biome_html_parser/tests/html_specs/ok/vue/v-html-text.vue

stderr:

find: 'grep' terminated by signal 13
find: 'grep' terminated by signal 13
find: 'grep' terminated by signal 13
find: 'grep' terminated by signal 13
find: 'grep' terminated by signal 13

Script executed:

# Check a Vue test file that likely has dynamic arguments
cat crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-dynamic.vue

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 0

stdout:

<template>
	<Foo :[key]="foo" />
</template>

Script executed:

# Search for any backslash handling in Vue context
rg "backslash|escape.*quote|quote.*escape" crates/biome_html_parser/src/ -i

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 0

stdout:

crates/biome_html_parser/src/lexer/mod.rs:    /// Consumes a quoted string literal token, handling escaped characters and unicode sequences.
crates/biome_html_parser/src/lexer/mod.rs:                        // Handle escaped `'` but only if this is a end quote string.
crates/biome_html_parser/src/lexer/mod.rs:                        // Handle escaped `'` but only if this is a end quote string.

Script executed:

# Check if there are any existing issues or TODOs about escape handling
rg "TODO|FIXME|BUG" crates/biome_html_parser/src/lexer/mod.rs -A 2

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 0

stdout:

    AS_KW, ATTACH_KW, AWAIT_KW, CATCH_KW, COMMENT, CONST_KW, DEBUG_KW, DOCTYPE_KW, EACH_KW,
    ELSE_KW, EOF, ERROR_TOKEN, HTML_KW, HTML_LITERAL, HTML_STRING_LITERAL, IDENT, IF_KW, KEY_KW,
    NEWLINE, RENDER_KW, SNIPPET_KW, THEN_KW, TOMBSTONE, UNICODE_BOM, WHITESPACE,
--
            b"debug" => DEBUG_KW,
            b"attach" => ATTACH_KW,
            b"const" => CONST_KW,

Script executed:

# Check for escaped character handling mentioned in the comments
rg -B 5 -A 10 "Handle escaped" crates/biome_html_parser/src/lexer/mod.rs

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 0

stdout:

                    self.advance(1);

                    match self.current_byte() {
                        Some(b'\n' | b'\r') => self.advance(1),

                        // Handle escaped `'` but only if this is a end quote string.
                        Some(b'\'') if quote == b'\'' => {
                            self.advance(1);
                        }

                        // Handle escaped `'` but only if this is a end quote string.
                        Some(b'"') if quote == b'"' => {
                            self.advance(1);
                        }

                        Some(b'u') => match (self.consume_unicode_escape(), state) {
                            (Ok(_), _) => {}
                            (Err(err), LexStringState::InString) => {
                                self.diagnostics.push(err);
                                state = LexStringState::InvalidEscapeSequence;
                            }

Script executed:

# Look for the consume_double_text_expression or similar to see how escapes are handled elsewhere
rg -A 30 "fn consume_double_text_expression" crates/biome_html_parser/src/lexer/mod.rs | head -50

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 0

stdout:

    fn consume_double_text_expression(&mut self, current: u8) -> HtmlSyntaxKind {
        match current {
            b'}' if self.at_closing_double_text_expression() => {
                self.consume_r_double_text_expression()
            }
            b'<' => self.consume_byte(T![<]),
            _ => {
                while let Some(current) = self.current_byte() {
                    match current {
                        b'}' if self.at_closing_double_text_expression() => break,
                        _ => {
                            self.advance(1);
                        }
                    }
                }
                HTML_LITERAL
            }
        }
    }

    /// Consumes tokens within a single text expression ('{...}') while tracking nested
    /// brackets until the matching closing bracket is found.
    fn consume_single_text_expression(&mut self) -> HtmlSyntaxKind {
        let mut brackets_stack = 0;
        while let Some(current) = self.current_byte() {
            match current {
                b'}' => {
                    if brackets_stack == 0 {
                        break;
                    } else {
                        brackets_stack -= 1;

Script executed:

# Check Vue spec or documentation on escaped quotes in directive arguments
find crates/biome_html_parser -name "*.rs" -type f -exec grep -l "directive.*argument\|VueDirectiveArgument" {} \; | xargs grep -B 5 -A 5 "escape" 2>/dev/null | head -40

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 0

stdout:

crates/biome_html_parser/src/lexer/mod.rs-                {
crates/biome_html_parser/src/lexer/mod.rs-                    self.consume_l_angle()
crates/biome_html_parser/src/lexer/mod.rs-                } else {
crates/biome_html_parser/src/lexer/mod.rs-                    self.push_diagnostic(
crates/biome_html_parser/src/lexer/mod.rs-                        ParseDiagnostic::new(
crates/biome_html_parser/src/lexer/mod.rs:                            "Unescaped `<` bracket character. Expected a tag or escaped character.",
crates/biome_html_parser/src/lexer/mod.rs-                            self.text_position()..self.text_position() + TextSize::from(1),
crates/biome_html_parser/src/lexer/mod.rs-                        )
crates/biome_html_parser/src/lexer/mod.rs:                        .with_hint("Replace this character with `&lt;` to escape it."),
crates/biome_html_parser/src/lexer/mod.rs-                    );
crates/biome_html_parser/src/lexer/mod.rs-                    self.consume_byte(HTML_LITERAL)
crates/biome_html_parser/src/lexer/mod.rs-                }
crates/biome_html_parser/src/lexer/mod.rs-            }
crates/biome_html_parser/src/lexer/mod.rs-            _ if is_at_start_identifier(current) => self
--
crates/biome_html_parser/src/lexer/mod.rs-        }
crates/biome_html_parser/src/lexer/mod.rs-
crates/biome_html_parser/src/lexer/mod.rs-        HTML_LITERAL
crates/biome_html_parser/src/lexer/mod.rs-    }
crates/biome_html_parser/src/lexer/mod.rs-
crates/biome_html_parser/src/lexer/mod.rs:    /// Consumes a quoted string literal token, handling escaped characters and unicode sequences.
crates/biome_html_parser/src/lexer/mod.rs:    /// Returns ERROR_TOKEN if the string is not properly terminated or contains invalid escapes.
crates/biome_html_parser/src/lexer/mod.rs-    fn consume_string_literal(&mut self, quote: u8) -> HtmlSyntaxKind {
crates/biome_html_parser/src/lexer/mod.rs-        self.assert_current_char_boundary();
crates/biome_html_parser/src/lexer/mod.rs-        let start = self.text_position();
crates/biome_html_parser/src/lexer/mod.rs-
crates/biome_html_parser/src/lexer/mod.rs-        self.advance(1); // Skip over the quote
--
crates/biome_html_parser/src/lexer/mod.rs-                    self.advance(1);
crates/biome_html_parser/src/lexer/mod.rs-
crates/biome_html_parser/src/lexer/mod.rs-                    match self.current_byte() {
crates/biome_html_parser/src/lexer/mod.rs-                        Some(b'\n' | b'\r') => self.advance(1),
crates/biome_html_parser/src/lexer/mod.rs-
crates/biome_html_parser/src/lexer/mod.rs:                        // Handle escaped `'` but only if this is a end quote string.
crates/biome_html_parser/src/lexer/mod.rs-                        Some(b'\'') if quote == b'\'' => {
crates/biome_html_parser/src/lexer/mod.rs-                            self.advance(1);
crates/biome_html_parser/src/lexer/mod.rs-                        }
crates/biome_html_parser/src/lexer/mod.rs-
crates/biome_html_parser/src/lexer/mod.rs:                        // Handle escaped `'` but only if this is a end quote string.
crates/biome_html_parser/src/lexer/mod.rs-                        Some(b'"') if quote == b'"' => {

Script executed:

# Check if there are any tests for Vue directive arguments with escaped characters
find crates/biome_html_parser/tests -name "*.vue" -type f -exec grep -H "\\\[" {} \;

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 0

stderr:

grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression
grep: Invalid regular expression

Script executed:

# Look at one of the dynamic argument tests more carefully
cat crates/biome_html_parser/tests/html_specs/ok/vue/v-bind-dynamic.vue.snap

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 0

stdout:

---
source: crates/biome_html_parser/tests/spec_test.rs
expression: snapshot
---
## Input

```vue
<template>
	<Foo :[key]="foo" />
</template>

```


## AST

```
HtmlRoot {
    bom_token: missing (optional),
    frontmatter: missing (optional),
    directive: missing (optional),
    html: HtmlElementList [
        HtmlElement {
            opening_element: HtmlOpeningElement {
                l_angle_token: L_ANGLE@0..1 "<" [] [],
                name: HtmlTagName {
                    value_token: HTML_LITERAL@1..9 "template" [] [],
                },
                attributes: HtmlAttributeList [],
                r_angle_token: R_ANGLE@9..10 ">" [] [],
            },
            children: HtmlElementList [
                HtmlSelfClosingElement {
                    l_angle_token: L_ANGLE@10..13 "<" [Newline("\n"), Whitespace("\t")] [],
                    name: HtmlTagName {
                        value_token: HTML_LITERAL@13..17 "Foo" [] [Whitespace(" ")],
                    },
                    attributes: HtmlAttributeList [
                        VueVBindShorthandDirective {
                            arg: VueDirectiveArgument {
                                colon_token: COLON@17..18 ":" [] [],
                                arg: VueDynamicArgument {
                                    l_brack_token: L_BRACKET@18..19 "[" [] [],
                                    name_token: HTML_LITERAL@19..22 "key" [] [],
                                    r_brack_token: R_BRACKET@22..23 "]" [] [],
                                },
                            },
                            modifiers: VueModifierList [],
                            initializer: HtmlAttributeInitializerClause {
                                eq_token: EQ@23..24 "=" [] [],
                                value: HtmlString {
                                    value_token: HTML_STRING_LITERAL@24..30 "\"foo\"" [] [Whitespace(" ")],
                                },
                            },
                        },
                    ],
                    slash_token: SLASH@30..31 "/" [] [],
                    r_angle_token: R_ANGLE@31..32 ">" [] [],
                },
            ],
            closing_element: HtmlClosingElement {
                l_angle_token: L_ANGLE@32..34 "<" [Newline("\n")] [],
                slash_token: SLASH@34..35 "/" [] [],
                name: HtmlTagName {
                    value_token: HTML_LITERAL@35..43 "template" [] [],
                },
                r_angle_token: R_ANGLE@43..44 ">" [] [],
            },
        },
    ],
    eof_token: EOF@44..45 "" [Newline("\n")] [],
}
```

## CST

```
0: HTML_ROOT@0..45
  0: (empty)
  1: (empty)
  2: (empty)
  3: HTML_ELEMENT_LIST@0..44
    0: HTML_ELEMENT@0..44
      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..32
        0: HTML_SELF_CLOSING_ELEMENT@10..32
          0: L_ANGLE@10..13 "<" [Newline("\n"), Whitespace("\t")] []
          1: HTML_TAG_NAME@13..17
            0: HTML_LITERAL@13..17 "Foo" [] [Whitespace(" ")]
          2: HTML_ATTRIBUTE_LIST@17..30
            0: VUE_V_BIND_SHORTHAND_DIRECTIVE@17..30
              0: VUE_DIRECTIVE_ARGUMENT@17..23
                0: COLON@17..18 ":" [] []
                1: VUE_DYNAMIC_ARGUMENT@18..23
                  0: L_BRACKET@18..19 "[" [] []
                  1: HTML_LITERAL@19..22 "key" [] []
                  2: R_BRACKET@22..23 "]" [] []
              1: VUE_MODIFIER_LIST@23..23
              2: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@23..30
                0: EQ@23..24 "=" [] []
                1: HTML_STRING@24..30
                  0: HTML_STRING_LITERAL@24..30 "\"foo\"" [] [Whitespace(" ")]
          3: SLASH@30..31 "/" [] []
          4: R_ANGLE@31..32 ">" [] []
      2: HTML_CLOSING_ELEMENT@32..44
        0: L_ANGLE@32..34 "<" [Newline("\n")] []
        1: SLASH@34..35 "/" [] []
        2: HTML_TAG_NAME@35..43
          0: HTML_LITERAL@35..43 "template" [] []
        3: R_ANGLE@43..44 ">" [] []
  4: EOF@44..45 "" [Newline("\n")] []

```

Script executed:

# Check the full QuotesSeen test to see if escapes are tested
rg -A 50 "#\[cfg\(test\)\]" crates/biome_html_parser/src/lexer/mod.rs | grep -A 50 "mod quotes_seen"

Repository: biomejs/biome


Repository: biomejs/biome
Exit code: 0

stdout:

mod quotes_seen {
    use crate::lexer::QuotesSeen;

    fn track(source: &str, quotes_seen: &mut QuotesSeen) {
        for char in source.as_bytes() {
            quotes_seen.check_byte(*char);
        }
    }

    #[test]
    fn is_not_empty() {
        let source = r#"'"`"#;
        let mut quotes_seen = QuotesSeen::new();
        track(source, &mut quotes_seen);
        assert!(!quotes_seen.is_empty());

        let source = r#"`'""'"#;
        let mut quotes_seen = QuotesSeen::new();
        track(source, &mut quotes_seen);
        assert!(!quotes_seen.is_empty());
    }
    #[test]
    fn is_empty() {
        let source = r#" '"``"' "#;
        let mut quotes_seen = QuotesSeen::new();
        track(source, &mut quotes_seen);
        assert!(quotes_seen.is_empty());

        let source = r#"// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh."#;
        let mut quotes_seen = QuotesSeen::new();
        track(source, &mut quotes_seen);
        assert!(quotes_seen.is_empty());
    }

    #[test]
    fn empty_inside_comments() {
        let source = r#"// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh."#;
        let mut quotes_seen = QuotesSeen::new();
        track(source, &mut quotes_seen);
        assert!(quotes_seen.is_empty());
    }

    #[test]
    fn empty_with_comments_outside_comments_1() {
        let source = r#"// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh.
const f = "something" "#;
        let mut quotes_seen = QuotesSeen::new();
        track(source, &mut quotes_seen);
        assert!(quotes_seen.is_empty());
    }

Handle escaped quotes in directive arguments.

QuotesSeen toggles quote state on every quote byte without escape awareness, so \" or \' inside a directive argument would prematurely end quote tracking and allow ] inside a string literal to close the argument early. Add escape-aware quote tracking to check_byte() or add a regression test documenting the intended behaviour.

🤖 Prompt for AI Agents
In `@crates/biome_html_parser/src/lexer/mod.rs` around lines 158 - 192, The quote
state in consume_token_vue_directive_argument is toggled blindly by
QuotesSeen::check_byte, so escaped quotes like \" or \' break tracking and let a
] inside a quoted string close the argument; update QuotesSeen::check_byte (or
add an escape-aware layer in consume_token_vue_directive_argument using
current_byte/peek_previous logic) to ignore quote bytes that are escaped (handle
backslash escaping, including escaped backslashes) while preserving normal
toggling otherwise, and add a regression test for
HtmlLexContext::VueDirectiveArgument that includes escaped quotes inside
brackets to ensure the parser does not prematurely close on ].


/// Consume a token in the [HtmlLexContext::Regular] context.
fn consume_token(&mut self, current: u8) -> HtmlSyntaxKind {
match current {
Expand Down Expand Up @@ -1078,6 +1114,9 @@ impl<'src> Lexer<'src> for HtmlLexer<'src> {
HtmlLexContext::Regular => self.consume_token(current),
HtmlLexContext::InsideTag => self.consume_token_inside_tag(current),
HtmlLexContext::InsideTagVue => self.consume_token_inside_tag_vue(current),
HtmlLexContext::VueDirectiveArgument => {
self.consume_token_vue_directive_argument()
}
HtmlLexContext::AttributeValue => self.consume_token_attribute_value(current),
HtmlLexContext::Doctype => self.consume_token_doctype(current),
HtmlLexContext::EmbeddedLanguage(lang) => {
Expand Down
2 changes: 1 addition & 1 deletion crates/biome_html_parser/src/syntax/vue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ fn parse_vue_dynamic_argument(p: &mut HtmlParser) -> ParsedSyntax {

let m = p.start();

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

Expand Down
2 changes: 2 additions & 0 deletions crates/biome_html_parser/src/token_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ pub(crate) enum HtmlLexContext {
InsideTag,
/// Like [InsideTag], but with Vue-specific tokens enabled.
InsideTagVue,
/// Lexes Vue directive arguments inside `[]`.
VueDirectiveArgument,
/// When the parser encounters a `=` token (the beginning of the attribute initializer clause), it switches to this context.
///
/// This is because attribute values can start and end with a `"` or `'` character, or be unquoted, and the lexer needs to know to start lexing a string literal.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<template
v-for="episode in episodes"
#[`${episode.id}-cell`]="{ row }"
:key="episode.id"
>
...
</template>

<template
v-for="episode in episodes"
v-slot:[`${episode.id}-cell`]="{ row }"
:key="episode.id"
>
...
</template>

<template
v-for="episode in episodes"
#[episode.id]="{ row }"
:key="episode.id"
>
...
</template>
Loading