1515//! Ported from Angular's `template/pipeline/src/ingest.ts`.
1616
1717use oxc_allocator:: { Allocator , Box , Vec } ;
18+ use oxc_diagnostics:: OxcDiagnostic ;
1819use oxc_span:: Atom ;
1920
2021use super :: compilation:: {
@@ -23,9 +24,10 @@ use super::compilation::{
2324} ;
2425use crate :: ast:: expression:: AngularExpression ;
2526use crate :: ast:: r3:: {
26- I18nMeta , I18nNode , R3BoundAttribute , R3BoundEvent , R3BoundText , R3Content , R3DeferredBlock ,
27- R3Element , R3ForLoopBlock , R3IfBlock , R3LetDeclaration , R3Node , R3SwitchBlock , R3Template ,
28- R3Text , R3TextAttribute , R3Variable , SecurityContext ,
27+ I18nIcuPlaceholder , I18nMeta , I18nNode , R3BoundAttribute , R3BoundEvent , R3BoundText , R3Content ,
28+ R3DeferredBlock , R3Element , R3ForLoopBlock , R3Icu , R3IcuPlaceholder , R3IfBlock ,
29+ R3LetDeclaration , R3Node , R3SwitchBlock , R3Template , R3Text , R3TextAttribute , R3Variable ,
30+ SecurityContext ,
2931} ;
3032use crate :: ir:: enums:: {
3133 BindingKind , DeferOpModifierKind , DeferTriggerKind , Namespace , SemanticVariableKind ,
@@ -38,9 +40,9 @@ use crate::ir::expression::{
3840use crate :: ir:: ops:: {
3941 BindingOp , ConditionalBranchCreateOp , ConditionalOp , ConditionalUpdateOp , ControlCreateOp ,
4042 CreateOp , CreateOpBase , DeclareLetOp , DeferOnOp , DeferOp , DeferWhenOp , ElementEndOp , ElementOp ,
41- ElementStartOp , ExtractedAttributeOp , I18nPlaceholder , InterpolateTextOp , ListenerOp , LocalRef ,
42- ProjectionOp , RepeaterCreateOp , RepeaterOp , RepeaterVarNames , SlotId , StoreLetOp , TemplateOp ,
43- TextOp , UpdateOp , UpdateOpBase , VariableOp , XrefId ,
43+ ElementStartOp , ExtractedAttributeOp , I18nPlaceholder , IcuEndOp , IcuStartOp , InterpolateTextOp ,
44+ ListenerOp , LocalRef , ProjectionOp , RepeaterCreateOp , RepeaterOp , RepeaterVarNames , SlotId ,
45+ StoreLetOp , TemplateOp , TextOp , UpdateOp , UpdateOpBase , VariableOp , XrefId ,
4446} ;
4547use crate :: output:: ast:: OutputExpression ;
4648use crate :: pipeline:: compilation:: { AliasVariable , ContextVariable } ;
@@ -395,8 +397,10 @@ pub fn ingest_component_with_options<'a>(
395397/// Consumes the node, taking ownership of expressions.
396398fn ingest_node < ' a > ( job : & mut ComponentCompilationJob < ' a > , view_xref : XrefId , node : R3Node < ' a > ) {
397399 match node {
398- R3Node :: Text ( text) => ingest_text ( job, view_xref, text. unbox ( ) ) ,
399- R3Node :: BoundText ( bound_text) => ingest_bound_text ( job, view_xref, bound_text. unbox ( ) ) ,
400+ R3Node :: Text ( text) => ingest_text ( job, view_xref, text. unbox ( ) , None ) ,
401+ R3Node :: BoundText ( bound_text) => {
402+ ingest_bound_text ( job, view_xref, bound_text. unbox ( ) , None )
403+ }
400404 R3Node :: Element ( element) => ingest_element ( job, view_xref, element. unbox ( ) ) ,
401405 R3Node :: Template ( template) => ingest_template ( job, view_xref, template. unbox ( ) ) ,
402406 R3Node :: Content ( content) => ingest_content ( job, view_xref, content. unbox ( ) ) ,
@@ -414,9 +418,7 @@ fn ingest_node<'a>(job: &mut ComponentCompilationJob<'a>, view_xref: XrefId, nod
414418 R3Node :: Comment ( _) => {
415419 // Comments are not ingested into IR
416420 }
417- R3Node :: Icu ( _) => {
418- // ICU expressions are handled separately
419- }
421+ R3Node :: Icu ( icu) => ingest_icu ( job, view_xref, icu. unbox ( ) ) ,
420422 R3Node :: UnknownBlock ( _) => {
421423 // Unknown blocks are skipped with a warning
422424 }
@@ -445,16 +447,24 @@ fn ingest_node<'a>(job: &mut ComponentCompilationJob<'a>, view_xref: XrefId, nod
445447}
446448
447449/// Ingests a static text node.
448- fn ingest_text < ' a > ( job : & mut ComponentCompilationJob < ' a > , view_xref : XrefId , text : R3Text < ' a > ) {
450+ ///
451+ /// `icu_placeholder` is provided when this text is part of an ICU expression,
452+ /// indicating the placeholder name for this text within the ICU message.
453+ fn ingest_text < ' a > (
454+ job : & mut ComponentCompilationJob < ' a > ,
455+ view_xref : XrefId ,
456+ text : R3Text < ' a > ,
457+ icu_placeholder : Option < Atom < ' a > > ,
458+ ) {
449459 let xref = job. allocate_xref_id ( ) ;
450460
451461 let op = CreateOp :: Text ( TextOp {
452462 base : CreateOpBase { source_span : Some ( text. source_span ) , ..Default :: default ( ) } ,
453463 xref,
454464 slot : None ,
455- initial_value : text. value , // Move instead of clone
465+ initial_value : text. value ,
456466 i18n_placeholder : None ,
457- icu_placeholder : None ,
467+ icu_placeholder,
458468 } ) ;
459469
460470 if let Some ( view) = job. view_mut ( view_xref) {
@@ -463,10 +473,14 @@ fn ingest_text<'a>(job: &mut ComponentCompilationJob<'a>, view_xref: XrefId, tex
463473}
464474
465475/// Ingests a bound text node (with interpolation).
476+ ///
477+ /// `icu_placeholder` is provided when this text is part of an ICU expression,
478+ /// indicating the placeholder name for this text within the ICU message.
466479fn ingest_bound_text < ' a > (
467480 job : & mut ComponentCompilationJob < ' a > ,
468481 view_xref : XrefId ,
469482 bound_text : R3BoundText < ' a > ,
483+ icu_placeholder : Option < Atom < ' a > > ,
470484) {
471485 let xref = job. allocate_xref_id ( ) ;
472486
@@ -477,7 +491,7 @@ fn ingest_bound_text<'a>(
477491 slot : None ,
478492 initial_value : Atom :: from ( "" ) ,
479493 i18n_placeholder : None ,
480- icu_placeholder : None ,
494+ icu_placeholder,
481495 } ) ;
482496
483497 if let Some ( view) = job. view_mut ( view_xref) {
@@ -501,6 +515,86 @@ fn ingest_bound_text<'a>(
501515 }
502516}
503517
518+ /// Checks if the i18n metadata is a Message containing a single IcuPlaceholder.
519+ /// Returns the ICU placeholder if so, None otherwise.
520+ fn get_single_icu_placeholder < ' a , ' b > (
521+ meta : & ' b Option < I18nMeta < ' a > > ,
522+ ) -> Option < & ' b I18nIcuPlaceholder < ' a > > {
523+ if let Some ( I18nMeta :: Message ( message) ) = meta {
524+ if message. nodes . len ( ) == 1 {
525+ if let I18nNode :: IcuPlaceholder ( icu_placeholder) = & message. nodes [ 0 ] {
526+ return Some ( icu_placeholder) ;
527+ }
528+ }
529+ }
530+ None
531+ }
532+
533+ /// Ingests an ICU expression node (plural, select, selectordinal).
534+ ///
535+ /// Creates IcuStartOp and IcuEndOp to bracket the ICU expression,
536+ /// and ingests all vars and placeholders within.
537+ ///
538+ /// Ported from Angular's `ingestIcu` in `template/pipeline/src/ingest.ts`.
539+ fn ingest_icu < ' a > ( job : & mut ComponentCompilationJob < ' a > , view_xref : XrefId , icu : R3Icu < ' a > ) {
540+ // Check if the i18n metadata is a Message with a single IcuPlaceholder
541+ // TypeScript: if (icu.i18n instanceof i18n.Message && isSingleI18nIcu(icu.i18n))
542+ let icu_placeholder_name = match get_single_icu_placeholder ( & icu. i18n ) {
543+ Some ( icu_placeholder) => icu_placeholder. name . clone ( ) ,
544+ None => {
545+ // TypeScript throws: Error(`Unhandled i18n metadata type for ICU: ${icu.i18n?.constructor.name}`)
546+ // We report as a diagnostic and return early
547+ job. diagnostics . push ( OxcDiagnostic :: error (
548+ "Unhandled i18n metadata type for ICU: expected Message with single IcuPlaceholder" ,
549+ ) . with_label ( icu. source_span ) ) ;
550+ return ;
551+ }
552+ } ;
553+
554+ let xref = job. allocate_xref_id ( ) ;
555+
556+ // Create IcuStartOp
557+ let start_op = CreateOp :: IcuStart ( IcuStartOp {
558+ base : CreateOpBase { source_span : Some ( icu. source_span ) , ..Default :: default ( ) } ,
559+ xref,
560+ context : None , // Will be set by create_i18n_contexts phase
561+ message : None , // Will be set by phases
562+ icu_placeholder : Some ( icu_placeholder_name) ,
563+ } ) ;
564+
565+ if let Some ( view) = job. view_mut ( view_xref) {
566+ view. create . push ( start_op) ;
567+ }
568+
569+ // Process vars (bound text expressions)
570+ // In Rust, vars is typed as HashMap<Atom, R3BoundText> so no runtime check needed
571+ for ( placeholder_name, bound_text) in icu. vars {
572+ ingest_bound_text ( job, view_xref, bound_text, Some ( placeholder_name) ) ;
573+ }
574+
575+ // Process placeholders (text or bound text)
576+ for ( placeholder_name, placeholder) in icu. placeholders {
577+ match placeholder {
578+ R3IcuPlaceholder :: Text ( text) => {
579+ ingest_text ( job, view_xref, text, Some ( placeholder_name) ) ;
580+ }
581+ R3IcuPlaceholder :: BoundText ( bound_text) => {
582+ ingest_bound_text ( job, view_xref, bound_text, Some ( placeholder_name) ) ;
583+ }
584+ }
585+ }
586+
587+ // Create IcuEndOp
588+ let end_op = CreateOp :: IcuEnd ( IcuEndOp {
589+ base : CreateOpBase { source_span : Some ( icu. source_span ) , ..Default :: default ( ) } ,
590+ xref,
591+ } ) ;
592+
593+ if let Some ( view) = job. view_mut ( view_xref) {
594+ view. create . push ( end_op) ;
595+ }
596+ }
597+
504598/// Splits a namespaced name like `:svg:path` into (namespace_key, element_name).
505599///
506600/// Ported from Angular's `splitNsName` in `src/ml_parser/tags.ts`.
@@ -1581,8 +1675,20 @@ fn ingest_defer_block<'a>(
15811675 DeferMetadata :: PerBlock { blocks } => {
15821676 // In PerBlock mode, look up the resolver from the blocks map using source_span
15831677 // Use remove() to take ownership (move) since we can't clone OutputExpression
1584- // If not found, the block has no lazy dependencies (own_resolver_fn is null)
1585- blocks. remove ( & defer_block. source_span ) . flatten ( )
1678+ // TypeScript throws if the block is not in the map at all
1679+ match blocks. remove ( & defer_block. source_span ) {
1680+ Some ( value) => {
1681+ // Key exists - value may be None (no lazy deps) or Some (has deps)
1682+ value
1683+ }
1684+ None => {
1685+ // TypeScript: throw Error(`AssertionError: unable to find a dependency function for this deferred block`)
1686+ job. diagnostics . push ( OxcDiagnostic :: error (
1687+ "AssertionError: unable to find a dependency function for this deferred block" ,
1688+ ) . with_label ( defer_block. source_span ) ) ;
1689+ None
1690+ }
1691+ }
15861692 }
15871693 DeferMetadata :: PerComponent { .. } => {
15881694 // In PerComponent mode, own_resolver_fn is null
@@ -1591,10 +1697,10 @@ fn ingest_defer_block<'a>(
15911697 }
15921698 } ;
15931699
1594- // For resolver_fn, take ownership from all_deferrable_deps_fn
1595- // Note: In PerComponent mode, all defer blocks share this, so we take it only once
1596- // and leave None for subsequent blocks (they'll get resolver_fn set by resolve_defer_deps_fns)
1597- let resolver_fn = job. all_deferrable_deps_fn . take ( ) ;
1700+ // In PerComponent mode, all defer blocks share the same allDeferrableDepsFn reference.
1701+ // We clone it so each defer block gets its own copy (matching TypeScript's behavior
1702+ // where ReadVarExpr is shared by reference).
1703+ let resolver_fn = job. all_deferrable_deps_fn . as_ref ( ) . map ( |expr| expr . clone_in ( job . allocator ) ) ;
15981704
15991705 let op = CreateOp :: Defer ( DeferOp {
16001706 base : CreateOpBase { source_span : Some ( defer_block. source_span ) , ..Default :: default ( ) } ,
0 commit comments