@@ -143,15 +143,14 @@ impl<'a, C: ParserConfig> ParserImpl<'a, C> {
143143 // Check if this is a comment (starts with `<!--`) or a doctype (starts with `<!doctype` or `<!DOCTYPE`)
144144 if let Some ( rest) = self . source_text . get ( start_pos..) {
145145 // Check for HTML comment `<!--`
146- if rest. starts_with ( "!--" ) {
147- // Find `-->` to close the comment
148- if let Some ( end_offset) = rest. find ( "-->" ) {
149- let comment_end = ( start_pos + end_offset + 3 ) as u32 ;
146+ if let Some ( after_open) = rest. strip_prefix ( "!--" ) {
147+ // Close must come after the open; an overlapping `<!-->` is not a
148+ // real comment (and naive search would slice backwards and panic).
149+ if let Some ( content_len) = after_open. find ( "-->" ) {
150+ let comment_end = ( start_pos + 3 + content_len + 3 ) as u32 ;
150151 let comment_start = span; // `<` position
151152
152- // Extract comment content (between `<!--` and `-->`)
153- // rest starts with "!--", so content starts at index 3
154- let content = & rest[ 3 ..end_offset] ;
153+ let content = & after_open[ ..content_len] ;
155154 let value = oxc_span:: Atom :: from ( content) ;
156155
157156 // Create the AstroComment node
@@ -438,7 +437,8 @@ impl<'a, C: ParserConfig> ParserImpl<'a, C> {
438437mod test {
439438 use oxc_allocator:: Allocator ;
440439 use oxc_ast:: ast:: {
441- JSXAttributeItem , JSXAttributeName , JSXChild , JSXElementName , JSXExpression , Statement ,
440+ Expression , JSXAttributeItem , JSXAttributeName , JSXChild , JSXElementName , JSXExpression ,
441+ Statement ,
442442 } ;
443443 use oxc_span:: SourceType ;
444444
@@ -5014,4 +5014,122 @@ console.log(msg);
50145014 main. children . iter ( ) . filter ( |c| matches ! ( c, JSXChild :: Element ( _) ) ) . count ( ) ;
50155015 assert_eq ! ( element_children, 2 , "expected <div> and <p> as children of <main>" ) ;
50165016 }
5017+
5018+ #[ test]
5019+ fn parse_astro_second_script_in_expression_is_raw_text ( ) {
5020+ // A non-first `<script>` sibling in a `{ ... }` expression must still parse
5021+ // its body as raw text, or a close tag in a template literal becomes a
5022+ // stray closing-tag error.
5023+ let allocator = Allocator :: default ( ) ;
5024+ let source_type = SourceType :: astro ( ) ;
5025+ let source = "{enabled && (\n <script src=\" x.js\" ></script>\n <script>var a = `</article>`;</script>\n )}" ;
5026+ let ret = Parser :: new ( & allocator, source, source_type) . parse_astro ( ) ;
5027+ assert ! ( !ret. panicked) ;
5028+ assert ! ( ret. errors. is_empty( ) , "errors: {:?}" , ret. errors) ;
5029+ }
5030+
5031+ #[ test]
5032+ fn parse_astro_bare_siblings_in_expression_keep_whitespace ( ) {
5033+ // Whitespace between bare JSX siblings in a `{ ... }` expression must
5034+ // survive as a JSXText child (the JS lexer skips it otherwise).
5035+ let allocator = Allocator :: default ( ) ;
5036+ let source_type = SourceType :: astro ( ) ;
5037+ let source = "{x && (<em>a</em>\n <span>b</span>)}" ;
5038+ let ret = Parser :: new ( & allocator, source, source_type) . parse_astro ( ) ;
5039+ assert ! ( ret. errors. is_empty( ) , "errors: {:?}" , ret. errors) ;
5040+
5041+ let JSXChild :: ExpressionContainer ( container) = & ret. root . body [ 0 ] else {
5042+ panic ! ( "expected expression container" ) ;
5043+ } ;
5044+ let Some ( Expression :: LogicalExpression ( logical) ) = container. expression . as_expression ( )
5045+ else {
5046+ panic ! ( "expected `&&` logical expression" ) ;
5047+ } ;
5048+ let right = match & logical. right {
5049+ Expression :: ParenthesizedExpression ( paren) => & paren. expression ,
5050+ other => other,
5051+ } ;
5052+ let Expression :: JSXFragment ( fragment) = right else {
5053+ panic ! ( "expected implicit fragment for bare siblings, got {right:?}" ) ;
5054+ } ;
5055+ let text_children = fragment
5056+ . children
5057+ . iter ( )
5058+ . filter ( |c| matches ! ( c, JSXChild :: Text ( _) ) )
5059+ . count ( ) ;
5060+ assert_eq ! ( text_children, 1 , "inter-sibling whitespace should be a JSXText node" ) ;
5061+ }
5062+
5063+ #[ test]
5064+ fn parse_astro_overlapping_comment_marker_does_not_panic ( ) {
5065+ // `<!-->` overlaps the `-->` with the `<!--` open; the comment parsers must
5066+ // look for the close at offset >= 3, not slice `rest[3..1]` and panic.
5067+ let allocator = Allocator :: default ( ) ;
5068+ let source_type = SourceType :: astro ( ) ;
5069+ for source in [ "{<a/><!-->}" , "<!-->" , "<div><!--></div>" , "{x && (<a/><!-->)}" ] {
5070+ // The contract is "no panic"; some of these are still errors.
5071+ let _ = Parser :: new ( & allocator, source, source_type) . parse_astro ( ) ;
5072+ }
5073+ }
5074+
5075+ #[ test]
5076+ fn parse_astro_comment_between_bare_siblings ( ) {
5077+ // An HTML comment in a run of bare JSX siblings is kept as an AstroComment
5078+ // child (matching Go), including the common case of the comment on its own
5079+ // line, which only works because the lexer no longer line-comments `<!--`.
5080+ let allocator = Allocator :: default ( ) ;
5081+ let source_type = SourceType :: astro ( ) ;
5082+ for source in [
5083+ "{x && (<a/><!--c--><b/>)}" ,
5084+ "{x && (\n <a/>\n <!-- c -->\n <b/>\n )}" ,
5085+ "{x && (<a/><!--c-->)}" ,
5086+ ] {
5087+ let ret = Parser :: new ( & allocator, source, source_type) . parse_astro ( ) ;
5088+ assert ! ( ret. errors. is_empty( ) , "{source:?} errors: {:?}" , ret. errors) ;
5089+
5090+ let JSXChild :: ExpressionContainer ( container) = & ret. root . body [ 0 ] else {
5091+ panic ! ( "expected expression container for {source:?}" ) ;
5092+ } ;
5093+ let Some ( Expression :: LogicalExpression ( logical) ) =
5094+ container. expression . as_expression ( )
5095+ else {
5096+ panic ! ( "expected `&&` logical expression for {source:?}" ) ;
5097+ } ;
5098+ let right = match & logical. right {
5099+ Expression :: ParenthesizedExpression ( paren) => & paren. expression ,
5100+ other => other,
5101+ } ;
5102+ let Expression :: JSXFragment ( fragment) = right else {
5103+ panic ! ( "expected implicit fragment for {source:?}, got {right:?}" ) ;
5104+ } ;
5105+ let comments =
5106+ fragment. children . iter ( ) . filter ( |c| matches ! ( c, JSXChild :: AstroComment ( _) ) ) . count ( ) ;
5107+ assert_eq ! ( comments, 1 , "{source:?} should keep the comment as a child" ) ;
5108+ }
5109+ }
5110+
5111+ #[ test]
5112+ fn parse_astro_jsx_lt_comparison_still_parses ( ) {
5113+ // The comment heuristic only diverts when the left side is a JSX element, so
5114+ // an ordinary JS `< !--` (less-than, negated pre-decrement) is untouched even
5115+ // when a `-->` appears later. Must not be misread as a comment.
5116+ let allocator = Allocator :: default ( ) ;
5117+ let source_type = SourceType :: astro ( ) ;
5118+ for source in [ "{(3 < !--n, m-->q)}" , "{x && y < !--count}" ] {
5119+ let ret = Parser :: new ( & allocator, source, source_type) . parse_astro ( ) ;
5120+ assert ! ( ret. errors. is_empty( ) , "{source:?} should parse: {:?}" , ret. errors) ;
5121+ }
5122+ }
5123+
5124+ #[ test]
5125+ fn parse_astro_shorthand_attribute_fatal_expr_does_not_panic ( ) {
5126+ // Guards the inverted name-span panic when a shorthand-attribute `{…}`
5127+ // expression fails to parse without advancing the lexer.
5128+ let allocator = Allocator :: default ( ) ;
5129+ let source_type = SourceType :: astro ( ) ;
5130+ for source in [ "<div><div{\n )</div>" , "{x && (<a/>\n <div{\n )}" , "<Comp {(\n } />" ] {
5131+ // The contract is "no panic"; these are still errors.
5132+ let _ = Parser :: new ( & allocator, source, source_type) . parse_astro ( ) ;
5133+ }
5134+ }
50175135}
0 commit comments