@@ -2122,41 +2122,48 @@ fn try_anchor_supplement(
21222122 // Find the nearest anchor with inter_col <= token_col
21232123 // (the last one in sorted order that doesn't exceed it).
21242124 let nearest = anchors. iter ( ) . rev ( ) . find ( |& & ( ic, _, _) | ic <= token_col) ;
2125- let & ( anchor_ic, anchor_fl, anchor_fc) = nearest?;
2126- let delta = i64:: from ( anchor_fc) - i64:: from ( anchor_ic) ;
2127- let candidate_col = u32:: try_from ( ( i64:: from ( token_col) + delta) . max ( 0 ) )
2128- . expect ( "candidate col exceeds u32" ) ;
2129-
2130- // Verify: the text at the candidate final position must match the
2131- // intermediate text at the token position. This confirms we are in
2132- // a quasi region (not a reformatted expression).
2133- let i_text = ctx. inter_lines . get ( gen_line) . copied ( ) . unwrap_or ( "" ) ;
2134- let f_text = ctx
2135- . final_lines
2136- . get ( anchor_fl as usize )
2137- . copied ( )
2138- . unwrap_or ( "" ) ;
2139- let tc = token_col as usize ;
2140- let cc = candidate_col as usize ;
2141- // Use a short verification window; 2 bytes is enough to confirm
2142- // we're at the same quasi text (e.g. "<p", "</", etc.).
2143- let verify_len = 2 ;
2144- let text_matches = tc + verify_len <= i_text. len ( )
2145- && cc + verify_len <= f_text. len ( )
2146- && i_text. as_bytes ( ) [ tc..tc + verify_len] == f_text. as_bytes ( ) [ cc..cc + verify_len] ;
21472125
2148- if text_matches {
2149- Some ( ( anchor_fl, candidate_col) )
2150- } else {
2151- None
2126+ if let Some ( & ( anchor_ic, anchor_fl, anchor_fc) ) = nearest {
2127+ let delta = i64:: from ( anchor_fc) - i64:: from ( anchor_ic) ;
2128+ let candidate_col = u32:: try_from ( ( i64:: from ( token_col) + delta) . max ( 0 ) )
2129+ . expect ( "candidate col exceeds u32" ) ;
2130+
2131+ // Verify: the text at the candidate final position must match the
2132+ // intermediate text at the token position. This confirms we are in
2133+ // a quasi region (not a reformatted expression).
2134+ let i_text = ctx. inter_lines . get ( gen_line) . copied ( ) . unwrap_or ( "" ) ;
2135+ let f_text = ctx
2136+ . final_lines
2137+ . get ( anchor_fl as usize )
2138+ . copied ( )
2139+ . unwrap_or ( "" ) ;
2140+ let tc = token_col as usize ;
2141+ let cc = candidate_col as usize ;
2142+ // Use a short verification window; 2 bytes is enough to confirm
2143+ // we're at the same quasi text (e.g. "<p", "</", etc.).
2144+ let verify_len = 2 ;
2145+ let text_matches = tc + verify_len <= i_text. len ( )
2146+ && cc + verify_len <= f_text. len ( )
2147+ && i_text. as_bytes ( ) [ tc..tc + verify_len] == f_text. as_bytes ( ) [ cc..cc + verify_len] ;
2148+
2149+ if text_matches {
2150+ return Some ( ( anchor_fl, candidate_col) ) ;
2151+ }
21522152 }
2153- } else {
2154- // No Phase-2 anchors for this intermediate line.
2153+ // No anchor at or before token_col (or text mismatch): the token is in
2154+ // a quasi text region that starts before all Phase-2 anchors on this
2155+ // line (e.g. `<span>` at col 0 when the JS suffix starts at col 25).
2156+ // Fall through to the window search below.
2157+ }
2158+
2159+ {
2160+ // Either: no Phase-2 anchors exist for this intermediate line (pure
2161+ // quasi text), or all anchors are after the token's column (token is
2162+ // in a quasi region before the first interpolation on this line).
21552163 //
2156- // This happens for lines that are pure template literal quasi text
2157- // (no JS expressions). Phase-2 reformatting may shift these lines
2158- // by inserting extra lines for object literal properties, but the
2159- // text content remains identical.
2164+ // Phase-2 reformatting may shift these lines by inserting extra lines
2165+ // for object literal properties, but the text content remains
2166+ // identical in the quasi regions.
21602167 //
21612168 // Search a window of final lines around the expected position for
21622169 // a line containing the same text at the token's column.
@@ -4058,22 +4065,20 @@ const html = '<b>bold</b>';
40584065 }
40594066
40604067 #[ test]
4061- fn test_set_html_template_literal_no_unescape_html ( ) {
4062- // `set:html={`template ${literal}`}` must NOT wrap with $$unescapeHTML.
4063- // The Go compiler passes template literals through as-is — matching Go behavior.
4068+ fn test_set_html_template_literal_uses_unescape_html ( ) {
4069+ // `set:html={`template ${literal}`}` must wrap with $$unescapeHTML.
4070+ // The $$render tagged template escapes HTML by default, so all set:html
4071+ // expressions — including template literals — need $$unescapeHTML to render
4072+ // as raw HTML.
40644073 let source = r#"---
40654074const name = 'world';
40664075---
40674076<div set:html={`Hello <b>${name}</b>`} />"# ;
40684077 let output = compile_astro ( source) ;
40694078
40704079 assert ! (
4071- !output. contains( "$$unescapeHTML(`" ) ,
4072- "set:html with template literal must NOT use $$unescapeHTML: {output}"
4073- ) ;
4074- assert ! (
4075- output. contains( "`Hello <b>${name}</b>`" ) ,
4076- "set:html template literal should be passed through as-is: {output}"
4080+ output. contains( "$$unescapeHTML(`Hello <b>${name}</b>`)" ) ,
4081+ "set:html with template literal must use $$unescapeHTML: {output}"
40774082 ) ;
40784083 }
40794084
@@ -4230,4 +4235,93 @@ import { CompA, CompB } from 'test';
42304235 "Mixed import must not be suppressed when one specifier is used normally: {output}"
42314236 ) ;
42324237 }
4238+
4239+ // -------------------------------------------------------------------------
4240+ // transition:persist tests
4241+ // -------------------------------------------------------------------------
4242+
4243+ #[ test]
4244+ fn test_transition_persist_with_explicit_value_on_html_element ( ) {
4245+ // transition:persist="form" on an HTML element should use the string value
4246+ // directly as data-astro-transition-persist, not a generated hash.
4247+ let source = r#"<form transition:persist="form"><input /></form>"# ;
4248+ let output = compile_astro ( source) ;
4249+
4250+ assert ! (
4251+ output. contains( r#"data-astro-transition-persist="form""# ) ,
4252+ "transition:persist with explicit value must use that value: {output}"
4253+ ) ;
4254+ // The output must not contain a $$createTransitionScope( call (only the import alias is ok)
4255+ assert ! (
4256+ !output. contains( "$$createTransitionScope(" ) ,
4257+ "transition:persist with explicit value must not call $$createTransitionScope: {output}"
4258+ ) ;
4259+ }
4260+
4261+ #[ test]
4262+ fn test_transition_persist_with_transition_name_on_html_element ( ) {
4263+ // transition:persist + transition:name="counter" — persist ID should be "counter"
4264+ let source = r#"<div transition:persist transition:name="counter"></div>"# ;
4265+ let output = compile_astro ( source) ;
4266+
4267+ assert ! (
4268+ output. contains( r#"data-astro-transition-persist="counter""# ) ,
4269+ "transition:persist must use transition:name value as persist ID: {output}"
4270+ ) ;
4271+ }
4272+
4273+ #[ test]
4274+ fn test_transition_persist_with_explicit_value_on_component ( ) {
4275+ // transition:persist="form" on a component should use the string value directly.
4276+ let source = r#"---
4277+ import MyComp from './MyComp.astro';
4278+ ---
4279+ <MyComp transition:persist="form" />"# ;
4280+ let output = compile_astro ( source) ;
4281+
4282+ assert ! (
4283+ output. contains( r#""data-astro-transition-persist": "form""# ) ,
4284+ "component transition:persist with explicit value must use that value: {output}"
4285+ ) ;
4286+ assert ! (
4287+ !output. contains( "$$createTransitionScope(" ) ,
4288+ "component transition:persist with explicit value must not call $$createTransitionScope: {output}"
4289+ ) ;
4290+ }
4291+
4292+ #[ test]
4293+ fn test_transition_persist_with_transition_name_on_component ( ) {
4294+ // transition:persist + transition:name="counter" on a component.
4295+ let source = r#"---
4296+ import MyComp from './MyComp.astro';
4297+ ---
4298+ <MyComp transition:persist transition:name="counter" />"# ;
4299+ let output = compile_astro ( source) ;
4300+
4301+ assert ! (
4302+ output. contains( r#""data-astro-transition-persist": "counter""# ) ,
4303+ "component transition:persist must use transition:name value: {output}"
4304+ ) ;
4305+ }
4306+
4307+ #[ test]
4308+ fn test_array_expression_in_jsx_transforms_children ( ) {
4309+ // JSX elements inside array expressions must be transformed.
4310+ // Previously they were left as raw JSX in output.
4311+ let source = r#"---
4312+ import Foo from './Foo.astro';
4313+ import Bar from './Bar.astro';
4314+ ---
4315+ <div>{[<Foo/>, <Bar/>]}</div>"# ;
4316+ let output = compile_astro ( source) ;
4317+
4318+ assert ! (
4319+ !output. contains( "<Foo/>" ) && !output. contains( "<Bar/>" ) ,
4320+ "JSX elements inside arrays must be transformed: {output}"
4321+ ) ;
4322+ assert ! (
4323+ output. contains( "$$renderComponent" ) ,
4324+ "JSX elements inside arrays must use $$renderComponent: {output}"
4325+ ) ;
4326+ }
42334327}
0 commit comments