Skip to content

Commit b76e59d

Browse files
committed
save
1 parent 3eeacb4 commit b76e59d

File tree

14 files changed

+712
-154
lines changed

14 files changed

+712
-154
lines changed

crates/oxc_angular_compiler/src/ast/r3.rs

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//!
66
//! Ported from Angular's `render3/r3_ast.ts`.
77
8-
use oxc_allocator::{Box, HashMap, Vec};
8+
use oxc_allocator::{Allocator, Box, HashMap, Vec};
99
use oxc_span::{Atom, Span};
1010

1111
use crate::ast::expression::{ASTWithSource, AngularExpression, BindingType, ParsedEventType};
@@ -161,6 +161,164 @@ pub struct I18nBlockPlaceholder<'a> {
161161
pub end_source_span: Option<Span>,
162162
}
163163

164+
// ============================================================================
165+
// i18n Clone implementations
166+
// ============================================================================
167+
168+
impl<'a> I18nMeta<'a> {
169+
/// Creates a deep clone of this i18n metadata using the provided allocator.
170+
pub fn clone_in(&self, allocator: &'a Allocator) -> Self {
171+
match self {
172+
I18nMeta::Message(msg) => I18nMeta::Message(msg.clone_in(allocator)),
173+
I18nMeta::Node(node) => I18nMeta::Node(node.clone_in(allocator)),
174+
I18nMeta::BlockPlaceholder(bp) => I18nMeta::BlockPlaceholder(bp.clone_in(allocator)),
175+
}
176+
}
177+
}
178+
179+
impl<'a> I18nMessage<'a> {
180+
/// Creates a deep clone of this i18n message using the provided allocator.
181+
pub fn clone_in(&self, allocator: &'a Allocator) -> Self {
182+
let mut nodes = Vec::new_in(allocator);
183+
for node in self.nodes.iter() {
184+
nodes.push(node.clone_in(allocator));
185+
}
186+
let mut legacy_ids = Vec::new_in(allocator);
187+
for id in self.legacy_ids.iter() {
188+
legacy_ids.push(id.clone());
189+
}
190+
I18nMessage {
191+
nodes,
192+
meaning: self.meaning.clone(),
193+
description: self.description.clone(),
194+
custom_id: self.custom_id.clone(),
195+
id: self.id.clone(),
196+
legacy_ids,
197+
}
198+
}
199+
}
200+
201+
impl<'a> I18nNode<'a> {
202+
/// Creates a deep clone of this i18n node using the provided allocator.
203+
pub fn clone_in(&self, allocator: &'a Allocator) -> Self {
204+
match self {
205+
I18nNode::Text(t) => I18nNode::Text(t.clone_in()),
206+
I18nNode::Container(c) => I18nNode::Container(c.clone_in(allocator)),
207+
I18nNode::Icu(i) => I18nNode::Icu(i.clone_in(allocator)),
208+
I18nNode::TagPlaceholder(tp) => I18nNode::TagPlaceholder(tp.clone_in(allocator)),
209+
I18nNode::Placeholder(p) => I18nNode::Placeholder(p.clone_in()),
210+
I18nNode::IcuPlaceholder(ip) => I18nNode::IcuPlaceholder(ip.clone_in(allocator)),
211+
I18nNode::BlockPlaceholder(bp) => I18nNode::BlockPlaceholder(bp.clone_in(allocator)),
212+
}
213+
}
214+
}
215+
216+
impl<'a> I18nText<'a> {
217+
/// Creates a clone of this i18n text (no allocator needed - only atoms and spans).
218+
pub fn clone_in(&self) -> Self {
219+
I18nText { value: self.value.clone(), source_span: self.source_span }
220+
}
221+
}
222+
223+
impl<'a> I18nContainer<'a> {
224+
/// Creates a deep clone of this i18n container using the provided allocator.
225+
pub fn clone_in(&self, allocator: &'a Allocator) -> Self {
226+
let mut children = Vec::new_in(allocator);
227+
for child in self.children.iter() {
228+
children.push(child.clone_in(allocator));
229+
}
230+
I18nContainer { children, source_span: self.source_span }
231+
}
232+
}
233+
234+
impl<'a> I18nIcu<'a> {
235+
/// Creates a deep clone of this ICU expression using the provided allocator.
236+
pub fn clone_in(&self, allocator: &'a Allocator) -> Self {
237+
let mut cases = HashMap::new_in(allocator);
238+
for (key, value) in self.cases.iter() {
239+
cases.insert(key.clone(), value.clone_in(allocator));
240+
}
241+
I18nIcu {
242+
expression: self.expression.clone(),
243+
icu_type: self.icu_type.clone(),
244+
cases,
245+
source_span: self.source_span,
246+
expression_placeholder: self.expression_placeholder.clone(),
247+
}
248+
}
249+
}
250+
251+
impl<'a> I18nTagPlaceholder<'a> {
252+
/// Creates a deep clone of this tag placeholder using the provided allocator.
253+
pub fn clone_in(&self, allocator: &'a Allocator) -> Self {
254+
let mut attrs = HashMap::new_in(allocator);
255+
for (key, value) in self.attrs.iter() {
256+
attrs.insert(key.clone(), value.clone());
257+
}
258+
let mut children = Vec::new_in(allocator);
259+
for child in self.children.iter() {
260+
children.push(child.clone_in(allocator));
261+
}
262+
I18nTagPlaceholder {
263+
tag: self.tag.clone(),
264+
attrs,
265+
start_name: self.start_name.clone(),
266+
close_name: self.close_name.clone(),
267+
children,
268+
is_void: self.is_void,
269+
source_span: self.source_span,
270+
start_source_span: self.start_source_span,
271+
end_source_span: self.end_source_span,
272+
}
273+
}
274+
}
275+
276+
impl<'a> I18nPlaceholder<'a> {
277+
/// Creates a clone of this placeholder (no allocator needed - only atoms and spans).
278+
pub fn clone_in(&self) -> Self {
279+
I18nPlaceholder {
280+
value: self.value.clone(),
281+
name: self.name.clone(),
282+
source_span: self.source_span,
283+
}
284+
}
285+
}
286+
287+
impl<'a> I18nIcuPlaceholder<'a> {
288+
/// Creates a deep clone of this ICU placeholder using the provided allocator.
289+
pub fn clone_in(&self, allocator: &'a Allocator) -> Self {
290+
I18nIcuPlaceholder {
291+
value: Box::new_in(self.value.clone_in(allocator), allocator),
292+
name: self.name.clone(),
293+
source_span: self.source_span,
294+
}
295+
}
296+
}
297+
298+
impl<'a> I18nBlockPlaceholder<'a> {
299+
/// Creates a deep clone of this block placeholder using the provided allocator.
300+
pub fn clone_in(&self, allocator: &'a Allocator) -> Self {
301+
let mut parameters = Vec::new_in(allocator);
302+
for param in self.parameters.iter() {
303+
parameters.push(param.clone());
304+
}
305+
let mut children = Vec::new_in(allocator);
306+
for child in self.children.iter() {
307+
children.push(child.clone_in(allocator));
308+
}
309+
I18nBlockPlaceholder {
310+
name: self.name.clone(),
311+
parameters,
312+
start_name: self.start_name.clone(),
313+
close_name: self.close_name.clone(),
314+
children,
315+
source_span: self.source_span,
316+
start_source_span: self.start_source_span,
317+
end_source_span: self.end_source_span,
318+
}
319+
}
320+
}
321+
164322
// ============================================================================
165323
// Core Node Enum
166324
// ============================================================================

