Skip to content

Commit c25d314

Browse files
committed
align html escape
1 parent 9e6203e commit c25d314

22 files changed

+997
-234
lines changed

crates/oxc_angular_compiler/src/ast/html.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,15 @@ pub struct HtmlText<'a> {
9595
#[derive(Debug)]
9696
pub struct HtmlElement<'a> {
9797
/// The element tag name.
98+
/// For regular elements: the tag name (e.g., "div", ":svg:rect").
99+
/// For selectorless components: the component name (e.g., "MyComp").
98100
pub name: Atom<'a>,
101+
/// For selectorless components: the namespace prefix (e.g., "svg" in `<ng-component:MyComp:svg:rect>`).
102+
/// None for regular elements or selectorless components without namespace.
103+
pub component_prefix: Option<Atom<'a>>,
104+
/// For selectorless components: the HTML tag name (e.g., "rect" in `<ng-component:MyComp:svg:rect>`).
105+
/// None for regular elements or selectorless components without tag name.
106+
pub component_tag_name: Option<Atom<'a>>,
99107
/// The element attributes.
100108
pub attrs: Vec<'a, HtmlAttribute<'a>>,
101109
/// Selectorless directives (e.g., @Dir, @Dir(attr="value")).
@@ -451,6 +459,8 @@ mod tests {
451459
// Create a simple tree: root element with two child elements
452460
let child1 = HtmlElement {
453461
name: Atom::from("span"),
462+
component_prefix: None,
463+
component_tag_name: None,
454464
attrs: Vec::new_in(&allocator),
455465
directives: Vec::new_in(&allocator),
456466
children: Vec::new_in(&allocator),
@@ -463,6 +473,8 @@ mod tests {
463473

464474
let child2 = HtmlElement {
465475
name: Atom::from("p"),
476+
component_prefix: None,
477+
component_tag_name: None,
466478
attrs: Vec::new_in(&allocator),
467479
directives: Vec::new_in(&allocator),
468480
children: Vec::new_in(&allocator),
@@ -479,6 +491,8 @@ mod tests {
479491

480492
let root = HtmlElement {
481493
name: Atom::from("div"),
494+
component_prefix: None,
495+
component_tag_name: None,
482496
attrs: Vec::new_in(&allocator),
483497
directives: Vec::new_in(&allocator),
484498
children,

crates/oxc_angular_compiler/src/component/decorator.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,9 @@ fn extract_string_value<'a>(expr: &Expression<'a>) -> Option<Atom<'a>> {
178178
Expression::StringLiteral(lit) => Some(lit.value.clone()),
179179
Expression::TemplateLiteral(tpl) if tpl.expressions.is_empty() => {
180180
// Simple template literal with no expressions: `template string`
181-
tpl.quasis.first().map(|q| q.value.raw.clone())
181+
// Use cooked value to properly interpret escape sequences (\n -> newline)
182+
// Angular evaluates template literals, so we need cooked, not raw
183+
tpl.quasis.first().and_then(|q| q.value.cooked.clone())
182184
}
183185
_ => None,
184186
}
@@ -207,8 +209,11 @@ fn extract_string_array<'a>(
207209
result.push(lit.value.clone());
208210
} else if let ArrayExpressionElement::TemplateLiteral(tpl) = element {
209211
if tpl.expressions.is_empty() {
212+
// Use cooked value to properly interpret escape sequences
210213
if let Some(quasi) = tpl.quasis.first() {
211-
result.push(quasi.value.raw.clone());
214+
if let Some(cooked) = &quasi.value.cooked {
215+
result.push(cooked.clone());
216+
}
212217
}
213218
}
214219
}

crates/oxc_angular_compiler/src/component/transform.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,12 +419,14 @@ fn compile_component_full<'a>(
419419

420420
// Stage 1: Parse HTML
421421
// Build parse options from component metadata
422+
// Angular always forces tokenizeExpansionForms: true in parseTemplate()
422423
let parse_options = ParseTemplateOptions {
423424
preserve_whitespaces: metadata.preserve_whitespaces,
424425
// Enable modern syntax features (block syntax, let declarations)
425426
enable_block_syntax: true,
426427
enable_let_syntax: true,
427-
// Other options left at defaults for now
428+
// Always enable ICU expansion forms - Angular forces this in template.ts:152
429+
tokenize_expansion_forms: true,
428430
..Default::default()
429431
};
430432
let parser = HtmlParser::with_options(allocator, template, file_path, &parse_options);
@@ -636,10 +638,13 @@ pub fn compile_component_template<'a>(
636638
let mut diagnostics = Vec::new();
637639

638640
// Stage 1: Parse HTML with default options
641+
// Angular always forces tokenizeExpansionForms: true in parseTemplate()
639642
let parse_options = ParseTemplateOptions {
640643
// Enable modern syntax features (block syntax, let declarations)
641644
enable_block_syntax: true,
642645
enable_let_syntax: true,
646+
// Always enable ICU expansion forms - Angular forces this in template.ts:152
647+
tokenize_expansion_forms: true,
643648
// preserve_whitespaces defaults to false, matching Angular's default
644649
..Default::default()
645650
};
@@ -730,10 +735,13 @@ pub fn compile_template_to_js_with_options<'a>(
730735
let mut diagnostics = Vec::new();
731736

732737
// Stage 1: Parse HTML with default options
738+
// Angular always forces tokenizeExpansionForms: true in parseTemplate()
733739
let parse_options = ParseTemplateOptions {
734740
// Enable modern syntax features (block syntax, let declarations)
735741
enable_block_syntax: true,
736742
enable_let_syntax: true,
743+
// Always enable ICU expansion forms - Angular forces this in template.ts:152
744+
tokenize_expansion_forms: true,
737745
// preserve_whitespaces defaults to false, matching Angular's default
738746
..Default::default()
739747
};

crates/oxc_angular_compiler/src/ir/ops.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,6 +1150,10 @@ pub struct RepeaterCreateOp<'a> {
11501150
pub vars: Option<u32>,
11511151
/// Var names for template.
11521152
pub var_names: RepeaterVarNames<'a>,
1153+
/// HTML tag name (for content projection).
1154+
pub tag: Option<Atom<'a>>,
1155+
/// HTML tag name for empty view (for content projection).
1156+
pub empty_tag: Option<Atom<'a>>,
11531157
/// I18n placeholder data (start_name and close_name for @for block).
11541158
pub i18n_placeholder: Option<I18nPlaceholder<'a>>,
11551159
/// I18n placeholder data for @empty view.
@@ -1535,6 +1539,12 @@ pub struct AttributeOp<'a> {
15351539
pub i18n_context: Option<XrefId>,
15361540
/// I18n message.
15371541
pub i18n_message: Option<XrefId>,
1542+
/// Whether this is a text attribute (static attribute from template).
1543+
///
1544+
/// Text attributes are extractable to the consts array and don't need
1545+
/// runtime updates. This is used by attribute_extraction to determine
1546+
/// if the attribute should be extracted.
1547+
pub is_text_attribute: bool,
15381548
}
15391549

15401550
/// DOM property binding.

crates/oxc_angular_compiler/src/parser/html/lexer.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,9 @@ pub struct HtmlLexer<'a> {
545545
/// Enable block tokenization (default: true).
546546
/// When enabled, standalone `}` characters become BLOCK_CLOSE tokens.
547547
tokenize_blocks: bool,
548+
/// Enable @let tokenization (default: true).
549+
/// When disabled, @let is treated as text or incomplete block.
550+
tokenize_let: bool,
548551
/// Characters to consider as leading trivia (for source map optimization).
549552
leading_trivia_chars: Option<Vec<char>>,
550553
/// Range start position (for processing a sub-range of input).
@@ -577,6 +580,7 @@ impl<'a> HtmlLexer<'a> {
577580
expansion_case_stack: Vec::new(),
578581
escaped_string: false,
579582
tokenize_blocks: true, // default to true like Angular
583+
tokenize_let: true, // default to true like Angular
580584
leading_trivia_chars: None,
581585
range_start_pos: 0,
582586
range_end_pos: length,
@@ -616,6 +620,12 @@ impl<'a> HtmlLexer<'a> {
616620
self
617621
}
618622

623+
/// Enables or disables @let tokenization.
624+
pub fn with_let(mut self, enabled: bool) -> Self {
625+
self.tokenize_let = enabled;
626+
self
627+
}
628+
619629
/// Sets the leading trivia characters for source map optimization.
620630
pub fn with_leading_trivia_chars(mut self, chars: Vec<char>) -> Self {
621631
self.leading_trivia_chars = Some(chars);
@@ -988,8 +998,8 @@ impl<'a> HtmlLexer<'a> {
988998
return;
989999
}
9901000

991-
// Check for @let declarations
992-
if self.peek() == '@' && self.starts_with("@let") {
1001+
// Check for @let declarations (only if tokenize_let is enabled)
1002+
if self.tokenize_let && self.peek() == '@' && self.starts_with("@let") {
9931003
// Make sure "@let" is followed by whitespace (not "@letter")
9941004
let next_char_index = self.index as usize + 4;
9951005
if next_char_index < self.input.len() {
@@ -1018,7 +1028,7 @@ impl<'a> HtmlLexer<'a> {
10181028

10191029
// Check for block start (@if, @for, etc.)
10201030
// Only match supported block keywords - `@` followed by non-keyword is text
1021-
if self.is_block_start() {
1031+
if self.tokenize_blocks && self.is_block_start() {
10221032
self.scan_block(start);
10231033
return;
10241034
}

crates/oxc_angular_compiler/src/parser/html/parser.rs

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,17 @@ pub struct HtmlParser<'a> {
6363
impl<'a> HtmlParser<'a> {
6464
/// Creates a new parser.
6565
pub fn new(allocator: &'a Allocator, source: &'a str, url: &str) -> Self {
66-
Self::new_internal(allocator, source, url, false, false, None)
66+
Self::new_internal(allocator, source, url, false, false, None, true, true, None)
6767
}
6868

6969
/// Creates a new parser with selectorless mode enabled.
7070
pub fn with_selectorless(allocator: &'a Allocator, source: &'a str, url: &str) -> Self {
71-
Self::new_internal(allocator, source, url, true, false, None)
71+
Self::new_internal(allocator, source, url, true, false, None, true, true, None)
7272
}
7373

7474
/// Creates a new parser with expansion forms (ICU messages) enabled.
7575
pub fn with_expansion_forms(allocator: &'a Allocator, source: &'a str, url: &str) -> Self {
76-
Self::new_internal(allocator, source, url, false, true, None)
76+
Self::new_internal(allocator, source, url, false, true, None, true, true, None)
7777
}
7878

7979
/// Creates a new parser with expansion forms and leading trivia chars enabled.
@@ -83,7 +83,17 @@ impl<'a> HtmlParser<'a> {
8383
url: &str,
8484
leading_trivia_chars: std::vec::Vec<char>,
8585
) -> Self {
86-
Self::new_internal(allocator, source, url, false, true, Some(leading_trivia_chars))
86+
Self::new_internal(
87+
allocator,
88+
source,
89+
url,
90+
false,
91+
true,
92+
Some(leading_trivia_chars),
93+
true,
94+
true,
95+
None,
96+
)
8797
}
8898

8999
/// Creates a new parser with the given template options.
@@ -103,6 +113,9 @@ impl<'a> HtmlParser<'a> {
103113
options.enable_selectorless,
104114
options.tokenize_expansion_forms,
105115
options.leading_trivia_chars.clone(),
116+
options.enable_block_syntax,
117+
options.enable_let_syntax,
118+
options.interpolation.as_ref().map(|(s, e)| (s.as_str(), e.as_str())),
106119
)
107120
}
108121

@@ -114,15 +127,24 @@ impl<'a> HtmlParser<'a> {
114127
selectorless: bool,
115128
expansion_forms: bool,
116129
leading_trivia_chars: Option<std::vec::Vec<char>>,
130+
tokenize_blocks: bool,
131+
tokenize_let: bool,
132+
interpolation: Option<(&str, &str)>,
117133
) -> Self {
118134
let mut lexer = HtmlLexer::new(source)
119135
.with_selectorless(selectorless)
120-
.with_expansion_forms(expansion_forms);
136+
.with_expansion_forms(expansion_forms)
137+
.with_blocks(tokenize_blocks)
138+
.with_let(tokenize_let);
121139

122140
if let Some(chars) = leading_trivia_chars {
123141
lexer = lexer.with_leading_trivia_chars(chars);
124142
}
125143

144+
if let Some((start, end)) = interpolation {
145+
lexer = lexer.with_interpolation(start, end);
146+
}
147+
126148
let result = lexer.tokenize();
127149
let source_file = Arc::new(ParseSourceFile::new(source.to_string(), url.to_string()));
128150

@@ -208,6 +230,8 @@ impl<'a> HtmlParser<'a> {
208230
&mut self.elements[idx],
209231
HtmlElement {
210232
name: Atom::from(""),
233+
component_prefix: None,
234+
component_tag_name: None,
211235
attrs: Vec::new_in(self.allocator),
212236
directives: Vec::new_in(self.allocator),
213237
children: Vec::new_in(self.allocator),
@@ -335,6 +359,8 @@ impl<'a> HtmlParser<'a> {
335359
&mut self.elements[idx],
336360
HtmlElement {
337361
name: Atom::from(""),
362+
component_prefix: None,
363+
component_tag_name: None,
338364
attrs: Vec::new_in(self.allocator),
339365
directives: Vec::new_in(self.allocator),
340366
children: Vec::new_in(self.allocator),
@@ -376,6 +402,8 @@ impl<'a> HtmlParser<'a> {
376402
&mut self.elements[match_elem_idx],
377403
HtmlElement {
378404
name: Atom::from(""),
405+
component_prefix: None,
406+
component_tag_name: None,
379407
attrs: Vec::new_in(self.allocator),
380408
directives: Vec::new_in(self.allocator),
381409
children: Vec::new_in(self.allocator),
@@ -521,11 +549,20 @@ impl<'a> HtmlParser<'a> {
521549
let start = start_token.start;
522550
// TagOpenStart has parts [prefix, name]
523551
// ComponentOpenStart has parts [component_name, prefix, tag_name]
524-
let (tag_name, local_name, has_ns_prefix) =
552+
let (tag_name, local_name, has_ns_prefix, component_prefix, component_tag_name) =
525553
if start_token.token_type == HtmlTokenType::ComponentOpenStart {
526-
// For components, use the component name (first part)
527-
let name = start_token.value().to_string();
528-
(name.clone(), name, false)
554+
// For components, extract all three parts:
555+
// parts[0] = component_name, parts[1] = prefix, parts[2] = tag_name
556+
let component_name = start_token.parts.first().cloned().unwrap_or_default();
557+
let prefix = start_token.parts.get(1).cloned().unwrap_or_default();
558+
let raw_tag_name = start_token.parts.get(2).cloned().unwrap_or_default();
559+
560+
// Store prefix and tag_name for HtmlElement
561+
let prefix_opt = if prefix.is_empty() { None } else { Some(prefix.clone()) };
562+
let tag_opt =
563+
if raw_tag_name.is_empty() { None } else { Some(raw_tag_name.clone()) };
564+
565+
(component_name.clone(), component_name, false, prefix_opt, tag_opt)
529566
} else {
530567
// For regular tags, include the namespace prefix if present
531568
// Angular uses :prefix:name format for namespaced elements
@@ -534,7 +571,7 @@ impl<'a> HtmlParser<'a> {
534571
let has_prefix = !prefix.is_empty();
535572
let full_name =
536573
if has_prefix { format!(":{}:{}", prefix, name) } else { name.to_string() };
537-
(full_name, name.to_string(), has_prefix)
574+
(full_name, name.to_string(), has_prefix, None, None)
538575
};
539576

540577
// Check if we need to auto-close the current element (HTML5 optional end tags)
@@ -596,6 +633,8 @@ impl<'a> HtmlParser<'a> {
596633

597634
let element = HtmlElement {
598635
name: Atom::from_in(tag_name.clone(), self.allocator),
636+
component_prefix: component_prefix.map(|p| Atom::from_in(p, self.allocator)),
637+
component_tag_name: component_tag_name.map(|t| Atom::from_in(t, self.allocator)),
599638
attrs,
600639
directives,
601640
children: Vec::new_in(self.allocator),
@@ -1071,6 +1110,8 @@ impl<'a> HtmlParser<'a> {
10711110
// Note: is_self_closing is false because this is an incomplete tag, not explicitly self-closing
10721111
let element = HtmlElement {
10731112
name: Atom::from_in(tag_name.clone(), self.allocator),
1113+
component_prefix: None,
1114+
component_tag_name: None,
10741115
attrs: Vec::new_in(self.allocator),
10751116
directives: Vec::new_in(self.allocator),
10761117
children: Vec::new_in(self.allocator),

crates/oxc_angular_compiler/src/parser/html/whitespace.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,8 @@ impl<'a> WhitespaceVisitor<'a> {
335335
fn clone_element_shallow(&self, el: &HtmlElement<'a>) -> HtmlElement<'a> {
336336
HtmlElement {
337337
name: el.name.clone(),
338+
component_prefix: el.component_prefix.clone(),
339+
component_tag_name: el.component_tag_name.clone(),
338340
attrs: self.clone_attributes(&el.attrs),
339341
directives: self.clone_directives(&el.directives),
340342
children: self.clone_children(&el.children),
@@ -510,6 +512,8 @@ impl<'a> WhitespaceVisitor<'a> {
510512
return Some(HtmlNode::Element(Box::new_in(
511513
HtmlElement {
512514
name: element.name.clone(),
515+
component_prefix: element.component_prefix.clone(),
516+
component_tag_name: element.component_tag_name.clone(),
513517
attrs,
514518
directives: self.clone_directives(&element.directives),
515519
children: self.clone_children(&element.children),
@@ -529,6 +533,8 @@ impl<'a> WhitespaceVisitor<'a> {
529533
Some(HtmlNode::Element(Box::new_in(
530534
HtmlElement {
531535
name: element.name.clone(),
536+
component_prefix: element.component_prefix.clone(),
537+
component_tag_name: element.component_tag_name.clone(),
532538
attrs: self.clone_attributes(&element.attrs),
533539
directives: self.clone_directives(&element.directives),
534540
children,

0 commit comments

Comments
 (0)