diff --git a/.changeset/chatty-jeans-shine.md b/.changeset/chatty-jeans-shine.md new file mode 100644 index 000000000000..6f134cb391eb --- /dev/null +++ b/.changeset/chatty-jeans-shine.md @@ -0,0 +1,18 @@ +--- +"@biomejs/biome": patch +--- + +Added parsing support for Svelte's new [comments-in-tags](https://github.com/sveltejs/svelte/pull/17671) feature. + +The HTML parser will now accept JS style comments in tags in Svelte files. +```svelte + + +
text
+``` diff --git a/crates/biome_html_formatter/src/comments.rs b/crates/biome_html_formatter/src/comments.rs index d8829edeeec6..7adba6ab01f7 100644 --- a/crates/biome_html_formatter/src/comments.rs +++ b/crates/biome_html_formatter/src/comments.rs @@ -54,10 +54,16 @@ impl CommentStyle for HtmlCommentStyle { .any(|(key, ..)| key == category!("format")) } - fn get_comment_kind(_comment: &SyntaxTriviaPieceComments) -> CommentKind { - // HTML comments are block comments (), not line comments - // They don't extend to the end of the line like // comments do - CommentKind::Block + fn get_comment_kind(comment: &SyntaxTriviaPieceComments) -> CommentKind { + // Svelte/Vue files can have JS-style `//` line comments inside tags. + // These must be treated as line comments so the formatter uses `line_suffix`, + // which forces a newline after them — preventing `>` from being swallowed. + // HTML `` comments and `/* */` block comments are both block-style. + if comment.text().starts_with("//") { + CommentKind::Line + } else { + CommentKind::Block + } } /// This allows us to override which comments are associated with which nodes. @@ -112,6 +118,36 @@ impl CommentStyle for HtmlCommentStyle { } } + // Attach comments between attributes to the following attribute as leading comments. + // This is required for suppression comments (e.g. `// biome-ignore format: reason`) + // to work on attributes: + // + // ```svelte + //
+ // ``` + // + // Without this rule, the comment would be a trailing comment of the preceding attribute + // (or a dangling comment of the opening element if it's the first attribute), and + // `is_suppressed(class_attr)` would return false. + // + // NOTE: Do NOT use `comment.kind().is_line()` here to detect `//` comments — the + // comment kind is determined after `place_comment()` runs in the pipeline, so + // `kind()` always returns `Block` at this point. Instead, inspect the text directly. + if matches!( + comment.enclosing_node().kind(), + HtmlSyntaxKind::HTML_OPENING_ELEMENT | HtmlSyntaxKind::HTML_SELF_CLOSING_ELEMENT + ) && let Some(following_node) = comment.following_node() + { + // Only re-attach for attribute-level following nodes. + // The closing tag case is already handled by the check above. + if !HtmlClosingElement::can_cast(following_node.kind()) { + return CommentPlacement::leading(following_node.clone(), comment); + } + } + // Fix trailing comments that should actually be leading comments for the next node. // ```html // 123456 diff --git a/crates/biome_html_formatter/src/verbatim.rs b/crates/biome_html_formatter/src/verbatim.rs index d3d98d1b2850..657cb833107c 100644 --- a/crates/biome_html_formatter/src/verbatim.rs +++ b/crates/biome_html_formatter/src/verbatim.rs @@ -6,7 +6,7 @@ use biome_formatter::format_element::tag::VerbatimKind; use biome_formatter::formatter::Formatter; use biome_formatter::prelude::{ Tag, empty_line, expand_parent, format_with, hard_line_break, line_suffix, - should_nestle_adjacent_doc_comments, soft_line_break_or_space, space, text, + should_nestle_adjacent_doc_comments, space, text, }; use biome_formatter::{ @@ -274,17 +274,15 @@ fn format_leading_comments_impl( } CommentKind::Line => { - // TODO: review logic here + // `//` line comments always require a hard line break after them because + // everything after `//` to end-of-line is part of the comment. Using + // `soft_line_break_or_space` would collapse the comment with the next + // attribute onto a single line, turning the `>` into part of the comment. match comment.lines_after() { 0 => {} - 1 => { - if comment.lines_before() == 0 { - biome_formatter::write!(f, [soft_line_break_or_space()])?; - } else { - biome_formatter::write!(f, [hard_line_break()])?; - } + _ => { + biome_formatter::write!(f, [hard_line_break()])?; } - _ => biome_formatter::write!(f, [empty_line()])?, } } } diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/comments-in-component-tags.svelte b/crates/biome_html_formatter/tests/specs/html/svelte/comments-in-component-tags.svelte new file mode 100644 index 000000000000..c9d7ec7e266d --- /dev/null +++ b/crates/biome_html_formatter/tests/specs/html/svelte/comments-in-component-tags.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/comments-in-component-tags.svelte.snap b/crates/biome_html_formatter/tests/specs/html/svelte/comments-in-component-tags.svelte.snap new file mode 100644 index 000000000000..115d1bbf17ec --- /dev/null +++ b/crates/biome_html_formatter/tests/specs/html/svelte/comments-in-component-tags.svelte.snap @@ -0,0 +1,66 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: svelte/comments-in-component-tags.svelte +--- + +# Input + +```svelte + + + + + + +``` + + +============================= + +# 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 + + + + + + +``` diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/comments-in-tags.svelte b/crates/biome_html_formatter/tests/specs/html/svelte/comments-in-tags.svelte new file mode 100644 index 000000000000..8cf5835046e1 --- /dev/null +++ b/crates/biome_html_formatter/tests/specs/html/svelte/comments-in-tags.svelte @@ -0,0 +1,58 @@ + +
text
+
text
+ + +
text
+ + +
text
+ + +
text
+ + +
text
+ + +
text
+ + +
text
+ + + + + + + + + diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/comments-in-tags.svelte.snap b/crates/biome_html_formatter/tests/specs/html/svelte/comments-in-tags.svelte.snap new file mode 100644 index 000000000000..30cdbef52ce4 --- /dev/null +++ b/crates/biome_html_formatter/tests/specs/html/svelte/comments-in-tags.svelte.snap @@ -0,0 +1,175 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: svelte/comments-in-tags.svelte +--- + +# Input + +```svelte + +
text
+
text
+ + +
text
+ + +
text
+ + +
text
+ + +
text
+ + +
text
+ + +
text
+ + + + + + + + + + +``` + + +============================= + +# 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 + +
text
+
+ text +
+ + +
+ text +
+ + +
+ text +
+ + +
+ text +
+ + +
+ text +
+ + +
+ text +
+ + +
+ text +
+ + + + + + + + + + +``` + +# Lines exceeding max width of 80 characters +``` + 11: +``` diff --git a/crates/biome_html_parser/src/lexer/mod.rs b/crates/biome_html_parser/src/lexer/mod.rs index c74f8f7e8b8e..3c23e8350d74 100644 --- a/crates/biome_html_parser/src/lexer/mod.rs +++ b/crates/biome_html_parser/src/lexer/mod.rs @@ -175,14 +175,24 @@ impl<'src> HtmlLexer<'src> { /// Consume a token in the [HtmlLexContext::InsideTagWithDirectives] context. /// This context is used for Vue files with Vue-specific directives. - fn consume_token_inside_tag_directives(&mut self, current: u8) -> HtmlSyntaxKind { + /// When `svelte` is `true`, also handles `//` and `/* */` as JS-style comments. + fn consume_token_inside_tag_directives(&mut self, current: u8, svelte: bool) -> HtmlSyntaxKind { let dispatched = lookup_byte(current); match dispatched { WHS => self.consume_newline_or_whitespaces(), LSS => self.consume_l_angle(), MOR => self.consume_byte(T![>]), - SLH => self.consume_byte(T![/]), + SLH => { + if svelte { + match self.byte_at(1).map(lookup_byte) { + Some(SLH) => return self.consume_js_line_comment(), + Some(MUL) => return self.consume_js_block_comment(), + _ => {} + } + } + self.consume_byte(T![/]) + } EQL => self.consume_byte(T![=]), EXL => self.consume_byte(T![!]), BEO => { @@ -261,6 +271,21 @@ impl<'src> HtmlLexer<'src> { } } + /// Consume a token in the [HtmlLexContext::InsideTagSvelte] context. + /// This context is used for Svelte files with JS-style comment support. + fn consume_token_inside_tag_svelte(&mut self, current: u8) -> HtmlSyntaxKind { + let dispatched = lookup_byte(current); + + if dispatched == SLH { + match self.byte_at(1).map(lookup_byte) { + Some(SLH) => return self.consume_js_line_comment(), + Some(MUL) => return self.consume_js_block_comment(), + _ => {} + } + } + self.consume_token_inside_tag(current) + } + /// Consume a token in the [HtmlLexContext::Regular] context. fn consume_token(&mut self, current: u8) -> HtmlSyntaxKind { let dispatched = lookup_byte(current); @@ -541,6 +566,49 @@ impl<'src> HtmlLexer<'src> { COMMENT } + /// Consumes a `//` single-line comment, returning COMMENT. + /// Does NOT consume the terminating newline — it must be emitted as a + /// separate NEWLINE trivia token to preserve leading/trailing trivia boundaries. + fn consume_js_line_comment(&mut self) -> HtmlSyntaxKind { + self.advance(2); + while let Some(chr) = self.current_byte() { + match chr { + b'\n' | b'\r' => break, + _ if chr.is_ascii() => self.advance(1), + _ => { + let c = self.current_char_unchecked(); + if is_linebreak(c) { + break; + } + self.advance(c.len_utf8()); + } + } + } + COMMENT + } + + /// Consumes a `/* */` block comment, returning COMMENT. + fn consume_js_block_comment(&mut self) -> HtmlSyntaxKind { + self.advance(2); + while let Some(chr) = self.current_byte() { + let dispatched = lookup_byte(chr); + match dispatched { + MUL if self.byte_at(1).map(lookup_byte) == Some(SLH) => { + self.advance(2); + return COMMENT; + } + IDT | ZER | DIG | WHS | COL | SLH | MIN | MUL => self.advance(1), + _ if chr.is_ascii() => self.advance(1), + _ => self.advance(self.current_char_unchecked().len_utf8()), + } + } + self.push_diagnostic(ParseDiagnostic::new( + "Unterminated block comment, expected `*/`", + self.current_start..self.text_position(), + )); + COMMENT + } + /// Consume a token in the [HtmlLexContext::CdataSection] context. fn consume_inside_cdata(&mut self, current: u8) -> HtmlSyntaxKind { let dispatched = lookup_byte(current); @@ -1271,10 +1339,13 @@ 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::InsideTagWithDirectives => { - self.consume_token_inside_tag_directives(current) + HtmlLexContext::InsideTagWithDirectives { svelte } => { + self.consume_token_inside_tag_directives(current, svelte) } HtmlLexContext::InsideTagAstro => self.consume_token_inside_tag_astro(current), + HtmlLexContext::InsideTagSvelte => { + self.consume_token_inside_tag_svelte(current) + } HtmlLexContext::VueDirectiveArgument => { self.consume_token_vue_directive_argument() } @@ -1438,6 +1509,11 @@ fn is_vue_directive_prefix_bytes(bytes: &[u8]) -> bool { bytes.starts_with(b"v-") } +/// Check if a char is a linebreak (for JS-style comments in Svelte) +fn is_linebreak(chr: char) -> bool { + matches!(chr, '\n' | '\r' | '\u{2028}' | '\u{2029}') +} + /// Identifiers can contain letters, numbers and `_` fn is_at_continue_identifier(byte: u8) -> bool { byte.is_ascii_alphanumeric() || byte == b'_' diff --git a/crates/biome_html_parser/src/lexer/tests.rs b/crates/biome_html_parser/src/lexer/tests.rs index 700b2951a439..ef431ccf5366 100644 --- a/crates/biome_html_parser/src/lexer/tests.rs +++ b/crates/biome_html_parser/src/lexer/tests.rs @@ -409,3 +409,89 @@ fn svelte_keywords() { WHITESPACE: 2, ) } + +#[test] +fn svelte_line_comment_inside_tag() { + assert_lex! { + HtmlLexContext::InsideTagSvelte, + "// comment\n", + COMMENT: 10, + NEWLINE: 1, + } +} + +#[test] +fn svelte_line_comment_without_newline() { + assert_lex! { + HtmlLexContext::InsideTagSvelte, + "// comment", + COMMENT: 10, + } +} + +#[test] +fn svelte_block_comment_single_line() { + assert_lex! { + HtmlLexContext::InsideTagSvelte, + "/* comment */", + COMMENT: 13, + } +} + +#[test] +fn svelte_block_comment_multiline() { + assert_lex! { + HtmlLexContext::InsideTagSvelte, + "/* line1\nline2 */", + COMMENT: 17, + } +} + +#[test] +fn plain_slash_inside_tag_not_a_comment() { + assert_lex! { + HtmlLexContext::InsideTag, + "/", + SLASH: 1, + } +} + +#[test] +fn plain_double_slash_inside_tag_not_a_comment() { + assert_lex! { + HtmlLexContext::InsideTag, + "//", + SLASH: 1, + SLASH: 1, + } +} + +#[test] +fn plain_slash_asterisk_inside_tag_not_a_comment() { + assert_lex! { + HtmlLexContext::InsideTag, + "/*", + SLASH: 1, + HTML_LITERAL: 1, + } +} + +#[test] +fn svelte_slash_remains_slash_in_svelte_context() { + assert_lex! { + HtmlLexContext::InsideTagSvelte, + "/", + SLASH: 1, + } +} + +#[test] +fn svelte_slash_after_comment() { + assert_lex! { + HtmlLexContext::InsideTagSvelte, + "// comment\n/", + COMMENT: 10, + NEWLINE: 1, + SLASH: 1, + } +} diff --git a/crates/biome_html_parser/src/parser.rs b/crates/biome_html_parser/src/parser.rs index f71dc76c553b..d43b3eee6e69 100644 --- a/crates/biome_html_parser/src/parser.rs +++ b/crates/biome_html_parser/src/parser.rs @@ -116,6 +116,7 @@ pub struct HtmlParseOptions { pub(crate) frontmatter: bool, pub(crate) text_expression: Option, pub(crate) vue: bool, + pub(crate) svelte: bool, pub(crate) is_html: bool, } @@ -152,6 +153,11 @@ impl HtmlParseOptions { self } + pub fn with_svelte(mut self) -> Self { + self.svelte = true; + self + } + pub fn is_html(&self) -> bool { self.is_html } @@ -178,7 +184,7 @@ impl From<&HtmlFileSource> for HtmlParseOptions { options = options.with_double_text_expression().with_vue(); } HtmlVariant::Svelte => { - options = options.with_single_text_expression(); + options = options.with_single_text_expression().with_svelte(); } } diff --git a/crates/biome_html_parser/src/syntax/mod.rs b/crates/biome_html_parser/src/syntax/mod.rs index 5992e67c1aec..5532f6e2a2a7 100644 --- a/crates/biome_html_parser/src/syntax/mod.rs +++ b/crates/biome_html_parser/src/syntax/mod.rs @@ -4,7 +4,9 @@ mod svelte; mod vue; use crate::parser::HtmlParser; -use crate::syntax::HtmlSyntaxFeatures::{Astro, DoubleTextExpressions, Svelte, Vue}; +use crate::syntax::HtmlSyntaxFeatures::{ + Astro, DoubleTextExpressions, SingleTextExpressions, Svelte, Vue, +}; use crate::syntax::astro::{ is_at_astro_directive_keyword, is_at_astro_directive_start, parse_astro_directive, parse_astro_fence, parse_astro_spread_or_expression, @@ -37,6 +39,8 @@ pub(crate) enum HtmlSyntaxFeatures { /// Exclusive to those documents that support text expressions with {{ }} DoubleTextExpressions, /// Exclusive to those documents that support text expressions with { } + SingleTextExpressions, + /// Exclusive to Svelte files (for Svelte-specific directives) Svelte, /// Exclusive to those documents that support Vue Vue, @@ -51,7 +55,10 @@ impl SyntaxFeature for HtmlSyntaxFeatures { DoubleTextExpressions => { p.options().text_expression == Some(TextExpressionKind::Double) } - Svelte => p.options().text_expression == Some(TextExpressionKind::Single), + SingleTextExpressions => { + p.options().text_expression == Some(TextExpressionKind::Single) + } + Svelte => p.options().svelte, Vue => p.options().vue, } } @@ -142,10 +149,10 @@ fn parse_doc_type(p: &mut HtmlParser) -> ParsedSyntax { /// will emit diagnostics. We want to allow them if they have no special meaning. #[inline(always)] 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 Vue.is_supported(p) { - HtmlLexContext::InsideTagWithDirectives + HtmlLexContext::InsideTagWithDirectives { svelte: false } + } else if Svelte.is_supported(p) { + HtmlLexContext::InsideTagSvelte } else { HtmlLexContext::InsideTag } @@ -165,11 +172,13 @@ fn is_possible_component(p: &HtmlParser, tag_name: &str) -> bool { /// for parsing component names, not for parsing attributes. #[inline(always)] fn component_name_context(p: &HtmlParser) -> HtmlLexContext { - if Vue.is_supported(p) || Svelte.is_supported(p) { + if Vue.is_supported(p) || Svelte.is_supported(p) || Astro.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::InsideTagWithDirectives + HtmlLexContext::InsideTagWithDirectives { + svelte: Svelte.is_supported(p), + } } else { HtmlLexContext::InsideTag } @@ -478,7 +487,7 @@ fn parse_attribute(p: &mut HtmlParser) -> ParsedSyntax { parse_vue_v_slot_shorthand_directive, |p, m| disabled_vue(p, m.range(p)), ), - T!['{'] if Svelte.is_supported(p) => parse_svelte_spread_or_expression(p), + T!['{'] if SingleTextExpressions.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!['{'] => Svelte.parse_exclusive_syntax( @@ -595,13 +604,16 @@ fn parse_attribute_initializer(p: &mut HtmlParser) -> ParsedSyntax { let m = p.start(); p.bump_with_context(T![=], HtmlLexContext::AttributeValue); if p.at(T!['{']) { - HtmlSyntaxFeatures::Svelte + HtmlSyntaxFeatures::SingleTextExpressions .parse_exclusive_syntax( p, |p| parse_single_text_expression(p, inside_tag_context(p)), |p, m| { - p.err_builder("Expressions are only valid inside Astro files.", m.range(p)) - .with_hint("Remove it or rename the file to have the .astro extension.") + p.err_builder( + "Text expressions are not supported in this context.", + m.range(p), + ) + .with_hint("Remove the expression or use a supported file type.") }, ) .or_recover_with_token_set( @@ -668,7 +680,7 @@ fn parse_double_text_expression(p: &mut HtmlParser, context: HtmlLexContext) -> if p.at(T!["}}"]) { p.expect_with_context(T!["}}"], context); if context == HtmlLexContext::InsideTag - || context == HtmlLexContext::InsideTagWithDirectives + || matches!(context, HtmlLexContext::InsideTagWithDirectives { .. }) { Present(m.complete(p, HTML_ATTRIBUTE_DOUBLE_TEXT_EXPRESSION)) } else { @@ -703,7 +715,7 @@ pub(crate) fn parse_single_text_expression( p: &mut HtmlParser, context: HtmlLexContext, ) -> ParsedSyntax { - if !Svelte.is_supported(p) { + if !SingleTextExpressions.is_supported(p) { return Absent; } @@ -721,8 +733,9 @@ pub(crate) fn parse_single_text_expression( if p.at(T!['}']) { p.bump_remap_with_context(T!['}'], context); if context == HtmlLexContext::InsideTag - || context == HtmlLexContext::InsideTagWithDirectives + || matches!(context, HtmlLexContext::InsideTagWithDirectives { .. }) || context == HtmlLexContext::InsideTagAstro + || context == HtmlLexContext::InsideTagSvelte { Present(m.complete(p, HTML_ATTRIBUTE_SINGLE_TEXT_EXPRESSION)) } else { diff --git a/crates/biome_html_parser/src/syntax/svelte.rs b/crates/biome_html_parser/src/syntax/svelte.rs index 88c0f4f797e5..3bb7738faf07 100644 --- a/crates/biome_html_parser/src/syntax/svelte.rs +++ b/crates/biome_html_parser/src/syntax/svelte.rs @@ -1,5 +1,5 @@ use crate::parser::HtmlParser; -use crate::syntax::HtmlSyntaxFeatures::Svelte; +use crate::syntax::HtmlSyntaxFeatures::{SingleTextExpressions, 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, @@ -341,7 +341,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 !Svelte.is_supported(p) { + if !SingleTextExpressions.is_supported(p) { return Absent; } @@ -363,12 +363,12 @@ pub(crate) fn parse_svelte_spread_or_expression(p: &mut HtmlParser) -> ParsedSyn .parse_element(p) .or_add_diagnostic(p, expected_expression); - p.expect_with_context(T!['}'], HtmlLexContext::InsideTag); + p.expect_with_context(T!['}'], HtmlLexContext::InsideTagSvelte); Present(m.complete(p, HTML_SPREAD_ATTRIBUTE)) } else { p.rewind(checkpoint); m.abandon(p); - parse_single_text_expression(p, HtmlLexContext::InsideTag) + parse_single_text_expression(p, HtmlLexContext::InsideTagSvelte) } } @@ -880,7 +880,7 @@ pub(crate) fn parse_attach_attribute(p: &mut HtmlParser) -> ParsedSyntax { parse_single_text_expression_content(p).or_add_diagnostic(p, expected_text_expression); - p.expect_with_context(T!['}'], HtmlLexContext::InsideTag); + p.expect_with_context(T!['}'], HtmlLexContext::InsideTagSvelte); Present(m.complete(p, SVELTE_ATTACH_ATTRIBUTE)) } @@ -958,7 +958,7 @@ fn parse_svelte_name(p: &mut HtmlParser) -> ParsedSyntax { fn parse_binding_literal(p: &mut HtmlParser) -> ParsedSyntax { let m = p.start(); - p.bump_with_context(HTML_LITERAL, HtmlLexContext::InsideTag); + p.bump_with_context(HTML_LITERAL, HtmlLexContext::InsideTagSvelte); Present(m.complete(p, SVELTE_LITERAL)) } diff --git a/crates/biome_html_parser/src/syntax/vue.rs b/crates/biome_html_parser/src/syntax/vue.rs index 2246b01e46ff..7dbce3ea4c6f 100644 --- a/crates/biome_html_parser/src/syntax/vue.rs +++ b/crates/biome_html_parser/src/syntax/vue.rs @@ -21,7 +21,10 @@ 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::InsideTagWithDirectives); + p.bump_remap_with_context( + IDENT, + HtmlLexContext::InsideTagWithDirectives { svelte: false }, + ); 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() @@ -71,7 +74,10 @@ 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::InsideTagWithDirectives); + p.bump_with_context( + T![@], + HtmlLexContext::InsideTagWithDirectives { svelte: false }, + ); // 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() @@ -100,7 +106,10 @@ 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::InsideTagWithDirectives); + p.bump_with_context( + T![#], + HtmlLexContext::InsideTagWithDirectives { svelte: false }, + ); // 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() @@ -129,7 +138,10 @@ fn parse_vue_directive_argument(p: &mut HtmlParser) -> ParsedSyntax { let m = p.start(); let pos = p.source().position(); - p.bump_with_context(T![:], HtmlLexContext::InsideTagWithDirectives); + p.bump_with_context( + T![:], + HtmlLexContext::InsideTagWithDirectives { svelte: false }, + ); // 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() @@ -149,7 +161,10 @@ 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::InsideTagWithDirectives); + p.expect_with_context( + HTML_LITERAL, + HtmlLexContext::InsideTagWithDirectives { svelte: false }, + ); Present(m.complete(p, VUE_STATIC_ARGUMENT)) } @@ -162,8 +177,14 @@ 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::InsideTagWithDirectives); - p.expect_with_context(T![']'], HtmlLexContext::InsideTagWithDirectives); + p.expect_with_context( + HTML_LITERAL, + HtmlLexContext::InsideTagWithDirectives { svelte: false }, + ); + p.expect_with_context( + T![']'], + HtmlLexContext::InsideTagWithDirectives { svelte: false }, + ); Present(m.complete(p, VUE_DYNAMIC_ARGUMENT)) } @@ -206,12 +227,21 @@ fn parse_vue_modifier(p: &mut HtmlParser) -> ParsedSyntax { let m = p.start(); - p.bump_with_context(T![.], HtmlLexContext::InsideTagWithDirectives); + p.bump_with_context( + T![.], + HtmlLexContext::InsideTagWithDirectives { svelte: false }, + ); if p.at(T![:]) { // `:` is actually a valid modifier, for example `@keydown.:` - p.bump_remap_with_context(HTML_LITERAL, HtmlLexContext::InsideTagWithDirectives); + p.bump_remap_with_context( + HTML_LITERAL, + HtmlLexContext::InsideTagWithDirectives { svelte: false }, + ); } else { - p.expect_with_context(HTML_LITERAL, HtmlLexContext::InsideTagWithDirectives); + p.expect_with_context( + HTML_LITERAL, + HtmlLexContext::InsideTagWithDirectives { svelte: false }, + ); } Present(m.complete(p, VUE_MODIFIER)) diff --git a/crates/biome_html_parser/src/token_source.rs b/crates/biome_html_parser/src/token_source.rs index 213293780f31..ea9981f9bcca 100644 --- a/crates/biome_html_parser/src/token_source.rs +++ b/crates/biome_html_parser/src/token_source.rs @@ -27,9 +27,13 @@ pub(crate) enum HtmlLexContext { Regular, /// 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 Component directives (v-bind, :, @, #, etc.) - InsideTagWithDirectives, + /// Like [InsideTag], but with Vue-style directive tokens enabled (`.`, `:`, `@`, `#`, etc.). + /// When `svelte` is `true`, also recognizes `//` and `/* */` as JS-style comments (for Svelte + /// component names which need both member-expression `.` support and comment support). + InsideTagWithDirectives { svelte: bool }, + /// Like [InsideTag], but with Svelte-specific tokens enabled. + /// This enables parsing of JS-style `//` and `/* */` comments as trivia. + InsideTagSvelte, /// Like [InsideTag], but with Astro-specific tokens enabled. /// This enables parsing of Astro directives (client:, set:, class:, is:, server:) InsideTagAstro, diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/unterminated_js_multiline_comment.svelte b/crates/biome_html_parser/tests/html_specs/error/svelte/unterminated_js_multiline_comment.svelte new file mode 100644 index 000000000000..376d26883b61 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/unterminated_js_multiline_comment.svelte @@ -0,0 +1,4 @@ +
text
diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/unterminated_js_multiline_comment.svelte.snap b/crates/biome_html_parser/tests/html_specs/error/svelte/unterminated_js_multiline_comment.svelte.snap new file mode 100644 index 000000000000..15aec2b02300 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/unterminated_js_multiline_comment.svelte.snap @@ -0,0 +1,94 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- + +## Input + +```svelte +
text
+ +``` + + +## 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..4 "div" [] [], + }, + attributes: HtmlAttributeList [], + r_angle_token: missing (required), + }, + children: HtmlElementList [], + closing_element: missing (required), + }, + ], + eof_token: EOF@4..50 "" [Newline("\n"), Whitespace(" "), Comments("/* block comment\n cl ...")] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..50 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..4 + 0: HTML_ELEMENT@0..4 + 0: HTML_OPENING_ELEMENT@0..4 + 0: L_ANGLE@0..1 "<" [] [] + 1: HTML_TAG_NAME@1..4 + 0: HTML_LITERAL@1..4 "div" [] [] + 2: HTML_ATTRIBUTE_LIST@4..4 + 3: (empty) + 1: HTML_ELEMENT_LIST@4..4 + 2: (empty) + 4: EOF@4..50 "" [Newline("\n"), Whitespace(" "), Comments("/* block comment\n cl ...")] [] + +``` + +## Diagnostics + +``` +unterminated_js_multiline_comment.svelte:2:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unterminated block comment, expected `*/` + + 1 │
2 │ /* block comment + │ ^^^^^^^^^^^^^^^^ + > 3 │ class="foo" + > 4 │ >text
+ > 5 │ + │ + +unterminated_js_multiline_comment.svelte:5:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `>` but instead the file ends + + 3 │ class="foo" + 4 │ >text
+ > 5 │ + │ + + i the file ends here + + 3 │ class="foo" + 4 │ >text + > 5 │ + │ + +``` diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/js_comments_in_component_tags.svelte b/crates/biome_html_parser/tests/html_specs/ok/svelte/js_comments_in_component_tags.svelte new file mode 100644 index 000000000000..c9d7ec7e266d --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/js_comments_in_component_tags.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/js_comments_in_component_tags.svelte.snap b/crates/biome_html_parser/tests/html_specs/ok/svelte/js_comments_in_component_tags.svelte.snap new file mode 100644 index 000000000000..a5e2aafa4af2 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/js_comments_in_component_tags.svelte.snap @@ -0,0 +1,236 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- + +## Input + +```svelte + + + + + + +``` + + +## AST + +``` +HtmlRoot { + bom_token: missing (optional), + frontmatter: missing (optional), + directive: missing (optional), + html: HtmlElementList [ + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@0..1 "<" [] [], + name: HtmlComponentName { + value_token: HTML_LITERAL@1..10 "Component" [] [], + }, + attributes: HtmlAttributeList [ + HtmlAttribute { + name: HtmlAttributeName { + value_token: HTML_LITERAL@10..35 "prop" [Newline("\n"), Comments("/* block comment */"), Newline("\n")] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@35..36 "=" [] [], + value: HtmlString { + value_token: HTML_STRING_LITERAL@36..43 "\"value\"" [] [], + }, + }, + }, + ], + slash_token: SLASH@43..45 "/" [Newline("\n")] [], + r_angle_token: R_ANGLE@45..46 ">" [] [], + }, + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@46..48 "<" [Newline("\n")] [], + name: HtmlComponentName { + value_token: HTML_LITERAL@48..57 "Component" [] [], + }, + attributes: HtmlAttributeList [ + HtmlAttribute { + name: HtmlAttributeName { + value_token: HTML_LITERAL@57..78 "prop" [Newline("\n"), Comments("// line comment"), Newline("\n")] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@78..79 "=" [] [], + value: HtmlString { + value_token: HTML_STRING_LITERAL@79..86 "\"value\"" [] [], + }, + }, + }, + ], + slash_token: SLASH@86..88 "/" [Newline("\n")] [], + r_angle_token: R_ANGLE@88..89 ">" [] [], + }, + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@89..91 "<" [Newline("\n")] [], + name: HtmlComponentName { + value_token: HTML_LITERAL@91..128 "Component" [] [Whitespace(" "), Comments("/* inline block comme ..."), Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlAttribute { + name: HtmlAttributeName { + value_token: HTML_LITERAL@128..132 "prop" [] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@132..133 "=" [] [], + value: HtmlString { + value_token: HTML_STRING_LITERAL@133..141 "\"value\"" [] [Whitespace(" ")], + }, + }, + }, + ], + slash_token: SLASH@141..142 "/" [] [], + r_angle_token: R_ANGLE@142..143 ">" [] [], + }, + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@143..145 "<" [Newline("\n")] [], + name: HtmlComponentName { + value_token: HTML_LITERAL@145..177 "Component" [] [Whitespace(" "), Comments("// inline line comment")], + }, + attributes: HtmlAttributeList [ + HtmlAttribute { + name: HtmlAttributeName { + value_token: HTML_LITERAL@177..182 "prop" [Newline("\n")] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@182..183 "=" [] [], + value: HtmlString { + value_token: HTML_STRING_LITERAL@183..191 "\"value\"" [] [Whitespace(" ")], + }, + }, + }, + ], + slash_token: SLASH@191..192 "/" [] [], + r_angle_token: R_ANGLE@192..193 ">" [] [], + }, + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@193..195 "<" [Newline("\n")] [], + name: HtmlMemberName { + object: HtmlComponentName { + value_token: HTML_LITERAL@195..204 "Component" [] [], + }, + dot_token: DOT@204..205 "." [] [], + member: HtmlTagName { + value_token: HTML_LITERAL@205..211 "Member" [] [], + }, + }, + attributes: HtmlAttributeList [ + HtmlAttribute { + name: HtmlAttributeName { + value_token: HTML_LITERAL@211..236 "prop" [Newline("\n"), Comments("/* block comment */"), Newline("\n")] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@236..237 "=" [] [], + value: HtmlString { + value_token: HTML_STRING_LITERAL@237..244 "\"value\"" [] [], + }, + }, + }, + ], + slash_token: SLASH@244..246 "/" [Newline("\n")] [], + r_angle_token: R_ANGLE@246..247 ">" [] [], + }, + ], + eof_token: EOF@247..248 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..248 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..247 + 0: HTML_SELF_CLOSING_ELEMENT@0..46 + 0: L_ANGLE@0..1 "<" [] [] + 1: HTML_COMPONENT_NAME@1..10 + 0: HTML_LITERAL@1..10 "Component" [] [] + 2: HTML_ATTRIBUTE_LIST@10..43 + 0: HTML_ATTRIBUTE@10..43 + 0: HTML_ATTRIBUTE_NAME@10..35 + 0: HTML_LITERAL@10..35 "prop" [Newline("\n"), Comments("/* block comment */"), Newline("\n")] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@35..43 + 0: EQ@35..36 "=" [] [] + 1: HTML_STRING@36..43 + 0: HTML_STRING_LITERAL@36..43 "\"value\"" [] [] + 3: SLASH@43..45 "/" [Newline("\n")] [] + 4: R_ANGLE@45..46 ">" [] [] + 1: HTML_SELF_CLOSING_ELEMENT@46..89 + 0: L_ANGLE@46..48 "<" [Newline("\n")] [] + 1: HTML_COMPONENT_NAME@48..57 + 0: HTML_LITERAL@48..57 "Component" [] [] + 2: HTML_ATTRIBUTE_LIST@57..86 + 0: HTML_ATTRIBUTE@57..86 + 0: HTML_ATTRIBUTE_NAME@57..78 + 0: HTML_LITERAL@57..78 "prop" [Newline("\n"), Comments("// line comment"), Newline("\n")] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@78..86 + 0: EQ@78..79 "=" [] [] + 1: HTML_STRING@79..86 + 0: HTML_STRING_LITERAL@79..86 "\"value\"" [] [] + 3: SLASH@86..88 "/" [Newline("\n")] [] + 4: R_ANGLE@88..89 ">" [] [] + 2: HTML_SELF_CLOSING_ELEMENT@89..143 + 0: L_ANGLE@89..91 "<" [Newline("\n")] [] + 1: HTML_COMPONENT_NAME@91..128 + 0: HTML_LITERAL@91..128 "Component" [] [Whitespace(" "), Comments("/* inline block comme ..."), Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@128..141 + 0: HTML_ATTRIBUTE@128..141 + 0: HTML_ATTRIBUTE_NAME@128..132 + 0: HTML_LITERAL@128..132 "prop" [] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@132..141 + 0: EQ@132..133 "=" [] [] + 1: HTML_STRING@133..141 + 0: HTML_STRING_LITERAL@133..141 "\"value\"" [] [Whitespace(" ")] + 3: SLASH@141..142 "/" [] [] + 4: R_ANGLE@142..143 ">" [] [] + 3: HTML_SELF_CLOSING_ELEMENT@143..193 + 0: L_ANGLE@143..145 "<" [Newline("\n")] [] + 1: HTML_COMPONENT_NAME@145..177 + 0: HTML_LITERAL@145..177 "Component" [] [Whitespace(" "), Comments("// inline line comment")] + 2: HTML_ATTRIBUTE_LIST@177..191 + 0: HTML_ATTRIBUTE@177..191 + 0: HTML_ATTRIBUTE_NAME@177..182 + 0: HTML_LITERAL@177..182 "prop" [Newline("\n")] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@182..191 + 0: EQ@182..183 "=" [] [] + 1: HTML_STRING@183..191 + 0: HTML_STRING_LITERAL@183..191 "\"value\"" [] [Whitespace(" ")] + 3: SLASH@191..192 "/" [] [] + 4: R_ANGLE@192..193 ">" [] [] + 4: HTML_SELF_CLOSING_ELEMENT@193..247 + 0: L_ANGLE@193..195 "<" [Newline("\n")] [] + 1: HTML_MEMBER_NAME@195..211 + 0: HTML_COMPONENT_NAME@195..204 + 0: HTML_LITERAL@195..204 "Component" [] [] + 1: DOT@204..205 "." [] [] + 2: HTML_TAG_NAME@205..211 + 0: HTML_LITERAL@205..211 "Member" [] [] + 2: HTML_ATTRIBUTE_LIST@211..244 + 0: HTML_ATTRIBUTE@211..244 + 0: HTML_ATTRIBUTE_NAME@211..236 + 0: HTML_LITERAL@211..236 "prop" [Newline("\n"), Comments("/* block comment */"), Newline("\n")] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@236..244 + 0: EQ@236..237 "=" [] [] + 1: HTML_STRING@237..244 + 0: HTML_STRING_LITERAL@237..244 "\"value\"" [] [] + 3: SLASH@244..246 "/" [Newline("\n")] [] + 4: R_ANGLE@246..247 ">" [] [] + 4: EOF@247..248 "" [Newline("\n")] [] + +``` diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/js_comments_in_tag.svelte b/crates/biome_html_parser/tests/html_specs/ok/svelte/js_comments_in_tag.svelte new file mode 100644 index 000000000000..bce860dac2db --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/js_comments_in_tag.svelte @@ -0,0 +1,33 @@ + + +
text
+ + + +content + + + + +
text
diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/js_comments_in_tag.svelte.snap b/crates/biome_html_parser/tests/html_specs/ok/svelte/js_comments_in_tag.svelte.snap new file mode 100644 index 000000000000..8d7a670b9898 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/js_comments_in_tag.svelte.snap @@ -0,0 +1,438 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- + +## Input + +```svelte + + +
text
+ + + +content + + + + +
text
+ +``` + + +## 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..7 "button" [] [], + }, + attributes: HtmlAttributeList [ + HtmlAttribute { + name: HtmlAttributeName { + value_token: HTML_LITERAL@7..42 "onclick" [Newline("\n"), Whitespace(" "), Comments("// single-line comment"), Newline("\n"), Whitespace(" ")] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@42..43 "=" [] [], + value: HtmlAttributeSingleTextExpression { + l_curly_token: L_CURLY@43..44 "{" [] [], + expression: HtmlTextExpression { + html_literal_token: HTML_LITERAL@44..54 "doTheThing" [] [], + }, + r_curly_token: R_CURLY@54..55 "}" [] [], + }, + }, + }, + ], + r_angle_token: R_ANGLE@55..57 ">" [Newline("\n")] [], + }, + children: HtmlElementList [ + HtmlContent { + value_token: HTML_LITERAL@57..65 "click me" [] [], + }, + ], + closing_element: HtmlClosingElement { + l_angle_token: L_ANGLE@65..66 "<" [] [], + slash_token: SLASH@66..67 "/" [] [], + name: HtmlTagName { + value_token: HTML_LITERAL@67..73 "button" [] [], + }, + r_angle_token: R_ANGLE@73..74 ">" [] [], + }, + }, + HtmlElement { + opening_element: HtmlOpeningElement { + l_angle_token: L_ANGLE@74..77 "<" [Newline("\n"), Newline("\n")] [], + name: HtmlTagName { + value_token: HTML_LITERAL@77..80 "div" [] [], + }, + attributes: HtmlAttributeList [ + HtmlAttribute { + name: HtmlAttributeName { + value_token: HTML_LITERAL@80..110 "class" [Newline("\n"), Whitespace(" "), Comments("/* block comment */"), Newline("\n"), Whitespace(" ")] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@110..111 "=" [] [], + value: HtmlString { + value_token: HTML_STRING_LITERAL@111..116 "\"foo\"" [] [], + }, + }, + }, + ], + r_angle_token: R_ANGLE@116..118 ">" [Newline("\n")] [], + }, + children: HtmlElementList [ + HtmlContent { + value_token: HTML_LITERAL@118..122 "text" [] [], + }, + ], + closing_element: HtmlClosingElement { + l_angle_token: L_ANGLE@122..123 "<" [] [], + slash_token: SLASH@123..124 "/" [] [], + name: HtmlTagName { + value_token: HTML_LITERAL@124..127 "div" [] [], + }, + r_angle_token: R_ANGLE@127..128 ">" [] [], + }, + }, + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@128..131 "<" [Newline("\n"), Newline("\n")] [], + name: HtmlTagName { + value_token: HTML_LITERAL@131..136 "input" [] [], + }, + attributes: HtmlAttributeList [ + HtmlAttribute { + name: HtmlAttributeName { + value_token: HTML_LITERAL@136..181 "type" [Newline("\n"), Whitespace(" "), Comments("/* multi-line\n bl ..."), Newline("\n"), Whitespace(" ")] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@181..182 "=" [] [], + value: HtmlString { + value_token: HTML_STRING_LITERAL@182..188 "\"text\"" [] [], + }, + }, + }, + ], + slash_token: SLASH@188..190 "/" [Newline("\n")] [], + r_angle_token: R_ANGLE@190..191 ">" [] [], + }, + HtmlElement { + opening_element: HtmlOpeningElement { + l_angle_token: L_ANGLE@191..194 "<" [Newline("\n"), Newline("\n")] [], + name: HtmlTagName { + value_token: HTML_LITERAL@194..198 "span" [] [], + }, + attributes: HtmlAttributeList [ + HtmlAttribute { + name: HtmlAttributeName { + value_token: HTML_LITERAL@198..235 "data-test" [Newline("\n"), Whitespace(" "), Comments("// comment before attr"), Newline("\n"), Whitespace(" ")] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@235..236 "=" [] [], + value: HtmlString { + value_token: HTML_STRING_LITERAL@236..243 "\"value\"" [] [], + }, + }, + }, + HtmlAttribute { + name: HtmlAttributeName { + value_token: HTML_LITERAL@243..278 "id" [Newline("\n"), Whitespace(" "), Comments("/* comment between at ..."), Newline("\n"), Whitespace(" ")] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@278..279 "=" [] [], + value: HtmlString { + value_token: HTML_STRING_LITERAL@279..288 "\"my-span\"" [] [], + }, + }, + }, + ], + r_angle_token: R_ANGLE@288..290 ">" [Newline("\n")] [], + }, + children: HtmlElementList [ + HtmlContent { + value_token: HTML_LITERAL@290..297 "content" [] [], + }, + ], + closing_element: HtmlClosingElement { + l_angle_token: L_ANGLE@297..298 "<" [] [], + slash_token: SLASH@298..299 "/" [] [], + name: HtmlTagName { + value_token: HTML_LITERAL@299..303 "span" [] [], + }, + r_angle_token: R_ANGLE@303..304 ">" [] [], + }, + }, + HtmlElement { + opening_element: HtmlOpeningElement { + l_angle_token: L_ANGLE@304..308 "<" [Newline("\n"), Newline("\n"), Newline("\n")] [], + name: HtmlTagName { + value_token: HTML_LITERAL@308..314 "button" [] [], + }, + attributes: HtmlAttributeList [ + HtmlAttribute { + name: HtmlAttributeName { + value_token: HTML_LITERAL@314..349 "onclick" [Newline("\n"), Whitespace(" "), Comments("// single-line comment"), Newline("\n"), Whitespace(" ")] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@349..350 "=" [] [], + value: HtmlAttributeSingleTextExpression { + l_curly_token: L_CURLY@350..351 "{" [] [], + expression: HtmlTextExpression { + html_literal_token: HTML_LITERAL@351..361 "doTheThing" [] [], + }, + r_curly_token: R_CURLY@361..362 "}" [] [], + }, + }, + }, + HtmlAttributeSingleTextExpression { + l_curly_token: L_CURLY@362..390 "{" [Newline("\n"), Whitespace(" "), Comments("// comment after attr"), Newline("\n"), Whitespace(" ")] [], + expression: HtmlTextExpression { + html_literal_token: HTML_LITERAL@390..394 "type" [] [], + }, + r_curly_token: R_CURLY@394..395 "}" [] [], + }, + ], + r_angle_token: R_ANGLE@395..426 ">" [Newline("\n"), Whitespace(" "), Comments("// comment after shor ..."), Newline("\n")] [], + }, + children: HtmlElementList [ + HtmlContent { + value_token: HTML_LITERAL@426..434 "click me" [] [], + }, + ], + closing_element: HtmlClosingElement { + l_angle_token: L_ANGLE@434..435 "<" [] [], + slash_token: SLASH@435..436 "/" [] [], + name: HtmlTagName { + value_token: HTML_LITERAL@436..442 "button" [] [], + }, + r_angle_token: R_ANGLE@442..443 ">" [] [], + }, + }, + HtmlElement { + opening_element: HtmlOpeningElement { + l_angle_token: L_ANGLE@443..446 "<" [Newline("\n"), Newline("\n")] [], + name: HtmlTagName { + value_token: HTML_LITERAL@446..470 "div" [] [Whitespace(" "), Comments("/* block comment */"), Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlAttribute { + name: HtmlAttributeName { + value_token: HTML_LITERAL@470..475 "class" [] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@475..476 "=" [] [], + value: HtmlString { + value_token: HTML_STRING_LITERAL@476..501 "\"foo\"" [] [Whitespace(" "), Comments("/* block comment */")], + }, + }, + }, + ], + r_angle_token: R_ANGLE@501..502 ">" [] [], + }, + children: HtmlElementList [ + HtmlContent { + value_token: HTML_LITERAL@502..506 "text" [] [], + }, + ], + closing_element: HtmlClosingElement { + l_angle_token: L_ANGLE@506..507 "<" [] [], + slash_token: SLASH@507..508 "/" [] [], + name: HtmlTagName { + value_token: HTML_LITERAL@508..511 "div" [] [], + }, + r_angle_token: R_ANGLE@511..512 ">" [] [], + }, + }, + ], + eof_token: EOF@512..513 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..513 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..512 + 0: HTML_ELEMENT@0..74 + 0: HTML_OPENING_ELEMENT@0..57 + 0: L_ANGLE@0..1 "<" [] [] + 1: HTML_TAG_NAME@1..7 + 0: HTML_LITERAL@1..7 "button" [] [] + 2: HTML_ATTRIBUTE_LIST@7..55 + 0: HTML_ATTRIBUTE@7..55 + 0: HTML_ATTRIBUTE_NAME@7..42 + 0: HTML_LITERAL@7..42 "onclick" [Newline("\n"), Whitespace(" "), Comments("// single-line comment"), Newline("\n"), Whitespace(" ")] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@42..55 + 0: EQ@42..43 "=" [] [] + 1: HTML_ATTRIBUTE_SINGLE_TEXT_EXPRESSION@43..55 + 0: L_CURLY@43..44 "{" [] [] + 1: HTML_TEXT_EXPRESSION@44..54 + 0: HTML_LITERAL@44..54 "doTheThing" [] [] + 2: R_CURLY@54..55 "}" [] [] + 3: R_ANGLE@55..57 ">" [Newline("\n")] [] + 1: HTML_ELEMENT_LIST@57..65 + 0: HTML_CONTENT@57..65 + 0: HTML_LITERAL@57..65 "click me" [] [] + 2: HTML_CLOSING_ELEMENT@65..74 + 0: L_ANGLE@65..66 "<" [] [] + 1: SLASH@66..67 "/" [] [] + 2: HTML_TAG_NAME@67..73 + 0: HTML_LITERAL@67..73 "button" [] [] + 3: R_ANGLE@73..74 ">" [] [] + 1: HTML_ELEMENT@74..128 + 0: HTML_OPENING_ELEMENT@74..118 + 0: L_ANGLE@74..77 "<" [Newline("\n"), Newline("\n")] [] + 1: HTML_TAG_NAME@77..80 + 0: HTML_LITERAL@77..80 "div" [] [] + 2: HTML_ATTRIBUTE_LIST@80..116 + 0: HTML_ATTRIBUTE@80..116 + 0: HTML_ATTRIBUTE_NAME@80..110 + 0: HTML_LITERAL@80..110 "class" [Newline("\n"), Whitespace(" "), Comments("/* block comment */"), Newline("\n"), Whitespace(" ")] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@110..116 + 0: EQ@110..111 "=" [] [] + 1: HTML_STRING@111..116 + 0: HTML_STRING_LITERAL@111..116 "\"foo\"" [] [] + 3: R_ANGLE@116..118 ">" [Newline("\n")] [] + 1: HTML_ELEMENT_LIST@118..122 + 0: HTML_CONTENT@118..122 + 0: HTML_LITERAL@118..122 "text" [] [] + 2: HTML_CLOSING_ELEMENT@122..128 + 0: L_ANGLE@122..123 "<" [] [] + 1: SLASH@123..124 "/" [] [] + 2: HTML_TAG_NAME@124..127 + 0: HTML_LITERAL@124..127 "div" [] [] + 3: R_ANGLE@127..128 ">" [] [] + 2: HTML_SELF_CLOSING_ELEMENT@128..191 + 0: L_ANGLE@128..131 "<" [Newline("\n"), Newline("\n")] [] + 1: HTML_TAG_NAME@131..136 + 0: HTML_LITERAL@131..136 "input" [] [] + 2: HTML_ATTRIBUTE_LIST@136..188 + 0: HTML_ATTRIBUTE@136..188 + 0: HTML_ATTRIBUTE_NAME@136..181 + 0: HTML_LITERAL@136..181 "type" [Newline("\n"), Whitespace(" "), Comments("/* multi-line\n bl ..."), Newline("\n"), Whitespace(" ")] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@181..188 + 0: EQ@181..182 "=" [] [] + 1: HTML_STRING@182..188 + 0: HTML_STRING_LITERAL@182..188 "\"text\"" [] [] + 3: SLASH@188..190 "/" [Newline("\n")] [] + 4: R_ANGLE@190..191 ">" [] [] + 3: HTML_ELEMENT@191..304 + 0: HTML_OPENING_ELEMENT@191..290 + 0: L_ANGLE@191..194 "<" [Newline("\n"), Newline("\n")] [] + 1: HTML_TAG_NAME@194..198 + 0: HTML_LITERAL@194..198 "span" [] [] + 2: HTML_ATTRIBUTE_LIST@198..288 + 0: HTML_ATTRIBUTE@198..243 + 0: HTML_ATTRIBUTE_NAME@198..235 + 0: HTML_LITERAL@198..235 "data-test" [Newline("\n"), Whitespace(" "), Comments("// comment before attr"), Newline("\n"), Whitespace(" ")] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@235..243 + 0: EQ@235..236 "=" [] [] + 1: HTML_STRING@236..243 + 0: HTML_STRING_LITERAL@236..243 "\"value\"" [] [] + 1: HTML_ATTRIBUTE@243..288 + 0: HTML_ATTRIBUTE_NAME@243..278 + 0: HTML_LITERAL@243..278 "id" [Newline("\n"), Whitespace(" "), Comments("/* comment between at ..."), Newline("\n"), Whitespace(" ")] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@278..288 + 0: EQ@278..279 "=" [] [] + 1: HTML_STRING@279..288 + 0: HTML_STRING_LITERAL@279..288 "\"my-span\"" [] [] + 3: R_ANGLE@288..290 ">" [Newline("\n")] [] + 1: HTML_ELEMENT_LIST@290..297 + 0: HTML_CONTENT@290..297 + 0: HTML_LITERAL@290..297 "content" [] [] + 2: HTML_CLOSING_ELEMENT@297..304 + 0: L_ANGLE@297..298 "<" [] [] + 1: SLASH@298..299 "/" [] [] + 2: HTML_TAG_NAME@299..303 + 0: HTML_LITERAL@299..303 "span" [] [] + 3: R_ANGLE@303..304 ">" [] [] + 4: HTML_ELEMENT@304..443 + 0: HTML_OPENING_ELEMENT@304..426 + 0: L_ANGLE@304..308 "<" [Newline("\n"), Newline("\n"), Newline("\n")] [] + 1: HTML_TAG_NAME@308..314 + 0: HTML_LITERAL@308..314 "button" [] [] + 2: HTML_ATTRIBUTE_LIST@314..395 + 0: HTML_ATTRIBUTE@314..362 + 0: HTML_ATTRIBUTE_NAME@314..349 + 0: HTML_LITERAL@314..349 "onclick" [Newline("\n"), Whitespace(" "), Comments("// single-line comment"), Newline("\n"), Whitespace(" ")] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@349..362 + 0: EQ@349..350 "=" [] [] + 1: HTML_ATTRIBUTE_SINGLE_TEXT_EXPRESSION@350..362 + 0: L_CURLY@350..351 "{" [] [] + 1: HTML_TEXT_EXPRESSION@351..361 + 0: HTML_LITERAL@351..361 "doTheThing" [] [] + 2: R_CURLY@361..362 "}" [] [] + 1: HTML_ATTRIBUTE_SINGLE_TEXT_EXPRESSION@362..395 + 0: L_CURLY@362..390 "{" [Newline("\n"), Whitespace(" "), Comments("// comment after attr"), Newline("\n"), Whitespace(" ")] [] + 1: HTML_TEXT_EXPRESSION@390..394 + 0: HTML_LITERAL@390..394 "type" [] [] + 2: R_CURLY@394..395 "}" [] [] + 3: R_ANGLE@395..426 ">" [Newline("\n"), Whitespace(" "), Comments("// comment after shor ..."), Newline("\n")] [] + 1: HTML_ELEMENT_LIST@426..434 + 0: HTML_CONTENT@426..434 + 0: HTML_LITERAL@426..434 "click me" [] [] + 2: HTML_CLOSING_ELEMENT@434..443 + 0: L_ANGLE@434..435 "<" [] [] + 1: SLASH@435..436 "/" [] [] + 2: HTML_TAG_NAME@436..442 + 0: HTML_LITERAL@436..442 "button" [] [] + 3: R_ANGLE@442..443 ">" [] [] + 5: HTML_ELEMENT@443..512 + 0: HTML_OPENING_ELEMENT@443..502 + 0: L_ANGLE@443..446 "<" [Newline("\n"), Newline("\n")] [] + 1: HTML_TAG_NAME@446..470 + 0: HTML_LITERAL@446..470 "div" [] [Whitespace(" "), Comments("/* block comment */"), Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@470..501 + 0: HTML_ATTRIBUTE@470..501 + 0: HTML_ATTRIBUTE_NAME@470..475 + 0: HTML_LITERAL@470..475 "class" [] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@475..501 + 0: EQ@475..476 "=" [] [] + 1: HTML_STRING@476..501 + 0: HTML_STRING_LITERAL@476..501 "\"foo\"" [] [Whitespace(" "), Comments("/* block comment */")] + 3: R_ANGLE@501..502 ">" [] [] + 1: HTML_ELEMENT_LIST@502..506 + 0: HTML_CONTENT@502..506 + 0: HTML_LITERAL@502..506 "text" [] [] + 2: HTML_CLOSING_ELEMENT@506..512 + 0: L_ANGLE@506..507 "<" [] [] + 1: SLASH@507..508 "/" [] [] + 2: HTML_TAG_NAME@508..511 + 0: HTML_LITERAL@508..511 "div" [] [] + 3: R_ANGLE@511..512 ">" [] [] + 4: EOF@512..513 "" [Newline("\n")] [] + +``` diff --git a/packages/prettier-compare/package.json b/packages/prettier-compare/package.json index 63548d8e24d1..6e1b91de56d1 100644 --- a/packages/prettier-compare/package.json +++ b/packages/prettier-compare/package.json @@ -14,7 +14,7 @@ "@opentui/react": "^0.1.75", "prettier": "^3.8.1", "prettier-plugin-astro": "^0.14.1", - "prettier-plugin-svelte": "^3.4.1", + "prettier-plugin-svelte": "^3.5.0", "react": "^19.2.4" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 644322b0e24e..e00e0c3e127c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -191,8 +191,8 @@ importers: specifier: ^0.14.1 version: 0.14.1 prettier-plugin-svelte: - specifier: ^3.4.1 - version: 3.4.1(prettier@3.8.1)(svelte@5.46.4) + specifier: ^3.5.0 + version: 3.5.0(prettier@3.8.1)(svelte@5.46.4) react: specifier: ^19.2.4 version: 19.2.4 @@ -1789,8 +1789,8 @@ packages: resolution: {integrity: sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==} engines: {node: ^14.15.0 || >=16.0.0} - prettier-plugin-svelte@3.4.1: - resolution: {integrity: sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg==} + prettier-plugin-svelte@3.5.0: + resolution: {integrity: sha512-2lLO/7EupnjO/95t+XZesXs8Bf3nYLIDfCo270h5QWbj/vjLqmrQ1LiRk9LPggxSDsnVYfehamZNf+rgQYApZg==} peerDependencies: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 @@ -3850,7 +3850,7 @@ snapshots: prettier: 3.8.1 sass-formatter: 0.7.9 - prettier-plugin-svelte@3.4.1(prettier@3.8.1)(svelte@5.46.4): + prettier-plugin-svelte@3.5.0(prettier@3.8.1)(svelte@5.46.4): dependencies: prettier: 3.8.1 svelte: 5.46.4