crates/oxc_angular_compiler/src/ir/list.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,14 @@ impl<'a, T: Op> OpList<'a, T> {
221221
/// # Safety
222222
/// The `after` pointer must point to a valid operation in this list.
223223
pub unsafe fn insert_after(&mut self, after: NonNull<T>, op: T) {
224+
let _ = unsafe { self.insert_after_returning_new(after, op) };
225+
}
226+
227+
/// Inserts an operation after the given operation and returns a pointer to the new operation.
228+
///
229+
/// # Safety
230+
/// The `after` pointer must point to a valid operation in this list.
231+
pub unsafe fn insert_after_returning_new(&mut self, after: NonNull<T>, op: T) -> NonNull<T> {
224232
let ptr = self.alloc_op(op);
225233

226234
// SAFETY: after is a valid pointer in this list
@@ -239,6 +247,7 @@ impl<'a, T: Op> OpList<'a, T> {
239247
}
240248

241249
self.len += 1;
250+
ptr
242251
}
243252

244253
/// Removes an operation from the list.

crates/oxc_angular_compiler/src/output/emitter.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1681,7 +1681,7 @@ mod tests {
16811681
));
16821682

16831683
let output = emitter.emit_expression(&array_expr);
1684-
assert_eq!(output, "[...arr,1,2,]");
1684+
assert_eq!(output, "[...arr,1,2]");
16851685
}
16861686

