Skip to content

Commit ceceb2a

Browse files
committed
Review 1
1 parent 909bda4 commit ceceb2a

29 files changed

+16598
-750
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1272,7 +1272,6 @@ impl<'a> Lexer<'a> {
12721272
len: u32,
12731273
escape_start: u32,
12741274
) -> Result<char, (u32, u32, String)> {
1275-
let _start_pos = self.index;
12761275
let mut value = 0u32;
12771276
let mut count = 0u32;
12781277

crates/oxc_angular_compiler/src/parser/expression/parser.rs

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -469,12 +469,6 @@ impl<'a> Parser<'a> {
469469
let source_end = self.peek().map(|t| t.index).unwrap_or(self.source.len() as u32);
470470

471471
let value_source = &self.source[start as usize..value_end as usize];
472-
let ast_with_source = ASTWithSource {
473-
ast: value,
474-
source: Some(Atom::from_in(value_source, self.allocator)),
475-
location: Atom::from_in("", self.allocator),
476-
absolute_offset: self.absolute_offset + start,
477-
};
478472

479473
let key = self.make_template_binding_identifier(
480474
&template_key.source,
@@ -483,7 +477,20 @@ impl<'a> Parser<'a> {
483477
);
484478
let source_span =
485479
AbsoluteSourceSpan::new(template_key.span.start, self.absolute_offset + source_end);
486-
let expr_binding = ExpressionBinding { source_span, key, value: Some(ast_with_source) };
480+
481+
// Angular expects None for empty values (e.g., `*a=""`)
482+
let expr_value = if value_source.is_empty() {
483+
None
484+
} else {
485+
Some(ASTWithSource {
486+
ast: value,
487+
source: Some(Atom::from_in(value_source, self.allocator)),
488+
location: Atom::from_in("", self.allocator),
489+
absolute_offset: self.absolute_offset + start,
490+
})
491+
};
492+
493+
let expr_binding = ExpressionBinding { source_span, key, value: expr_value };
487494
bindings.push(TemplateBinding::Expression(expr_binding));
488495

489496
// Check for `as` binding after the primary expression (e.g., `*ngIf="cond | async as result"`)
@@ -615,12 +622,9 @@ impl<'a> Parser<'a> {
615622
value_end,
616623
)
617624
} else {
618-
// For bare `let item`, value is `$implicit`
625+
// For bare `let item`, Angular expects no value (None)
619626
// Source span includes trailing space after the variable name
620-
(
621-
Some(self.make_template_binding_identifier("$implicit", key_start, key_start)),
622-
key_end + 1,
623-
)
627+
(None, key_end + 1)
624628
};
625629

626630
// Source span starts from 'let' keyword
@@ -3041,12 +3045,11 @@ mod tests {
30413045
_ => panic!("Expected expression binding"),
30423046
}
30433047

3044-
// Second binding: let item (variable binding with $implicit)
3048+
// Second binding: let item (variable binding with no value - Angular behavior)
30453049
match &result.bindings[1] {
30463050
TemplateBinding::Variable(var) => {
30473051
assert_eq!(var.key.source.as_str(), "item");
3048-
assert!(var.value.is_some());
3049-
assert_eq!(var.value.as_ref().unwrap().source.as_str(), "$implicit");
3052+
assert!(var.value.is_none());
30503053
}
30513054
_ => panic!("Expected variable binding"),
30523055
}
@@ -3083,10 +3086,11 @@ mod tests {
30833086
_ => panic!("Expected expression binding"),
30843087
}
30853088

3086-
// Second: let item ($implicit)
3089+
// Second: let item (no value)
30873090
match &result.bindings[1] {
30883091
TemplateBinding::Variable(var) => {
30893092
assert_eq!(var.key.source.as_str(), "item");
3093+
assert!(var.value.is_none());
30903094
}
30913095
_ => panic!("Expected variable binding"),
30923096
}

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,55 @@ pub fn decode_entity(entity: &str) -> Option<String> {
4545
}
4646
}
4747

48+
/// Decodes all HTML entities in a string.
49+
///
50+
/// For backward compatibility (matching Angular's behavior), this decodes
51+
/// HTML entities that appear in interpolation expressions.
52+
///
53+
/// Pattern: `&entity;` where entity can be:
54+
/// - Named: `amp`, `lt`, `gt`, `quot`, etc.
55+
/// - Decimal: `#123`
56+
/// - Hex: `#x7B` or `#X7B`
57+
pub fn decode_entities_in_string(s: &str) -> String {
58+
let mut result = String::with_capacity(s.len());
59+
let mut chars = s.char_indices().peekable();
60+
61+
while let Some((i, ch)) = chars.next() {
62+
if ch == '&' {
63+
// Look for entity pattern: &...;
64+
let mut entity_end = None;
65+
let remaining = &s[i..];
66+
67+
// Find the semicolon
68+
for (j, c) in remaining.char_indices() {
69+
if c == ';' {
70+
entity_end = Some(j);
71+
break;
72+
}
73+
// Entities shouldn't be too long, and should contain valid chars
74+
if j > 32 || (!c.is_ascii_alphanumeric() && c != '#' && c != '&') {
75+
break;
76+
}
77+
}
78+
79+
if let Some(end) = entity_end {
80+
let entity = &remaining[..=end];
81+
if let Some(decoded) = decode_entity(entity) {
82+
result.push_str(&decoded);
83+
// Skip past the entity in the input
84+
for _ in 0..end {
85+
chars.next();
86+
}
87+
continue;
88+
}
89+
}
90+
}
91+
result.push(ch);
92+
}
93+
94+
result
95+
}
96+
4897
/// Creates the named entities map.
4998
fn create_named_entities() -> FxHashMap<&'static str, &'static str> {
5099
let mut map = FxHashMap::default();

0 commit comments

Comments
 (0)