@@ -4548,6 +4548,177 @@ export async function getStaticPaths() {
45484548 }
45494549 }
45504550
4551+ fn count_expression_containers ( children : & [ JSXChild < ' _ > ] ) -> usize {
4552+ children
4553+ . iter ( )
4554+ . map ( |child| match child {
4555+ JSXChild :: ExpressionContainer ( _) => 1 ,
4556+ JSXChild :: Element ( el) => count_expression_containers ( & el. children ) ,
4557+ JSXChild :: Fragment ( fragment) => count_expression_containers ( & fragment. children ) ,
4558+ _ => 0 ,
4559+ } )
4560+ . sum ( )
4561+ }
4562+
4563+ #[ test]
4564+ fn parse_astro_expression_after_math_closing_tag ( ) {
4565+ let source = "<math>{x}</math>{y}" ;
4566+ let allocator = Allocator :: default ( ) ;
4567+ let source_type = SourceType :: astro ( ) ;
4568+ let ret = Parser :: new ( & allocator, source, source_type) . parse_astro ( ) ;
4569+ assert ! ( !ret. panicked, "parser panicked: {:?}" , ret. errors) ;
4570+ assert ! ( ret. errors. is_empty( ) , "errors: {:?}" , ret. errors) ;
4571+
4572+ let Some ( JSXChild :: Element ( math) ) =
4573+ ret. root . body . iter ( ) . find ( |c| matches ! ( c, JSXChild :: Element ( _) ) )
4574+ else {
4575+ panic ! ( "should have a <math> element" ) ;
4576+ } ;
4577+ assert_eq ! (
4578+ count_expression_containers( & math. children) ,
4579+ 0 ,
4580+ "{{x}} inside <math> should be text"
4581+ ) ;
4582+ assert_eq ! (
4583+ count_expression_containers( & ret. root. body) ,
4584+ 1 ,
4585+ "{{y}} after </math> should be an expression"
4586+ ) ;
4587+ }
4588+
4589+ #[ test]
4590+ fn parse_astro_expression_after_nested_math_closing_tag ( ) {
4591+ let source = "<math><math>{a}</math>{b}</math>{c}" ;
4592+ let allocator = Allocator :: default ( ) ;
4593+ let source_type = SourceType :: astro ( ) ;
4594+ let ret = Parser :: new ( & allocator, source, source_type) . parse_astro ( ) ;
4595+ assert ! ( !ret. panicked, "parser panicked: {:?}" , ret. errors) ;
4596+ assert ! ( ret. errors. is_empty( ) , "errors: {:?}" , ret. errors) ;
4597+
4598+ let Some ( JSXChild :: Element ( math) ) =
4599+ ret. root . body . iter ( ) . find ( |c| matches ! ( c, JSXChild :: Element ( _) ) )
4600+ else {
4601+ panic ! ( "should have a <math> element" ) ;
4602+ } ;
4603+ assert_eq ! (
4604+ count_expression_containers( & math. children) ,
4605+ 0 ,
4606+ "braces inside nested <math> should be text"
4607+ ) ;
4608+ assert_eq ! (
4609+ count_expression_containers( & ret. root. body) ,
4610+ 1 ,
4611+ "{{c}} after the outer </math> should be an expression"
4612+ ) ;
4613+ }
4614+
4615+ #[ test]
4616+ fn parse_astro_expression_after_self_closing_math ( ) {
4617+ let source = "<math />{y}" ;
4618+ let allocator = Allocator :: default ( ) ;
4619+ let source_type = SourceType :: astro ( ) ;
4620+ let ret = Parser :: new ( & allocator, source, source_type) . parse_astro ( ) ;
4621+ assert ! ( !ret. panicked, "parser panicked: {:?}" , ret. errors) ;
4622+ assert ! ( ret. errors. is_empty( ) , "errors: {:?}" , ret. errors) ;
4623+ assert_eq ! (
4624+ count_expression_containers( & ret. root. body) ,
4625+ 1 ,
4626+ "{{y}} after <math /> should be an expression"
4627+ ) ;
4628+ }
4629+
4630+ #[ test]
4631+ fn parse_astro_unclosed_math_reports_error ( ) {
4632+ let source = "<math>{x}" ;
4633+ let allocator = Allocator :: default ( ) ;
4634+ let source_type = SourceType :: astro ( ) ;
4635+ let ret = Parser :: new ( & allocator, source, source_type) . parse_astro ( ) ;
4636+ assert ! ( !ret. errors. is_empty( ) , "unclosed <math> should report an error" ) ;
4637+ }
4638+
4639+ #[ test]
4640+ fn parse_astro_stray_closing_tag_inside_math ( ) {
4641+ let source = "<math>{x}</foo>{y}</math>{z}" ;
4642+ let allocator = Allocator :: default ( ) ;
4643+ let source_type = SourceType :: astro ( ) ;
4644+ let ret = Parser :: new ( & allocator, source, source_type) . parse_astro ( ) ;
4645+ assert_eq ! ( ret. errors. len( ) , 1 , "only the stray </foo> should error: {:?}" , ret. errors) ;
4646+
4647+ let Some ( JSXChild :: Element ( math) ) =
4648+ ret. root . body . iter ( ) . find ( |c| matches ! ( c, JSXChild :: Element ( _) ) )
4649+ else {
4650+ panic ! ( "should have a <math> element" ) ;
4651+ } ;
4652+ assert_eq ! (
4653+ count_expression_containers( & math. children) ,
4654+ 0 ,
4655+ "a stray closing tag should not end foreign content"
4656+ ) ;
4657+ assert_eq ! (
4658+ count_expression_containers( & ret. root. body) ,
4659+ 1 ,
4660+ "{{z}} after </math> should be an expression"
4661+ ) ;
4662+ }
4663+
4664+ #[ test]
4665+ fn parse_astro_math_inside_expression_container ( ) {
4666+ let source = "<div>{<math>{x}</math>}</div>" ;
4667+ let allocator = Allocator :: default ( ) ;
4668+ let source_type = SourceType :: astro ( ) ;
4669+ let ret = Parser :: new ( & allocator, source, source_type) . parse_astro ( ) ;
4670+ assert ! ( !ret. panicked, "parser panicked: {:?}" , ret. errors) ;
4671+ assert ! ( ret. errors. is_empty( ) , "errors: {:?}" , ret. errors) ;
4672+
4673+ let Some ( JSXChild :: Element ( div) ) =
4674+ ret. root . body . iter ( ) . find ( |c| matches ! ( c, JSXChild :: Element ( _) ) )
4675+ else {
4676+ panic ! ( "should have a <div> element" ) ;
4677+ } ;
4678+ let Some ( JSXChild :: ExpressionContainer ( container) ) = div. children . first ( ) else {
4679+ panic ! ( "<div> should have an expression container child" ) ;
4680+ } ;
4681+ let JSXExpression :: JSXElement ( math) = & container. expression else {
4682+ panic ! ( "expression should contain the <math> element" ) ;
4683+ } ;
4684+ assert_eq ! (
4685+ count_expression_containers( & math. children) ,
4686+ 0 ,
4687+ "{{x}} inside <math> should be text"
4688+ ) ;
4689+ }
4690+
4691+ #[ test]
4692+ fn parse_astro_expression_after_closing_tag_of_element_outside_math ( ) {
4693+ // `</div>` closes the unterminated <math> as well as the <div>
4694+ let source = "<div><math>{x}</div>{y}</div>" ;
4695+ let allocator = Allocator :: default ( ) ;
4696+ let source_type = SourceType :: astro ( ) ;
4697+ let ret = Parser :: new ( & allocator, source, source_type) . parse_astro ( ) ;
4698+ assert ! ( !ret. panicked, "parser panicked: {:?}" , ret. errors) ;
4699+ assert_eq ! ( ret. errors. len( ) , 1 , "only the mismatched tag should error: {:?}" , ret. errors) ;
4700+ assert_eq ! (
4701+ count_expression_containers( & ret. root. body) ,
4702+ 1 ,
4703+ "{{y}} after </div> should be an expression"
4704+ ) ;
4705+ }
4706+
4707+ #[ test]
4708+ fn parse_astro_expression_after_math_inside_element ( ) {
4709+ let source = "<div><math>{x}</math></div>{y}" ;
4710+ let allocator = Allocator :: default ( ) ;
4711+ let source_type = SourceType :: astro ( ) ;
4712+ let ret = Parser :: new ( & allocator, source, source_type) . parse_astro ( ) ;
4713+ assert ! ( !ret. panicked, "parser panicked: {:?}" , ret. errors) ;
4714+ assert ! ( ret. errors. is_empty( ) , "errors: {:?}" , ret. errors) ;
4715+ assert_eq ! (
4716+ count_expression_containers( & ret. root. body) ,
4717+ 1 ,
4718+ "{{y}} after </div> should be an expression"
4719+ ) ;
4720+ }
4721+
45514722 #[ test]
45524723 fn parse_astro_empty_attribute_expression ( ) {
45534724 // Empty expression in attribute value should be allowed in Astro (no TS17000 error)
0 commit comments