16871687
#[test]
@@ -1719,6 +1719,6 @@ mod tests {
17191719
));
17201720

17211721
let output = emitter.emit_expression(&array_expr);
1722-
assert_eq!(output, "[...a,...b,]");
1722+
assert_eq!(output, "[...a,...b]");
17231723
}
17241724
}

crates/oxc_angular_compiler/src/pipeline/ingest.rs

Lines changed: 81 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,14 +1079,6 @@ fn ingest_element<'a>(
10791079

10801080
if let Some(view) = job.view_mut(view_xref) {
10811081
view.create.push(start_op);
1082-
1083-
// If the element has a [field] property binding, add ControlCreateOp.
1084-
// This is used for form control bindings that require synchronization.
1085-
if let Some(span) = field_input_span {
1086-
view.create.push(CreateOp::ControlCreate(ControlCreateOp {
1087-
base: CreateOpBase { source_span: Some(span), ..Default::default() },
1088-
}));
1089-
}
10901082
}
10911083

10921084
// Ingest static attributes (must happen BEFORE bound inputs for proper order)
@@ -1127,56 +1119,66 @@ fn ingest_element<'a>(
11271119
view.create.push(i18n_attrs_op);
11281120
}
11291121

1130-
// Start i18n block if element has i18n Message metadata
1122+
// Start i18n block if element has i18n Message metadata AND has children.
1123+
// Skip i18n for elements with no content (e.g., self-closing elements with i18n attr).
11311124
// Ported from Angular's ingest.ts lines 300-307
11321125
let i18n_block_id = if let Some(I18nMeta::Message(ref message)) = element.i18n {
1133-
let i18n_xref = job.allocate_xref_id();
1126+
// Only create i18n block if there are children to translate
1127+
if element.children.is_empty() {
1128+
None
1129+
} else {
1130+
let i18n_xref = job.allocate_xref_id();
11341131

1135-
// Store i18n message metadata for later phases
1136-
// Clone legacy_ids using the allocator
1137-
let mut legacy_ids = Vec::new_in(allocator);
1138-
for id in message.legacy_ids.iter() {
1139-
legacy_ids.push(id.clone());
1140-
}
1132+
// Store i18n message metadata for later phases
1133+
// Clone legacy_ids using the allocator
1134+
let mut legacy_ids = Vec::new_in(allocator);
1135+
for id in message.legacy_ids.iter() {
1136+
legacy_ids.push(id.clone());
1137+
}
11411138

1142-
let metadata = I18nMessageMetadata {
1143-
message_id: if message.id.is_empty() { None } else { Some(message.id.clone()) },
1144-
custom_id: if message.custom_id.is_empty() {
1145-
None
1146-
} else {
1147-
Some(message.custom_id.clone())
1148-
},
1149-
meaning: if message.meaning.is_empty() { None } else { Some(message.meaning.clone()) },
1150-
description: if message.description.is_empty() {
1151-
None
1152-
} else {
1153-
Some(message.description.clone())
1154-
},
1155-
legacy_ids,
1156-
};
1157-
job.i18n_message_metadata.insert(i18n_xref, metadata);
1139+
let metadata = I18nMessageMetadata {
1140+
message_id: if message.id.is_empty() { None } else { Some(message.id.clone()) },
1141+
custom_id: if message.custom_id.is_empty() {
1142+
None
1143+
} else {
1144+
Some(message.custom_id.clone())
1145+
},
1146+
meaning: if message.meaning.is_empty() {
1147+
None
1148+
} else {
1149+
Some(message.meaning.clone())
1150+
},
1151+
description: if message.description.is_empty() {
1152+
None
1153+
} else {
1154+
Some(message.description.clone())
1155+
},
1156+
legacy_ids,
1157+
};
1158+
job.i18n_message_metadata.insert(i18n_xref, metadata);
11581159

1159-
// Create I18nStartOp
1160-
let i18n_start = CreateOp::I18nStart(I18nStartOp {
1161-
base: CreateOpBase {
1162-
source_span: Some(element.start_source_span),
1163-
..Default::default()
1164-
},
1165-
xref: i18n_xref,
1166-
slot: None,
1167-
context: None, // Will be set by create_i18n_contexts phase
1168-
message: Some(i18n_xref), // Message xref for metadata lookup
1169-
i18n_placeholder: None, // Root i18n block has no placeholder
1170-
sub_template_index: None, // Will be set by propagate_i18n_blocks phase
1171-
root: None, // Root i18n block has no root
1172-
message_index: None, // Will be set by i18n_const_collection phase
1173-
});
1160+
// Create I18nStartOp
1161+
let i18n_start = CreateOp::I18nStart(I18nStartOp {
1162+
base: CreateOpBase {
1163+
source_span: Some(element.start_source_span),
1164+
..Default::default()
1165+
},
1166+
xref: i18n_xref,
1167+
slot: None,
1168+
context: None, // Will be set by create_i18n_contexts phase
1169+
message: Some(i18n_xref), // Message xref for metadata lookup
1170+
i18n_placeholder: None, // Root i18n block has no placeholder
1171+
sub_template_index: None, // Will be set by propagate_i18n_blocks phase
1172+
root: None, // Root i18n block has no root
1173+
message_index: None, // Will be set by i18n_const_collection phase
1174+
});
11741175

1175-
if let Some(view) = job.view_mut(view_xref) {
1176-
view.create.push(i18n_start);
1177-
}
1176+
if let Some(view) = job.view_mut(view_xref) {
1177+
view.create.push(i18n_start);
1178+
}
11781179

1179-
Some(i18n_xref)
1180+
Some(i18n_xref)
1181+
}
11801182
} else {
11811183
None
11821184
};
@@ -1214,6 +1216,18 @@ fn ingest_element<'a>(
12141216
if let Some(view) = job.view_mut(view_xref) {
12151217
view.create.push(end_op);
12161218
}
1219+
1220+
// We want to ensure that the controlCreateOp is after the ops that create the element.
1221+
// Ported from Angular's ingest.ts lines 319-327.
1222+
// If the element has a [field] property binding, add ControlCreateOp.
1223+
// This is used for form control bindings that require synchronization.
1224+
if let Some(span) = field_input_span {
1225+
if let Some(view) = job.view_mut(view_xref) {
1226+
view.create.push(CreateOp::ControlCreate(ControlCreateOp {
1227+
base: CreateOpBase { source_span: Some(span), ..Default::default() },
1228+
}));
1229+
}
1230+
}
12171231
}
12181232

