Skip to content

Commit 3eeacb4

Browse files
committed
save
1 parent 1e1dc0a commit 3eeacb4

28 files changed

+358
-228
lines changed

crates/oxc_angular_compiler/src/ir/ops.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,6 +1290,11 @@ pub struct I18nOp<'a> {
12901290
pub message: Option<XrefId>,
12911291
/// I18n placeholder data (start_name and close_name for i18n blocks).
12921292
pub i18n_placeholder: Option<I18nPlaceholder<'a>>,
1293+
/// Sub-template index for nested templates inside i18n blocks.
1294+
/// None for root-level i18n blocks.
1295+
pub sub_template_index: Option<u32>,
1296+
/// Root i18n block reference (for nested i18n blocks).
1297+
pub root: Option<XrefId>,
12931298
/// Index into the consts array for the i18n message.
12941299
/// Set by the i18n_const_collection phase.
12951300
pub message_index: Option<u32>,

crates/oxc_angular_compiler/src/output/emitter.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -855,10 +855,6 @@ impl JsEmitter {
855855
fn visit_literal_array<'a>(&self, entries: &[OutputExpression<'a>], ctx: &mut EmitterContext) {
856856
ctx.print("[");
857857
self.visit_all_expressions(entries, ctx, ",");
858-
// Add trailing comma for non-empty arrays to match Angular's output
859-
if !entries.is_empty() {
860-
ctx.print(",");
861-
}
862858
ctx.print("]");
863859
}
864860

@@ -896,10 +892,6 @@ impl JsEmitter {
896892
ctx.print(":");
897893
self.visit_expression(&entry.value, ctx);
898894
}
899-
// Add trailing comma for non-empty object literals to match Angular's output
900-
if !entries.is_empty() {
901-
ctx.print(",");
902-
}
903895
if incremented_indent {
904896
ctx.dec_indent();
905897
ctx.dec_indent();

crates/oxc_angular_compiler/src/pipeline/ingest.rs

Lines changed: 77 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -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;
2526
use crate::ast::expression::{AngularExpression, ParsedEventType};
2627
use 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

Comments
 (0)