@@ -234,6 +234,9 @@ struct TemplateSectionMeta {
234234 /// These are resolved into `text_runs` during finalization.
235235 /// Each entry is `(path, raw)` where raw indicates triple-brace `{{{...}}}`.
236236 text_bindings : Vec < ( String , bool ) > ,
237+ /// Byte offsets in `html` where compiler-generated text markers start.
238+ /// Authored CSS may contain marker-like text; only these offsets are metadata.
239+ text_marker_offsets : Vec < usize > ,
237240 /// Client text-run metadata: `(slot, parts, raw)`.
238241 /// `raw` is true when the binding uses triple-brace `{{{...}}}` syntax.
239242 text_runs : Vec < ( SlotLocator , Vec < CompiledAttrPart > , bool ) > ,
@@ -856,6 +859,7 @@ fn compile_section(input: &str, blocks: &mut Vec<TemplateSectionMeta>) -> Templa
856859 let mut meta = TemplateSectionMeta {
857860 html : String :: with_capacity ( input. len ( ) ) ,
858861 text_bindings : Vec :: new ( ) ,
862+ text_marker_offsets : Vec :: new ( ) ,
859863 text_runs : Vec :: new ( ) ,
860864 attr_bindings : Vec :: new ( ) ,
861865 attr_groups : Vec :: new ( ) ,
@@ -988,7 +992,7 @@ fn compile_text_binding_at(
988992 let expr = input[ index + 3 ..end] . trim ( ) ;
989993 let idx = meta. text_bindings . len ( ) ;
990994 meta. text_bindings . push ( ( expr. to_string ( ) , true ) ) ;
991- let _ = write ! ( meta. html , "<!--t:{ idx}-->" ) ;
995+ emit_text_marker ( meta, idx) ;
992996 return Some ( end + 3 ) ;
993997 }
994998 }
@@ -998,14 +1002,21 @@ fn compile_text_binding_at(
9981002 let expr = input[ index + 2 ..end] . trim ( ) ;
9991003 let idx = meta. text_bindings . len ( ) ;
10001004 meta. text_bindings . push ( ( expr. to_string ( ) , false ) ) ;
1001- let _ = write ! ( meta. html , "<!--t:{ idx}-->" ) ;
1005+ emit_text_marker ( meta, idx) ;
10021006 return Some ( end + 2 ) ;
10031007 }
10041008 }
10051009
10061010 None
10071011}
10081012
1013+ fn emit_text_marker ( meta : & mut TemplateSectionMeta , index : usize ) {
1014+ meta. text_marker_offsets . push ( meta. html . len ( ) ) ;
1015+ meta. html . push_str ( "<!--t:" ) ;
1016+ let _ = write ! ( meta. html, "{index}" ) ;
1017+ meta. html . push_str ( "-->" ) ;
1018+ }
1019+
10091020fn compile_style_content ( input : & str , meta : & mut TemplateSectionMeta ) {
10101021 let bytes = input. as_bytes ( ) ;
10111022 let len = bytes. len ( ) ;
@@ -1055,7 +1066,7 @@ fn compile_css_signal_comment(comment: &str, meta: &mut TemplateSectionMeta) ->
10551066 if let Some ( signal) = comment_policy:: parse_css_signal_comment ( comment) {
10561067 let idx = meta. text_bindings . len ( ) ;
10571068 meta. text_bindings . push ( ( signal. path , signal. raw ) ) ;
1058- let _ = write ! ( meta. html , "<!--t:{ idx}-->" ) ;
1069+ emit_text_marker ( meta, idx) ;
10591070 return true ;
10601071 }
10611072
@@ -1116,6 +1127,7 @@ fn ascii_starts_with_ignore_case(input: &[u8], prefix: &[u8]) -> bool {
11161127enum FragmentNode {
11171128 Element ( FragmentElement ) ,
11181129 Text ( String ) ,
1130+ TextMarker ( usize ) ,
11191131 Comment ( String ) ,
11201132}
11211133
@@ -1135,7 +1147,8 @@ struct FragmentAttr {
11351147
11361148fn finalize_template_section ( meta : & mut TemplateSectionMeta ) {
11371149 let raw_html = std:: mem:: take ( & mut meta. html ) ;
1138- let nodes = parse_fragment_nodes ( & raw_html) ;
1150+ let text_marker_offsets = std:: mem:: take ( & mut meta. text_marker_offsets ) ;
1151+ let nodes = parse_fragment_nodes ( & raw_html, & text_marker_offsets) ;
11391152 let text_bindings = meta. text_bindings . clone ( ) ;
11401153 let mut finalized_html = String :: with_capacity ( raw_html. len ( ) ) ;
11411154 let mut text_runs = Vec :: new ( ) ;
@@ -1233,6 +1246,7 @@ fn process_fragment_children(
12331246 }
12341247
12351248 match & nodes[ index] {
1249+ FragmentNode :: TextMarker ( _) => { }
12361250 FragmentNode :: Comment ( data) => {
12371251 if let Some ( idx) = parse_marker_index ( data, "c:" ) {
12381252 if let Some ( slot) = condition_slots. get_mut ( idx) {
@@ -1377,14 +1391,13 @@ fn collect_text_run(
13771391 }
13781392 consumed += 1 ;
13791393 }
1380- FragmentNode :: Comment ( data) => {
1381- if let Some ( index) = parse_marker_index ( data, "t:" ) {
1382- if let Some ( ( path, raw) ) = text_bindings. get ( index) {
1383- parts. push ( CompiledAttrPart :: Dynamic ( path. clone ( ) ) ) ;
1384- has_dynamic = true ;
1385- if * raw {
1386- is_raw = true ;
1387- }
1394+ FragmentNode :: Comment ( _) => break ,
1395+ FragmentNode :: TextMarker ( index) => {
1396+ if let Some ( ( path, raw) ) = text_bindings. get ( * index) {
1397+ parts. push ( CompiledAttrPart :: Dynamic ( path. clone ( ) ) ) ;
1398+ has_dynamic = true ;
1399+ if * raw {
1400+ is_raw = true ;
13881401 }
13891402 consumed += 1 ;
13901403 } else {
@@ -1442,7 +1455,7 @@ fn emit_html_attr_value(value: &str, out: &mut String) {
14421455 }
14431456}
14441457
1445- fn parse_fragment_nodes ( input : & str ) -> Vec < FragmentNode > {
1458+ fn parse_fragment_nodes ( input : & str , text_marker_offsets : & [ usize ] ) -> Vec < FragmentNode > {
14461459 let mut roots = Vec :: new ( ) ;
14471460 let mut stack: Vec < FragmentElement > = Vec :: new ( ) ;
14481461 let bytes = input. as_bytes ( ) ;
@@ -1451,6 +1464,18 @@ fn parse_fragment_nodes(input: &str) -> Vec<FragmentNode> {
14511464
14521465 while index < len {
14531466 let remaining = & input[ index..] ;
1467+ if let Some ( ( marker_index, marker_end) ) =
1468+ find_text_marker_comment ( input, index, 0 , text_marker_offsets)
1469+ {
1470+ push_fragment_node (
1471+ & mut roots,
1472+ & mut stack,
1473+ FragmentNode :: TextMarker ( marker_index) ,
1474+ ) ;
1475+ index = marker_end;
1476+ continue ;
1477+ }
1478+
14541479 if remaining. starts_with ( "<!--" ) {
14551480 if let Some ( close) = remaining. find ( "-->" ) {
14561481 push_fragment_node (
@@ -1474,9 +1499,33 @@ fn parse_fragment_nodes(input: &str) -> Vec<FragmentNode> {
14741499 }
14751500
14761501 if remaining. starts_with ( '<' ) {
1477- if let Some ( ( element, consumed) ) = parse_fragment_start_tag ( remaining) {
1502+ if let Some ( ( mut element, consumed) ) = parse_fragment_start_tag ( remaining) {
14781503 if element. self_closing {
14791504 push_fragment_node ( & mut roots, & mut stack, FragmentNode :: Element ( element) ) ;
1505+ } else if element. tag_name . eq_ignore_ascii_case ( "style" ) {
1506+ let content_start = index + consumed;
1507+ if let Some ( ( close_start, close_end) ) = find_style_end_tag ( input, content_start)
1508+ {
1509+ push_style_raw_text_nodes (
1510+ & mut element. children ,
1511+ & input[ content_start..close_start] ,
1512+ content_start,
1513+ text_marker_offsets,
1514+ ) ;
1515+ push_fragment_node ( & mut roots, & mut stack, FragmentNode :: Element ( element) ) ;
1516+ index = close_end;
1517+ continue ;
1518+ }
1519+
1520+ push_style_raw_text_nodes (
1521+ & mut element. children ,
1522+ & input[ content_start..] ,
1523+ content_start,
1524+ text_marker_offsets,
1525+ ) ;
1526+ push_fragment_node ( & mut roots, & mut stack, FragmentNode :: Element ( element) ) ;
1527+ index = len;
1528+ continue ;
14801529 } else {
14811530 stack. push ( element) ;
14821531 }
@@ -1500,6 +1549,70 @@ fn parse_fragment_nodes(input: &str) -> Vec<FragmentNode> {
15001549 roots
15011550}
15021551
1552+ fn push_style_raw_text_nodes (
1553+ children : & mut Vec < FragmentNode > ,
1554+ input : & str ,
1555+ base_offset : usize ,
1556+ text_marker_offsets : & [ usize ] ,
1557+ ) {
1558+ let bytes = input. as_bytes ( ) ;
1559+ let len = bytes. len ( ) ;
1560+ let mut index = 0usize ;
1561+ let mut text_start = 0usize ;
1562+
1563+ while index + 10 <= len {
1564+ if let Some ( ( marker_index, marker_end) ) =
1565+ find_text_marker_comment ( input, index, base_offset, text_marker_offsets)
1566+ {
1567+ if text_start < index {
1568+ children. push ( FragmentNode :: Text ( input[ text_start..index] . to_string ( ) ) ) ;
1569+ }
1570+ children. push ( FragmentNode :: TextMarker ( marker_index) ) ;
1571+ index = marker_end;
1572+ text_start = marker_end;
1573+ continue ;
1574+ }
1575+ index += 1 ;
1576+ }
1577+
1578+ if text_start < len {
1579+ children. push ( FragmentNode :: Text ( input[ text_start..] . to_string ( ) ) ) ;
1580+ }
1581+ }
1582+
1583+ fn find_text_marker_comment (
1584+ input : & str ,
1585+ index : usize ,
1586+ base_offset : usize ,
1587+ text_marker_offsets : & [ usize ] ,
1588+ ) -> Option < ( usize , usize ) > {
1589+ let bytes = input. as_bytes ( ) ;
1590+ if bytes. get ( index..index + 6 ) != Some ( b"<!--t:" ) {
1591+ return None ;
1592+ }
1593+ if text_marker_offsets
1594+ . binary_search ( & ( base_offset + index) )
1595+ . is_err ( )
1596+ {
1597+ return None ;
1598+ }
1599+
1600+ let mut cursor = index + 6 ;
1601+ let digit_start = cursor;
1602+ let mut marker_index = 0usize ;
1603+ while cursor < bytes. len ( ) && bytes[ cursor] . is_ascii_digit ( ) {
1604+ marker_index = marker_index
1605+ . checked_mul ( 10 ) ?
1606+ . checked_add ( ( bytes[ cursor] - b'0' ) as usize ) ?;
1607+ cursor += 1 ;
1608+ }
1609+ if cursor == digit_start || bytes. get ( cursor..cursor + 3 ) != Some ( b"-->" ) {
1610+ return None ;
1611+ }
1612+
1613+ Some ( ( marker_index, cursor + 3 ) )
1614+ }
1615+
15031616fn push_fragment_node (
15041617 roots : & mut Vec < FragmentNode > ,
15051618 stack : & mut [ FragmentElement ] ,
@@ -2512,6 +2625,47 @@ mod tests {
25122625 assert ! ( !result. contains( "*/" ) ) ;
25132626 }
25142627
2628+ #[ test]
2629+ fn test_metadata_keeps_legal_style_comment_with_html_like_tag_as_raw_text ( ) {
2630+ let result = generate_compiled_template (
2631+ "my-component" ,
2632+ r#"<style>:host { display: block; }/*! @license The <my-component> element. */.container { padding: 16px; }</style><div>hello</div>"# ,
2633+ ) ;
2634+
2635+ assert_no_client_markers ( & result) ;
2636+ assert ! ( result. contains( "@license The <my-component> element." ) ) ;
2637+ assert ! ( !result. contains( "</my-component>" ) ) ;
2638+ assert ! ( result. contains( "</style><div>hello</div>" ) ) ;
2639+ }
2640+
2641+ #[ test]
2642+ fn test_metadata_keeps_marker_like_text_in_legal_style_comment_literal ( ) {
2643+ let result = generate_compiled_template (
2644+ "my-component" ,
2645+ r#"<p>{{title}}</p><style>/*! @license <!--t:0--> */.x { color: red; }</style>"# ,
2646+ ) ;
2647+
2648+ assert ! ( result. contains( "<!--t:0-->" ) ) ;
2649+ assert ! ( result
2650+ . contains( r#"h:"<p></p><style>/*! @license <!--t:0--> */.x { color: red; }</style>""# ) ) ;
2651+ assert ! ( result. contains( r#",tx:[[[[0],0],[["title"]]]]"# ) ) ;
2652+ assert ! ( !result. contains( r#"[[[1],0],[["title"]]]"# ) ) ;
2653+ }
2654+
2655+ #[ test]
2656+ fn test_metadata_keeps_marker_like_style_text_between_real_signal_markers ( ) {
2657+ let result = generate_compiled_template (
2658+ "my-component" ,
2659+ r#"<style>/*{{first}}*/📚/*! @license <!--t:0--> <my-component> *//*{{{second}}}*/</style><div>done</div>"# ,
2660+ ) ;
2661+
2662+ assert ! ( result. contains( r#"["first"]"# ) ) ;
2663+ assert ! ( result. contains( r#"["second"]"# ) ) ;
2664+ assert ! ( result. contains( "📚/*! @license <!--t:0--> <my-component> */" ) ) ;
2665+ assert ! ( !result. contains( "</my-component>" ) ) ;
2666+ assert ! ( result. contains( "</style><div>done</div>" ) ) ;
2667+ }
2668+
25152669 #[ test]
25162670 fn test_metadata_strips_style_line_comments_without_processing_bindings ( ) {
25172671 let result = generate_compiled_template (
0 commit comments