12191233
/// Ingests static attributes from an element into BindingOp operations.
@@ -1792,9 +1806,24 @@ fn ingest_template<'a>(
17921806
references,
17931807
variables,
17941808
source_span,
1809+
i18n,
17951810
..
17961811
} = template;
17971812

1813+
// Extract i18n placeholder if template is inside an i18n block (TagPlaceholder).
1814+
// Ported from Angular's ingest.ts line 357:
1815+
// const i18nPlaceholder = tmpl.i18n instanceof i18n.TagPlaceholder ? tmpl.i18n : undefined;
1816+
let i18n_placeholder = if let Some(I18nMeta::Node(I18nNode::TagPlaceholder(tag_placeholder))) =
1817+
&i18n
1818+
{
1819+
Some(I18nPlaceholder::new(
1820+
tag_placeholder.start_name.clone(),
1821+
if tag_placeholder.is_void { None } else { Some(tag_placeholder.close_name.clone()) },
1822+
))
1823+
} else {
1824+
None
1825+
};
1826+
17981827
// Create embedded view for template content.
17991828
// In TypeScript, allocateView() returns the embedded view, and its xref is used as the
18001829
// TemplateOp's xref. There is NO separate xref allocation - TemplateOp.xref IS the embedded
@@ -1851,7 +1880,7 @@ fn ingest_template<'a>(
18511880
attributes: None, // Set by attribute extraction phase
18521881
local_refs,
18531882
local_refs_index: None, // Set by local_refs extraction phase
1854-
i18n_placeholder: None,
1883+
i18n_placeholder,
18551884
});
18561885

18571886
if let Some(view) = job.view_mut(view_xref) {

0 commit comments

Comments
 (0)