diff --git a/crates/biome_html_parser/src/lexer/mod.rs b/crates/biome_html_parser/src/lexer/mod.rs index 4c799f15aa6c..b8294abe2e62 100644 --- a/crates/biome_html_parser/src/lexer/mod.rs +++ b/crates/biome_html_parser/src/lexer/mod.rs @@ -88,6 +88,16 @@ impl<'src> HtmlLexer<'src> { } } + fn consume_token_attribute_value(&mut self, current: u8) -> HtmlSyntaxKind { + match current { + b'\n' | b'\r' | b'\t' | b' ' => self.consume_newline_or_whitespaces(), + b'<' => self.consume_byte(T![<]), + b'>' => self.consume_byte(T![>]), + b'\'' | b'"' => self.consume_string_literal(current), + _ => self.consume_unquoted_string_literal(), + } + } + /// Bumps the current byte and creates a lexed token of the passed in kind. #[inline] fn consume_byte(&mut self, tok: HtmlSyntaxKind) -> HtmlSyntaxKind { @@ -233,6 +243,41 @@ impl<'src> HtmlLexer<'src> { } } + /// Consume an attribute value that is not quoted. + /// + /// See: https://html.spec.whatwg.org/#attributes-2 under "Unquoted attribute value syntax" + fn consume_unquoted_string_literal(&mut self) -> HtmlSyntaxKind { + let mut content_started = false; + let mut encountered_invalid = false; + while let Some(current) = self.current_byte() { + match current { + // these characters safely terminate an unquoted attribute value + b'\n' | b'\r' | b'\t' | b' ' | b'>' => break, + // these characters are absolutely invalid in an unquoted attribute value + b'?' | b'\'' | b'"' | b'=' | b'<' | b'`' => { + encountered_invalid = true; + break; + } + _ if current.is_ascii() => { + self.advance(1); + content_started = true; + } + _ => break, + } + } + + if content_started && !encountered_invalid { + HTML_STRING_LITERAL + } else { + let char = self.current_char_unchecked(); + self.push_diagnostic(ParseDiagnostic::new( + "Unexpected character in unquoted attribute value", + self.text_position()..self.text_position() + char.text_len(), + )); + self.consume_unexpected_character() + } + } + fn consume_l_angle(&mut self) -> HtmlSyntaxKind { self.assert_byte(b'<'); @@ -385,6 +430,7 @@ impl<'src> Lexer<'src> for HtmlLexer<'src> { Some(current) => match context { HtmlLexContext::Regular => self.consume_token(current), HtmlLexContext::OutsideTag => self.consume_token_outside_tag(current), + HtmlLexContext::AttributeValue => self.consume_token_attribute_value(current), }, None => EOF, } diff --git a/crates/biome_html_parser/src/lexer/tests.rs b/crates/biome_html_parser/src/lexer/tests.rs index 7940a7e575b5..e5ecdfd921e6 100644 --- a/crates/biome_html_parser/src/lexer/tests.rs +++ b/crates/biome_html_parser/src/lexer/tests.rs @@ -250,3 +250,38 @@ fn html_text_spaces_with_lines() { HTML_LITERAL: 18, } } + +#[test] +fn unquoted_attribute_value_1() { + assert_lex! { + HtmlLexContext::AttributeValue, + "value", + HTML_STRING_LITERAL: 5, + } +} + +#[test] +fn unquoted_attribute_value_2() { + assert_lex! { + HtmlLexContext::AttributeValue, + "value value\tvalue\n", + HTML_STRING_LITERAL: 5, + WHITESPACE: 1, + HTML_STRING_LITERAL: 5, + WHITESPACE: 1, + HTML_STRING_LITERAL: 5, + NEWLINE: 1, + } +} + +#[test] +fn unquoted_attribute_value_invalid_chars() { + assert_lex! { + HtmlLexContext::AttributeValue, + "?<='\"`", + ERROR_TOKEN: 1, + L_ANGLE: 1, + ERROR_TOKEN: 1, + ERROR_TOKEN: 3, + } +} diff --git a/crates/biome_html_parser/src/syntax/mod.rs b/crates/biome_html_parser/src/syntax/mod.rs index d8470466a923..e7cca9c9d8f8 100644 --- a/crates/biome_html_parser/src/syntax/mod.rs +++ b/crates/biome_html_parser/src/syntax/mod.rs @@ -214,7 +214,7 @@ fn parse_literal(p: &mut HtmlParser) -> ParsedSyntax { Present(m.complete(p, HTML_NAME)) } -fn parse_string_literal(p: &mut HtmlParser) -> ParsedSyntax { +fn parse_attribute_string_literal(p: &mut HtmlParser) -> ParsedSyntax { if !p.at(HTML_STRING_LITERAL) { return Absent; } @@ -230,7 +230,7 @@ fn parse_attribute_initializer(p: &mut HtmlParser) -> ParsedSyntax { return Absent; } let m = p.start(); - p.bump(T![=]); - parse_string_literal(p).or_add_diagnostic(p, expected_initializer); + p.bump_with_context(T![=], HtmlLexContext::AttributeValue); + parse_attribute_string_literal(p).or_add_diagnostic(p, expected_initializer); Present(m.complete(p, HTML_ATTRIBUTE_INITIALIZER_CLAUSE)) } diff --git a/crates/biome_html_parser/src/token_source.rs b/crates/biome_html_parser/src/token_source.rs index 9c8d6b809ad9..336f0b8ccd6a 100644 --- a/crates/biome_html_parser/src/token_source.rs +++ b/crates/biome_html_parser/src/token_source.rs @@ -23,6 +23,10 @@ pub(crate) enum HtmlLexContext { /// /// The exeptions being `<` which indicates the start of a tag, and `>` which is invalid syntax if not preceeded with a `<`. OutsideTag, + /// 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. + AttributeValue, } impl LexContext for HtmlLexContext { diff --git a/crates/biome_html_parser/tests/html_specs/error/attributes/invalid-unqouted-value1.html b/crates/biome_html_parser/tests/html_specs/error/attributes/invalid-unqouted-value1.html new file mode 100644 index 000000000000..56609257ea95 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/attributes/invalid-unqouted-value1.html @@ -0,0 +1,4 @@ +
+
foo
+
foo
+
diff --git a/crates/biome_html_parser/tests/html_specs/error/attributes/invalid-unqouted-value1.html.snap b/crates/biome_html_parser/tests/html_specs/error/attributes/invalid-unqouted-value1.html.snap new file mode 100644 index 000000000000..ff3240e8dcbe --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/attributes/invalid-unqouted-value1.html.snap @@ -0,0 +1,251 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```html +
+
foo
+
foo
+
+ +``` + + +## AST + +``` +HtmlRoot { + bom_token: missing (optional), + directive: missing (optional), + html: HtmlElement { + opening_element: HtmlOpeningElement { + l_angle_token: L_ANGLE@0..1 "<" [] [], + name: HtmlName { + value_token: HTML_LITERAL@1..4 "div" [] [], + }, + attributes: HtmlAttributeList [], + r_angle_token: R_ANGLE@4..5 ">" [] [], + }, + children: HtmlElementList [ + HtmlBogusElement { + items: [ + HtmlBogus { + items: [ + L_ANGLE@5..8 "<" [Newline("\n"), Whitespace("\t")] [], + HtmlName { + value_token: HTML_LITERAL@8..12 "div" [] [Whitespace(" ")], + }, + HtmlBogus { + items: [ + HtmlAttribute { + name: HtmlName { + value_token: HTML_LITERAL@12..17 "class" [] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@17..18 "=" [] [], + value: missing (required), + }, + }, + HtmlBogusElement { + items: [ + ERROR_TOKEN@18..20 "=" [] [Whitespace(" ")], + ], + }, + ], + }, + R_ANGLE@20..21 ">" [] [], + ], + }, + HtmlElementList [ + HtmlContent { + value_token: HTML_LITERAL@21..24 "foo" [] [], + }, + ], + HtmlClosingElement { + l_angle_token: L_ANGLE@24..25 "<" [] [], + slash_token: SLASH@25..26 "/" [] [], + name: HtmlName { + value_token: HTML_LITERAL@26..29 "div" [] [], + }, + r_angle_token: R_ANGLE@29..30 ">" [] [], + }, + ], + }, + HtmlBogusElement { + items: [ + HtmlBogus { + items: [ + L_ANGLE@30..33 "<" [Newline("\n"), Whitespace("\t")] [], + HtmlName { + value_token: HTML_LITERAL@33..37 "div" [] [Whitespace(" ")], + }, + HtmlBogus { + items: [ + HtmlAttribute { + name: HtmlName { + value_token: HTML_LITERAL@37..42 "class" [] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@42..43 "=" [] [], + value: missing (required), + }, + }, + HtmlBogusElement { + items: [ + ERROR_TOKEN@43..45 "?" [] [Whitespace(" ")], + ], + }, + ], + }, + R_ANGLE@45..46 ">" [] [], + ], + }, + HtmlElementList [ + HtmlContent { + value_token: HTML_LITERAL@46..49 "foo" [] [], + }, + ], + HtmlClosingElement { + l_angle_token: L_ANGLE@49..50 "<" [] [], + slash_token: SLASH@50..51 "/" [] [], + name: HtmlName { + value_token: HTML_LITERAL@51..54 "div" [] [], + }, + r_angle_token: R_ANGLE@54..55 ">" [] [], + }, + ], + }, + ], + closing_element: HtmlClosingElement { + l_angle_token: L_ANGLE@55..57 "<" [Newline("\n")] [], + slash_token: SLASH@57..58 "/" [] [], + name: HtmlName { + value_token: HTML_LITERAL@58..61 "div" [] [], + }, + r_angle_token: R_ANGLE@61..62 ">" [] [], + }, + }, + eof_token: EOF@62..63 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..63 + 0: (empty) + 1: (empty) + 2: HTML_ELEMENT@0..62 + 0: HTML_OPENING_ELEMENT@0..5 + 0: L_ANGLE@0..1 "<" [] [] + 1: HTML_NAME@1..4 + 0: HTML_LITERAL@1..4 "div" [] [] + 2: HTML_ATTRIBUTE_LIST@4..4 + 3: R_ANGLE@4..5 ">" [] [] + 1: HTML_ELEMENT_LIST@5..55 + 0: HTML_BOGUS_ELEMENT@5..30 + 0: HTML_BOGUS@5..21 + 0: L_ANGLE@5..8 "<" [Newline("\n"), Whitespace("\t")] [] + 1: HTML_NAME@8..12 + 0: HTML_LITERAL@8..12 "div" [] [Whitespace(" ")] + 2: HTML_BOGUS@12..20 + 0: HTML_ATTRIBUTE@12..18 + 0: HTML_NAME@12..17 + 0: HTML_LITERAL@12..17 "class" [] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@17..18 + 0: EQ@17..18 "=" [] [] + 1: (empty) + 1: HTML_BOGUS_ELEMENT@18..20 + 0: ERROR_TOKEN@18..20 "=" [] [Whitespace(" ")] + 3: R_ANGLE@20..21 ">" [] [] + 1: HTML_ELEMENT_LIST@21..24 + 0: HTML_CONTENT@21..24 + 0: HTML_LITERAL@21..24 "foo" [] [] + 2: HTML_CLOSING_ELEMENT@24..30 + 0: L_ANGLE@24..25 "<" [] [] + 1: SLASH@25..26 "/" [] [] + 2: HTML_NAME@26..29 + 0: HTML_LITERAL@26..29 "div" [] [] + 3: R_ANGLE@29..30 ">" [] [] + 1: HTML_BOGUS_ELEMENT@30..55 + 0: HTML_BOGUS@30..46 + 0: L_ANGLE@30..33 "<" [Newline("\n"), Whitespace("\t")] [] + 1: HTML_NAME@33..37 + 0: HTML_LITERAL@33..37 "div" [] [Whitespace(" ")] + 2: HTML_BOGUS@37..45 + 0: HTML_ATTRIBUTE@37..43 + 0: HTML_NAME@37..42 + 0: HTML_LITERAL@37..42 "class" [] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@42..43 + 0: EQ@42..43 "=" [] [] + 1: (empty) + 1: HTML_BOGUS_ELEMENT@43..45 + 0: ERROR_TOKEN@43..45 "?" [] [Whitespace(" ")] + 3: R_ANGLE@45..46 ">" [] [] + 1: HTML_ELEMENT_LIST@46..49 + 0: HTML_CONTENT@46..49 + 0: HTML_LITERAL@46..49 "foo" [] [] + 2: HTML_CLOSING_ELEMENT@49..55 + 0: L_ANGLE@49..50 "<" [] [] + 1: SLASH@50..51 "/" [] [] + 2: HTML_NAME@51..54 + 0: HTML_LITERAL@51..54 "div" [] [] + 3: R_ANGLE@54..55 ">" [] [] + 2: HTML_CLOSING_ELEMENT@55..62 + 0: L_ANGLE@55..57 "<" [Newline("\n")] [] + 1: SLASH@57..58 "/" [] [] + 2: HTML_NAME@58..61 + 0: HTML_LITERAL@58..61 "div" [] [] + 3: R_ANGLE@61..62 ">" [] [] + 3: EOF@62..63 "" [Newline("\n")] [] + +``` + +## Diagnostics + +``` +invalid-unqouted-value1.html:2:13 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected character in unquoted attribute value + + 1 │
+ > 2 │
foo
+ │ ^ + 3 │
foo
+ 4 │
+ +invalid-unqouted-value1.html:2:13 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected character `=` + + 1 │
+ > 2 │
foo
+ │ ^ + 3 │
foo
+ 4 │
+ +invalid-unqouted-value1.html:3:13 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected character in unquoted attribute value + + 1 │
+ 2 │
foo
+ > 3 │
foo
+ │ ^ + 4 │
+ 5 │ + +invalid-unqouted-value1.html:3:13 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected character `?` + + 1 │
+ 2 │
foo
+ > 3 │
foo
+ │ ^ + 4 │
+ 5 │ + +``` diff --git a/crates/biome_html_parser/tests/html_specs/error/attributes/invalid-unquoted-value2.html b/crates/biome_html_parser/tests/html_specs/error/attributes/invalid-unquoted-value2.html new file mode 100644 index 000000000000..fe61e92c4303 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/attributes/invalid-unquoted-value2.html @@ -0,0 +1,4 @@ +
+
foo
+
foo
+
diff --git a/crates/biome_html_parser/tests/html_specs/error/attributes/invalid-unquoted-value2.html.snap b/crates/biome_html_parser/tests/html_specs/error/attributes/invalid-unquoted-value2.html.snap new file mode 100644 index 000000000000..46cdb81333ee --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/attributes/invalid-unquoted-value2.html.snap @@ -0,0 +1,293 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```html +
+
foo
+
foo
+
+ +``` + + +## AST + +``` +HtmlRoot { + bom_token: missing (optional), + directive: missing (optional), + html: HtmlElement { + opening_element: HtmlOpeningElement { + l_angle_token: L_ANGLE@0..1 "<" [] [], + name: HtmlName { + value_token: HTML_LITERAL@1..4 "div" [] [], + }, + attributes: HtmlAttributeList [], + r_angle_token: R_ANGLE@4..5 ">" [] [], + }, + children: HtmlElementList [ + HtmlBogusElement { + items: [ + HtmlBogus { + items: [ + L_ANGLE@5..8 "<" [Newline("\n"), Whitespace("\t")] [], + HtmlName { + value_token: HTML_LITERAL@8..12 "div" [] [Whitespace(" ")], + }, + HtmlBogus { + items: [ + HtmlAttribute { + name: HtmlName { + value_token: HTML_LITERAL@12..17 "class" [] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@17..18 "=" [] [], + value: missing (required), + }, + }, + HtmlBogusElement { + items: [ + ERROR_TOKEN@18..22 "foo\"" [] [], + HTML_LITERAL@22..26 "bar" [] [Whitespace(" ")], + ], + }, + ], + }, + R_ANGLE@26..27 ">" [] [], + ], + }, + HtmlElementList [ + HtmlContent { + value_token: HTML_LITERAL@27..30 "foo" [] [], + }, + ], + HtmlClosingElement { + l_angle_token: L_ANGLE@30..31 "<" [] [], + slash_token: SLASH@31..32 "/" [] [], + name: HtmlName { + value_token: HTML_LITERAL@32..35 "div" [] [], + }, + r_angle_token: R_ANGLE@35..36 ">" [] [], + }, + ], + }, + HtmlBogusElement { + items: [ + HtmlBogus { + items: [ + L_ANGLE@36..39 "<" [Newline("\n"), Whitespace("\t")] [], + HtmlName { + value_token: HTML_LITERAL@39..43 "div" [] [Whitespace(" ")], + }, + HtmlBogus { + items: [ + HtmlAttribute { + name: HtmlName { + value_token: HTML_LITERAL@43..48 "class" [] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@48..49 "=" [] [], + value: missing (required), + }, + }, + HtmlBogusElement { + items: [ + ERROR_TOKEN@49..53 "foo'" [] [], + HTML_LITERAL@53..57 "bar" [] [Whitespace(" ")], + ], + }, + ], + }, + R_ANGLE@57..58 ">" [] [], + ], + }, + HtmlElementList [ + HtmlContent { + value_token: HTML_LITERAL@58..61 "foo" [] [], + }, + ], + HtmlClosingElement { + l_angle_token: L_ANGLE@61..62 "<" [] [], + slash_token: SLASH@62..63 "/" [] [], + name: HtmlName { + value_token: HTML_LITERAL@63..66 "div" [] [], + }, + r_angle_token: R_ANGLE@66..67 ">" [] [], + }, + ], + }, + ], + closing_element: HtmlClosingElement { + l_angle_token: L_ANGLE@67..69 "<" [Newline("\n")] [], + slash_token: SLASH@69..70 "/" [] [], + name: HtmlName { + value_token: HTML_LITERAL@70..73 "div" [] [], + }, + r_angle_token: R_ANGLE@73..74 ">" [] [], + }, + }, + eof_token: EOF@74..75 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..75 + 0: (empty) + 1: (empty) + 2: HTML_ELEMENT@0..74 + 0: HTML_OPENING_ELEMENT@0..5 + 0: L_ANGLE@0..1 "<" [] [] + 1: HTML_NAME@1..4 + 0: HTML_LITERAL@1..4 "div" [] [] + 2: HTML_ATTRIBUTE_LIST@4..4 + 3: R_ANGLE@4..5 ">" [] [] + 1: HTML_ELEMENT_LIST@5..67 + 0: HTML_BOGUS_ELEMENT@5..36 + 0: HTML_BOGUS@5..27 + 0: L_ANGLE@5..8 "<" [Newline("\n"), Whitespace("\t")] [] + 1: HTML_NAME@8..12 + 0: HTML_LITERAL@8..12 "div" [] [Whitespace(" ")] + 2: HTML_BOGUS@12..26 + 0: HTML_ATTRIBUTE@12..18 + 0: HTML_NAME@12..17 + 0: HTML_LITERAL@12..17 "class" [] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@17..18 + 0: EQ@17..18 "=" [] [] + 1: (empty) + 1: HTML_BOGUS_ELEMENT@18..26 + 0: ERROR_TOKEN@18..22 "foo\"" [] [] + 1: HTML_LITERAL@22..26 "bar" [] [Whitespace(" ")] + 3: R_ANGLE@26..27 ">" [] [] + 1: HTML_ELEMENT_LIST@27..30 + 0: HTML_CONTENT@27..30 + 0: HTML_LITERAL@27..30 "foo" [] [] + 2: HTML_CLOSING_ELEMENT@30..36 + 0: L_ANGLE@30..31 "<" [] [] + 1: SLASH@31..32 "/" [] [] + 2: HTML_NAME@32..35 + 0: HTML_LITERAL@32..35 "div" [] [] + 3: R_ANGLE@35..36 ">" [] [] + 1: HTML_BOGUS_ELEMENT@36..67 + 0: HTML_BOGUS@36..58 + 0: L_ANGLE@36..39 "<" [Newline("\n"), Whitespace("\t")] [] + 1: HTML_NAME@39..43 + 0: HTML_LITERAL@39..43 "div" [] [Whitespace(" ")] + 2: HTML_BOGUS@43..57 + 0: HTML_ATTRIBUTE@43..49 + 0: HTML_NAME@43..48 + 0: HTML_LITERAL@43..48 "class" [] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@48..49 + 0: EQ@48..49 "=" [] [] + 1: (empty) + 1: HTML_BOGUS_ELEMENT@49..57 + 0: ERROR_TOKEN@49..53 "foo'" [] [] + 1: HTML_LITERAL@53..57 "bar" [] [Whitespace(" ")] + 3: R_ANGLE@57..58 ">" [] [] + 1: HTML_ELEMENT_LIST@58..61 + 0: HTML_CONTENT@58..61 + 0: HTML_LITERAL@58..61 "foo" [] [] + 2: HTML_CLOSING_ELEMENT@61..67 + 0: L_ANGLE@61..62 "<" [] [] + 1: SLASH@62..63 "/" [] [] + 2: HTML_NAME@63..66 + 0: HTML_LITERAL@63..66 "div" [] [] + 3: R_ANGLE@66..67 ">" [] [] + 2: HTML_CLOSING_ELEMENT@67..74 + 0: L_ANGLE@67..69 "<" [Newline("\n")] [] + 1: SLASH@69..70 "/" [] [] + 2: HTML_NAME@70..73 + 0: HTML_LITERAL@70..73 "div" [] [] + 3: R_ANGLE@73..74 ">" [] [] + 3: EOF@74..75 "" [Newline("\n")] [] + +``` + +## Diagnostics + +``` +invalid-unquoted-value2.html:2:13 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an initializer but instead found 'foo"'. + + 1 │
+ > 2 │
foo
+ │ ^^^^ + 3 │
foo
+ 4 │
+ + i Expected an initializer here. + + 1 │
+ > 2 │
foo
+ │ ^^^^ + 3 │
foo
+ 4 │
+ +invalid-unquoted-value2.html:2:16 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected character in unquoted attribute value + + 1 │
+ > 2 │
foo
+ │ ^ + 3 │
foo
+ 4 │
+ +invalid-unquoted-value2.html:2:16 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected character `"` + + 1 │
+ > 2 │
foo
+ │ ^ + 3 │
foo
+ 4 │
+ +invalid-unquoted-value2.html:3:13 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an initializer but instead found 'foo''. + + 1 │
+ 2 │
foo
+ > 3 │
foo
+ │ ^^^^ + 4 │
+ 5 │ + + i Expected an initializer here. + + 1 │
+ 2 │
foo
+ > 3 │
foo
+ │ ^^^^ + 4 │
+ 5 │ + +invalid-unquoted-value2.html:3:16 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected character in unquoted attribute value + + 1 │
+ 2 │
foo
+ > 3 │
foo
+ │ ^ + 4 │
+ 5 │ + +invalid-unquoted-value2.html:3:16 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected character `'` + + 1 │
+ 2 │
foo
+ > 3 │
foo
+ │ ^ + 4 │
+ 5 │ + +``` diff --git a/crates/biome_html_parser/tests/html_specs/ok/attributes/attributes-unquoted.html b/crates/biome_html_parser/tests/html_specs/ok/attributes/attributes-unquoted.html new file mode 100644 index 000000000000..ff54955bd955 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/ok/attributes/attributes-unquoted.html @@ -0,0 +1,11 @@ +
+
bar
+
bar
+
bar
+
bar
+
bar
+
bar
+ + +
diff --git a/crates/biome_html_parser/tests/html_specs/ok/attributes/attributes-unquoted.html.snap b/crates/biome_html_parser/tests/html_specs/ok/attributes/attributes-unquoted.html.snap new file mode 100644 index 000000000000..920afd0d2e16 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/ok/attributes/attributes-unquoted.html.snap @@ -0,0 +1,493 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```html +
+
bar
+
bar
+
bar
+
bar
+
bar
+
bar
+ + +
+ +``` + + +## AST + +``` +HtmlRoot { + bom_token: missing (optional), + directive: missing (optional), + html: HtmlElement { + opening_element: HtmlOpeningElement { + l_angle_token: L_ANGLE@0..1 "<" [] [], + name: HtmlName { + value_token: HTML_LITERAL@1..4 "div" [] [], + }, + attributes: HtmlAttributeList [], + r_angle_token: R_ANGLE@4..5 ">" [] [], + }, + children: HtmlElementList [ + HtmlElement { + opening_element: HtmlOpeningElement { + l_angle_token: L_ANGLE@5..8 "<" [Newline("\n"), Whitespace("\t")] [], + name: HtmlName { + value_token: HTML_LITERAL@8..12 "div" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlAttribute { + name: HtmlName { + value_token: HTML_LITERAL@12..16 "data" [] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@16..17 "=" [] [], + value: HtmlString { + value_token: HTML_STRING_LITERAL@17..20 "foo" [] [], + }, + }, + }, + ], + r_angle_token: R_ANGLE@20..21 ">" [] [], + }, + children: HtmlElementList [ + HtmlContent { + value_token: HTML_LITERAL@21..24 "bar" [] [], + }, + ], + closing_element: HtmlClosingElement { + l_angle_token: L_ANGLE@24..25 "<" [] [], + slash_token: SLASH@25..26 "/" [] [], + name: HtmlName { + value_token: HTML_LITERAL@26..29 "div" [] [], + }, + r_angle_token: R_ANGLE@29..30 ">" [] [], + }, + }, + HtmlElement { + opening_element: HtmlOpeningElement { + l_angle_token: L_ANGLE@30..33 "<" [Newline("\n"), Whitespace("\t")] [], + name: HtmlName { + value_token: HTML_LITERAL@33..37 "div" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlAttribute { + name: HtmlName { + value_token: HTML_LITERAL@37..41 "data" [] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@41..42 "=" [] [], + value: HtmlString { + value_token: HTML_STRING_LITERAL@42..46 "foo" [] [Whitespace(" ")], + }, + }, + }, + ], + r_angle_token: R_ANGLE@46..47 ">" [] [], + }, + children: HtmlElementList [ + HtmlContent { + value_token: HTML_LITERAL@47..50 "bar" [] [], + }, + ], + closing_element: HtmlClosingElement { + l_angle_token: L_ANGLE@50..51 "<" [] [], + slash_token: SLASH@51..52 "/" [] [], + name: HtmlName { + value_token: HTML_LITERAL@52..55 "div" [] [], + }, + r_angle_token: R_ANGLE@55..56 ">" [] [], + }, + }, + HtmlElement { + opening_element: HtmlOpeningElement { + l_angle_token: L_ANGLE@56..59 "<" [Newline("\n"), Whitespace("\t")] [], + name: HtmlName { + value_token: HTML_LITERAL@59..63 "div" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlAttribute { + name: HtmlName { + value_token: HTML_LITERAL@63..68 "data" [] [Whitespace(" ")], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@68..70 "=" [] [Whitespace(" ")], + value: HtmlString { + value_token: HTML_STRING_LITERAL@70..74 "foo" [] [Whitespace(" ")], + }, + }, + }, + ], + r_angle_token: R_ANGLE@74..75 ">" [] [], + }, + children: HtmlElementList [ + HtmlContent { + value_token: HTML_LITERAL@75..78 "bar" [] [], + }, + ], + closing_element: HtmlClosingElement { + l_angle_token: L_ANGLE@78..79 "<" [] [], + slash_token: SLASH@79..80 "/" [] [], + name: HtmlName { + value_token: HTML_LITERAL@80..83 "div" [] [], + }, + r_angle_token: R_ANGLE@83..84 ">" [] [], + }, + }, + HtmlElement { + opening_element: HtmlOpeningElement { + l_angle_token: L_ANGLE@84..87 "<" [Newline("\n"), Whitespace("\t")] [], + name: HtmlName { + value_token: HTML_LITERAL@87..91 "div" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlAttribute { + name: HtmlName { + value_token: HTML_LITERAL@91..95 "data" [] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@95..97 "=" [] [Whitespace("\t")], + value: HtmlString { + value_token: HTML_STRING_LITERAL@97..101 "foo" [] [Whitespace(" ")], + }, + }, + }, + ], + r_angle_token: R_ANGLE@101..102 ">" [] [], + }, + children: HtmlElementList [ + HtmlContent { + value_token: HTML_LITERAL@102..105 "bar" [] [], + }, + ], + closing_element: HtmlClosingElement { + l_angle_token: L_ANGLE@105..106 "<" [] [], + slash_token: SLASH@106..107 "/" [] [], + name: HtmlName { + value_token: HTML_LITERAL@107..110 "div" [] [], + }, + r_angle_token: R_ANGLE@110..111 ">" [] [], + }, + }, + HtmlElement { + opening_element: HtmlOpeningElement { + l_angle_token: L_ANGLE@111..114 "<" [Newline("\n"), Whitespace("\t")] [], + name: HtmlName { + value_token: HTML_LITERAL@114..118 "div" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlAttribute { + name: HtmlName { + value_token: HTML_LITERAL@118..122 "data" [] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@122..124 "=" [] [Whitespace("\t")], + value: HtmlString { + value_token: HTML_STRING_LITERAL@124..128 "foo" [] [Whitespace("\t")], + }, + }, + }, + ], + r_angle_token: R_ANGLE@128..129 ">" [] [], + }, + children: HtmlElementList [ + HtmlContent { + value_token: HTML_LITERAL@129..132 "bar" [] [], + }, + ], + closing_element: HtmlClosingElement { + l_angle_token: L_ANGLE@132..133 "<" [] [], + slash_token: SLASH@133..134 "/" [] [], + name: HtmlName { + value_token: HTML_LITERAL@134..137 "div" [] [], + }, + r_angle_token: R_ANGLE@137..138 ">" [] [], + }, + }, + HtmlElement { + opening_element: HtmlOpeningElement { + l_angle_token: L_ANGLE@138..141 "<" [Newline("\n"), Whitespace("\t")] [], + name: HtmlName { + value_token: HTML_LITERAL@141..145 "div" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlAttribute { + name: HtmlName { + value_token: HTML_LITERAL@145..149 "data" [] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@149..150 "=" [] [], + value: HtmlString { + value_token: HTML_STRING_LITERAL@150..154 "foo" [Newline("\n")] [], + }, + }, + }, + ], + r_angle_token: R_ANGLE@154..155 ">" [] [], + }, + children: HtmlElementList [ + HtmlContent { + value_token: HTML_LITERAL@155..158 "bar" [] [], + }, + ], + closing_element: HtmlClosingElement { + l_angle_token: L_ANGLE@158..159 "<" [] [], + slash_token: SLASH@159..160 "/" [] [], + name: HtmlName { + value_token: HTML_LITERAL@160..163 "div" [] [], + }, + r_angle_token: R_ANGLE@163..164 ">" [] [], + }, + }, + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@164..167 "<" [Newline("\n"), Whitespace("\t")] [], + name: HtmlName { + value_token: HTML_LITERAL@167..171 "img" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlAttribute { + name: HtmlName { + value_token: HTML_LITERAL@171..175 "data" [] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@175..176 "=" [] [], + value: HtmlString { + value_token: HTML_STRING_LITERAL@176..180 "foo/" [] [], + }, + }, + }, + ], + slash_token: missing (optional), + r_angle_token: R_ANGLE@180..181 ">" [] [], + }, + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@181..184 "<" [Newline("\n"), Whitespace("\t")] [], + name: HtmlName { + value_token: HTML_LITERAL@184..188 "img" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlAttribute { + name: HtmlName { + value_token: HTML_LITERAL@188..192 "data" [] [], + }, + initializer: HtmlAttributeInitializerClause { + eq_token: EQ@192..193 "=" [] [], + value: HtmlString { + value_token: HTML_STRING_LITERAL@193..197 "foo" [] [Whitespace(" ")], + }, + }, + }, + ], + slash_token: SLASH@197..198 "/" [] [], + r_angle_token: R_ANGLE@198..199 ">" [] [], + }, + ], + closing_element: HtmlClosingElement { + l_angle_token: L_ANGLE@199..201 "<" [Newline("\n")] [], + slash_token: SLASH@201..202 "/" [] [], + name: HtmlName { + value_token: HTML_LITERAL@202..205 "div" [] [], + }, + r_angle_token: R_ANGLE@205..206 ">" [] [], + }, + }, + eof_token: EOF@206..207 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..207 + 0: (empty) + 1: (empty) + 2: HTML_ELEMENT@0..206 + 0: HTML_OPENING_ELEMENT@0..5 + 0: L_ANGLE@0..1 "<" [] [] + 1: HTML_NAME@1..4 + 0: HTML_LITERAL@1..4 "div" [] [] + 2: HTML_ATTRIBUTE_LIST@4..4 + 3: R_ANGLE@4..5 ">" [] [] + 1: HTML_ELEMENT_LIST@5..199 + 0: HTML_ELEMENT@5..30 + 0: HTML_OPENING_ELEMENT@5..21 + 0: L_ANGLE@5..8 "<" [Newline("\n"), Whitespace("\t")] [] + 1: HTML_NAME@8..12 + 0: HTML_LITERAL@8..12 "div" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@12..20 + 0: HTML_ATTRIBUTE@12..20 + 0: HTML_NAME@12..16 + 0: HTML_LITERAL@12..16 "data" [] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@16..20 + 0: EQ@16..17 "=" [] [] + 1: HTML_STRING@17..20 + 0: HTML_STRING_LITERAL@17..20 "foo" [] [] + 3: R_ANGLE@20..21 ">" [] [] + 1: HTML_ELEMENT_LIST@21..24 + 0: HTML_CONTENT@21..24 + 0: HTML_LITERAL@21..24 "bar" [] [] + 2: HTML_CLOSING_ELEMENT@24..30 + 0: L_ANGLE@24..25 "<" [] [] + 1: SLASH@25..26 "/" [] [] + 2: HTML_NAME@26..29 + 0: HTML_LITERAL@26..29 "div" [] [] + 3: R_ANGLE@29..30 ">" [] [] + 1: HTML_ELEMENT@30..56 + 0: HTML_OPENING_ELEMENT@30..47 + 0: L_ANGLE@30..33 "<" [Newline("\n"), Whitespace("\t")] [] + 1: HTML_NAME@33..37 + 0: HTML_LITERAL@33..37 "div" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@37..46 + 0: HTML_ATTRIBUTE@37..46 + 0: HTML_NAME@37..41 + 0: HTML_LITERAL@37..41 "data" [] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@41..46 + 0: EQ@41..42 "=" [] [] + 1: HTML_STRING@42..46 + 0: HTML_STRING_LITERAL@42..46 "foo" [] [Whitespace(" ")] + 3: R_ANGLE@46..47 ">" [] [] + 1: HTML_ELEMENT_LIST@47..50 + 0: HTML_CONTENT@47..50 + 0: HTML_LITERAL@47..50 "bar" [] [] + 2: HTML_CLOSING_ELEMENT@50..56 + 0: L_ANGLE@50..51 "<" [] [] + 1: SLASH@51..52 "/" [] [] + 2: HTML_NAME@52..55 + 0: HTML_LITERAL@52..55 "div" [] [] + 3: R_ANGLE@55..56 ">" [] [] + 2: HTML_ELEMENT@56..84 + 0: HTML_OPENING_ELEMENT@56..75 + 0: L_ANGLE@56..59 "<" [Newline("\n"), Whitespace("\t")] [] + 1: HTML_NAME@59..63 + 0: HTML_LITERAL@59..63 "div" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@63..74 + 0: HTML_ATTRIBUTE@63..74 + 0: HTML_NAME@63..68 + 0: HTML_LITERAL@63..68 "data" [] [Whitespace(" ")] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@68..74 + 0: EQ@68..70 "=" [] [Whitespace(" ")] + 1: HTML_STRING@70..74 + 0: HTML_STRING_LITERAL@70..74 "foo" [] [Whitespace(" ")] + 3: R_ANGLE@74..75 ">" [] [] + 1: HTML_ELEMENT_LIST@75..78 + 0: HTML_CONTENT@75..78 + 0: HTML_LITERAL@75..78 "bar" [] [] + 2: HTML_CLOSING_ELEMENT@78..84 + 0: L_ANGLE@78..79 "<" [] [] + 1: SLASH@79..80 "/" [] [] + 2: HTML_NAME@80..83 + 0: HTML_LITERAL@80..83 "div" [] [] + 3: R_ANGLE@83..84 ">" [] [] + 3: HTML_ELEMENT@84..111 + 0: HTML_OPENING_ELEMENT@84..102 + 0: L_ANGLE@84..87 "<" [Newline("\n"), Whitespace("\t")] [] + 1: HTML_NAME@87..91 + 0: HTML_LITERAL@87..91 "div" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@91..101 + 0: HTML_ATTRIBUTE@91..101 + 0: HTML_NAME@91..95 + 0: HTML_LITERAL@91..95 "data" [] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@95..101 + 0: EQ@95..97 "=" [] [Whitespace("\t")] + 1: HTML_STRING@97..101 + 0: HTML_STRING_LITERAL@97..101 "foo" [] [Whitespace(" ")] + 3: R_ANGLE@101..102 ">" [] [] + 1: HTML_ELEMENT_LIST@102..105 + 0: HTML_CONTENT@102..105 + 0: HTML_LITERAL@102..105 "bar" [] [] + 2: HTML_CLOSING_ELEMENT@105..111 + 0: L_ANGLE@105..106 "<" [] [] + 1: SLASH@106..107 "/" [] [] + 2: HTML_NAME@107..110 + 0: HTML_LITERAL@107..110 "div" [] [] + 3: R_ANGLE@110..111 ">" [] [] + 4: HTML_ELEMENT@111..138 + 0: HTML_OPENING_ELEMENT@111..129 + 0: L_ANGLE@111..114 "<" [Newline("\n"), Whitespace("\t")] [] + 1: HTML_NAME@114..118 + 0: HTML_LITERAL@114..118 "div" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@118..128 + 0: HTML_ATTRIBUTE@118..128 + 0: HTML_NAME@118..122 + 0: HTML_LITERAL@118..122 "data" [] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@122..128 + 0: EQ@122..124 "=" [] [Whitespace("\t")] + 1: HTML_STRING@124..128 + 0: HTML_STRING_LITERAL@124..128 "foo" [] [Whitespace("\t")] + 3: R_ANGLE@128..129 ">" [] [] + 1: HTML_ELEMENT_LIST@129..132 + 0: HTML_CONTENT@129..132 + 0: HTML_LITERAL@129..132 "bar" [] [] + 2: HTML_CLOSING_ELEMENT@132..138 + 0: L_ANGLE@132..133 "<" [] [] + 1: SLASH@133..134 "/" [] [] + 2: HTML_NAME@134..137 + 0: HTML_LITERAL@134..137 "div" [] [] + 3: R_ANGLE@137..138 ">" [] [] + 5: HTML_ELEMENT@138..164 + 0: HTML_OPENING_ELEMENT@138..155 + 0: L_ANGLE@138..141 "<" [Newline("\n"), Whitespace("\t")] [] + 1: HTML_NAME@141..145 + 0: HTML_LITERAL@141..145 "div" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@145..154 + 0: HTML_ATTRIBUTE@145..154 + 0: HTML_NAME@145..149 + 0: HTML_LITERAL@145..149 "data" [] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@149..154 + 0: EQ@149..150 "=" [] [] + 1: HTML_STRING@150..154 + 0: HTML_STRING_LITERAL@150..154 "foo" [Newline("\n")] [] + 3: R_ANGLE@154..155 ">" [] [] + 1: HTML_ELEMENT_LIST@155..158 + 0: HTML_CONTENT@155..158 + 0: HTML_LITERAL@155..158 "bar" [] [] + 2: HTML_CLOSING_ELEMENT@158..164 + 0: L_ANGLE@158..159 "<" [] [] + 1: SLASH@159..160 "/" [] [] + 2: HTML_NAME@160..163 + 0: HTML_LITERAL@160..163 "div" [] [] + 3: R_ANGLE@163..164 ">" [] [] + 6: HTML_SELF_CLOSING_ELEMENT@164..181 + 0: L_ANGLE@164..167 "<" [Newline("\n"), Whitespace("\t")] [] + 1: HTML_NAME@167..171 + 0: HTML_LITERAL@167..171 "img" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@171..180 + 0: HTML_ATTRIBUTE@171..180 + 0: HTML_NAME@171..175 + 0: HTML_LITERAL@171..175 "data" [] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@175..180 + 0: EQ@175..176 "=" [] [] + 1: HTML_STRING@176..180 + 0: HTML_STRING_LITERAL@176..180 "foo/" [] [] + 3: (empty) + 4: R_ANGLE@180..181 ">" [] [] + 7: HTML_SELF_CLOSING_ELEMENT@181..199 + 0: L_ANGLE@181..184 "<" [Newline("\n"), Whitespace("\t")] [] + 1: HTML_NAME@184..188 + 0: HTML_LITERAL@184..188 "img" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@188..197 + 0: HTML_ATTRIBUTE@188..197 + 0: HTML_NAME@188..192 + 0: HTML_LITERAL@188..192 "data" [] [] + 1: HTML_ATTRIBUTE_INITIALIZER_CLAUSE@192..197 + 0: EQ@192..193 "=" [] [] + 1: HTML_STRING@193..197 + 0: HTML_STRING_LITERAL@193..197 "foo" [] [Whitespace(" ")] + 3: SLASH@197..198 "/" [] [] + 4: R_ANGLE@198..199 ">" [] [] + 2: HTML_CLOSING_ELEMENT@199..206 + 0: L_ANGLE@199..201 "<" [Newline("\n")] [] + 1: SLASH@201..202 "/" [] [] + 2: HTML_NAME@202..205 + 0: HTML_LITERAL@202..205 "div" [] [] + 3: R_ANGLE@205..206 ">" [] [] + 3: EOF@206..207 "" [Newline("\n")] [] + +```