@@ -22,6 +22,7 @@ use super::compilation::{
2222 CTX_REF , ComponentCompilationJob , DeferBlockDepsEmitMode , DeferMetadata ,
2323 HostBindingCompilationJob , I18nMessageMetadata , TemplateCompilationMode ,
2424} ;
25+ use super :: conversion:: prefix_with_namespace;
2526use crate :: ast:: expression:: { AngularExpression , ParsedEventType } ;
2627use crate :: ast:: r3:: {
2728 I18nIcuPlaceholder , I18nMeta , I18nNode , R3BoundAttribute , R3BoundEvent , R3BoundText , R3Content ,
@@ -1800,14 +1801,35 @@ fn ingest_template<'a>(
18001801 // view's xref.
18011802 let xref = job. allocate_view ( Some ( view_xref) ) ;
18021803
1803- // Compute fn_name_suffix from tag name, matching TypeScript's behavior.
1804- // TypeScript: functionNameSuffix = tagNameWithoutNamespace === null ? '' : prefixWithNamespace(tagName, namespace)
1805- // prefixWithNamespace converts hyphens to underscores (e.g., "ng-template" → "ng_template")
1806- let fn_name_suffix = tag_name. as_ref ( ) . map ( |tag| {
1807- let suffix = tag. as_str ( ) . replace ( '-' , "_" ) ;
1804+ // Parse namespace from tag name (e.g., `:svg:path` → ("svg", "path")).
1805+ // Matches TypeScript's ingestTemplate (lines 351-358 in ingest.ts):
1806+ // let tagNameWithoutNamespace = tmpl.tagName;
1807+ // if (tmpl.tagName) {
1808+ // [namespacePrefix, tagNameWithoutNamespace] = splitNsName(tmpl.tagName);
1809+ // }
1810+ // const namespace = namespaceForKey(namespacePrefix);
1811+ let ( namespace_key, tag_name_without_namespace) =
1812+ tag_name. as_ref ( ) . map_or ( ( None , None ) , |tag| {
1813+ let ( ns, stripped) = split_ns_name ( tag. as_str ( ) ) ;
1814+ ( ns, Some ( stripped) )
1815+ } ) ;
1816+ let namespace = namespace_for_key ( namespace_key) ;
1817+
1818+ // Compute fn_name_suffix from stripped tag name, matching TypeScript's behavior.
1819+ // TypeScript (lines 359-360):
1820+ // const functionNameSuffix = tagNameWithoutNamespace === null
1821+ // ? '' : prefixWithNamespace(tagNameWithoutNamespace, namespace);
1822+ // prefixWithNamespace returns `:svg:tagName` for SVG, `:math:tagName` for Math, or just
1823+ // `tagName` for HTML. The sanitizeIdentifier function later replaces non-word chars with `_`.
1824+ let fn_name_suffix = tag_name_without_namespace. map ( |stripped_tag| {
1825+ let suffix = prefix_with_namespace ( stripped_tag, namespace) ;
18081826 Atom :: from ( allocator. alloc_str ( & suffix) )
18091827 } ) ;
18101828
1829+ // Build the tag atom from the stripped tag name (without namespace prefix).
1830+ // TypeScript passes `tagNameWithoutNamespace` to createTemplateOp (line 367).
1831+ let tag = tag_name_without_namespace. map ( |s| Atom :: from ( allocator. alloc_str ( s) ) ) ;
1832+
18111833 // Convert references to local refs - needed for template op creation
18121834 let local_refs = ingest_references_owned ( allocator, references) ;
18131835
@@ -1819,8 +1841,8 @@ fn ingest_template<'a>(
18191841 xref,
18201842 embedded_view : xref,
18211843 slot : None ,
1822- tag : tag_name . clone ( ) , // Use tag from template for content projection
1823- namespace : Namespace :: Html ,
1844+ tag,
1845+ namespace,
18241846 template_kind,
18251847 fn_name_suffix,
18261848 block : None ,
@@ -2067,29 +2089,15 @@ fn ingest_template<'a>(
20672089 xref : i18n_xref,
20682090 } ;
20692091
2070- // Insert the ops into the child view's create list
2071- // TypeScript uses OpList.insertAfter(head) and OpList.insertBefore(tail)
2092+ // Insert the ops into the child view's create list.
2093+ // TypeScript uses OpList.insertAfter(head) and OpList.insertBefore(tail),
2094+ // but Angular's OpList has sentinel nodes at head/tail, so insertAfter(head)
2095+ // means "insert as first real element" and insertBefore(tail) means "insert
2096+ // as last real element". Our OpList doesn't have sentinels, so we use
2097+ // push_front for I18nStart (first element) and push for I18nEnd (last element).
20722098 if let Some ( view) = job. view_mut ( xref) {
2073- // Get the head and tail pointers for insertion
2074- if let Some ( head_ptr) = view. create . head_ptr ( ) {
2075- // SAFETY: head_ptr is a valid pointer from the list
2076- unsafe {
2077- view. create . insert_after ( head_ptr, CreateOp :: I18nStart ( i18n_start) ) ;
2078- }
2079- } else {
2080- // List is empty, just push
2081- view. create . push ( CreateOp :: I18nStart ( i18n_start) ) ;
2082- }
2083-
2084- if let Some ( tail_ptr) = view. create . tail_ptr ( ) {
2085- // SAFETY: tail_ptr is a valid pointer from the list
2086- unsafe {
2087- view. create . insert_before ( tail_ptr, CreateOp :: I18nEnd ( i18n_end) ) ;
2088- }
2089- } else {
2090- // List is empty (shouldn't happen after push), just push
2091- view. create . push ( CreateOp :: I18nEnd ( i18n_end) ) ;
2092- }
2099+ view. create . push_front ( CreateOp :: I18nStart ( i18n_start) ) ;
2100+ view. create . push ( CreateOp :: I18nEnd ( i18n_end) ) ;
20932101 }
20942102 }
20952103}
@@ -2455,17 +2463,52 @@ fn ingest_for_block<'a>(
24552463 }
24562464
24572465 // Handle @empty block if present
2458- let ( empty_view, empty_tag) = if let Some ( empty) = for_block. empty {
2466+ let ( empty_view, empty_tag, empty_i18n_placeholder ) = if let Some ( empty) = for_block. empty {
24592467 let empty_xref = job. allocate_view ( Some ( view_xref) ) ;
24602468 // Infer tag name from single root element for content projection (@empty)
24612469 let empty_tag =
24622470 ingest_control_flow_insertion_point ( job, view_xref, empty_xref, & empty. children ) ;
2471+
2472+ // Extract i18n placeholder from @empty block if present.
2473+ // Per Angular's ingest.ts lines 970-974, only BlockPlaceholder is valid for @empty.
2474+ let empty_i18n_placeholder = match empty. i18n {
2475+ Some ( I18nMeta :: BlockPlaceholder ( ref placeholder) ) => Some ( I18nPlaceholder :: new (
2476+ placeholder. start_name . clone ( ) ,
2477+ Some ( placeholder. close_name . clone ( ) ) ,
2478+ ) ) ,
2479+ Some ( _) => {
2480+ job. diagnostics . push (
2481+ OxcDiagnostic :: error ( "Unhandled i18n metadata type for @empty" )
2482+ . with_label ( empty. source_span ) ,
2483+ ) ;
2484+ None
2485+ }
2486+ None => None ,
2487+ } ;
2488+
24632489 for child in empty. children {
24642490 ingest_node ( job, empty_xref, child) ;
24652491 }
2466- ( Some ( empty_xref) , empty_tag)
2492+ ( Some ( empty_xref) , empty_tag, empty_i18n_placeholder )
24672493 } else {
2468- ( None , None )
2494+ ( None , None , None )
2495+ } ;
2496+
2497+ // Extract i18n placeholder from @for block if present.
2498+ // Per Angular's ingest.ts lines 967-969, only BlockPlaceholder is valid for @for.
2499+ let i18n_placeholder = match for_block. i18n {
2500+ Some ( I18nMeta :: BlockPlaceholder ( ref placeholder) ) => Some ( I18nPlaceholder :: new (
2501+ placeholder. start_name . clone ( ) ,
2502+ Some ( placeholder. close_name . clone ( ) ) ,
2503+ ) ) ,
2504+ Some ( _) => {
2505+ job. diagnostics . push (
2506+ OxcDiagnostic :: error ( "Unhandled i18n metadata type for @for" )
2507+ . with_label ( for_block. source_span ) ,
2508+ ) ;
2509+ None
2510+ }
2511+ None => None ,
24692512 } ;
24702513
24712514 // Convert the track expression from the for block.
@@ -2492,8 +2535,8 @@ fn ingest_for_block<'a>(
24922535 attributes : None ,
24932536 empty_tag,
24942537 empty_attributes : None ,
2495- i18n_placeholder : None ,
2496- empty_i18n_placeholder : None ,
2538+ i18n_placeholder,
2539+ empty_i18n_placeholder,
24972540 } ) ;
24982541
24992542 if let Some ( view) = job. view_mut ( view_xref) {
0 commit comments