From 5c4bf5464f3899d5f758cb8fc8b4ecd000c663fd Mon Sep 17 00:00:00 2001 From: LongYinan Date: Tue, 24 Feb 2026 17:08:41 +0800 Subject: [PATCH] fix(angular): use regex-based else-if pattern matching to match Angular's ELSE_IF_PATTERN Angular uses the regex `/^else[^\S\r\n]+if/` to detect "else if" connected blocks, which means block names like "else ifx" also match as chained else-if branches. Our parser was using exact string matching ("else if"), causing names like "else ifx" to fall through to the default BlockType::If and emit two independent conditionals instead of a single chained one. Fix by reusing `is_else_if_pattern()` (made public) in the parser's block type classification, matching Angular's regex-based connected-block detection. Co-Authored-By: Claude Opus 4.6 --- .../src/parser/html/parser.rs | 7 ++++++- .../src/transform/control_flow.rs | 8 ++++++-- .../tests/r3_template_transform_test.rs | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/crates/oxc_angular_compiler/src/parser/html/parser.rs b/crates/oxc_angular_compiler/src/parser/html/parser.rs index 1c1ef11ea..df1fb36f7 100644 --- a/crates/oxc_angular_compiler/src/parser/html/parser.rs +++ b/crates/oxc_angular_compiler/src/parser/html/parser.rs @@ -15,6 +15,7 @@ use crate::ast::html::{ InterpolatedToken, InterpolatedTokenType, }; use crate::parser::expression::BindingParser; +use crate::transform::control_flow::is_else_if_pattern; use crate::util::{ParseError, ParseLocation, ParseSourceFile, ParseSourceSpan}; use super::entities::decode_entities_in_string; @@ -1501,7 +1502,11 @@ impl<'a> HtmlParser<'a> { let block_type = match name.as_str() { "if" => BlockType::If, "else" => BlockType::Else, - "else if" => BlockType::ElseIf, + // Match Angular's ELSE_IF_PATTERN: /^else[^\S\r\n]+if/ + // Any block name starting with "else " followed by "if" (e.g. "else if", + // "else ifx") is classified as ElseIf, matching Angular's regex-based + // connected-block detection. + _ if is_else_if_pattern(&name) => BlockType::ElseIf, "for" => BlockType::For, "empty" => BlockType::Empty, "switch" => BlockType::Switch, diff --git a/crates/oxc_angular_compiler/src/transform/control_flow.rs b/crates/oxc_angular_compiler/src/transform/control_flow.rs index b616a6e08..d9efa0c84 100644 --- a/crates/oxc_angular_compiler/src/transform/control_flow.rs +++ b/crates/oxc_angular_compiler/src/transform/control_flow.rs @@ -36,8 +36,12 @@ fn parse_as_alias(s: &str) -> Option<&str> { Some(after_as.trim_start()) } -/// Check if string matches `^else\s+if` pattern. -fn is_else_if_pattern(s: &str) -> bool { +/// Check if string matches Angular's `ELSE_IF_PATTERN`: `/^else[^\S\r\n]+if/`. +/// +/// Any name starting with "else" followed by at least one whitespace character +/// and then "if" is treated as an else-if block. This means names like +/// "else ifx" also match, which is intentional to mirror Angular's behavior. +pub fn is_else_if_pattern(s: &str) -> bool { if !s.starts_with("else") { return false; } diff --git a/crates/oxc_angular_compiler/tests/r3_template_transform_test.rs b/crates/oxc_angular_compiler/tests/r3_template_transform_test.rs index 386d22654..de6102647 100644 --- a/crates/oxc_angular_compiler/tests/r3_template_transform_test.rs +++ b/crates/oxc_angular_compiler/tests/r3_template_transform_test.rs @@ -1070,6 +1070,21 @@ mod if_blocks { assert_eq!(result[1], h!["IfBlockBranch", "cond1"]); assert_eq!(result[3], h!["IfBlockBranch", "cond2"]); } + + /// Angular uses the regex `/^else[^\S\r\n]+if/` to detect "else if" blocks, + /// which means block names like "else ifx" still match as connected else-if + /// branches. Our parser must replicate this behavior to avoid emitting two + /// independent conditionals instead of a single chained one. + #[test] + fn should_treat_else_if_prefix_as_connected_block() { + // "else ifx" should still be chained as a connected else-if branch, + // matching Angular's regex-based ELSE_IF_PATTERN: /^else[^\S\r\n]+if/ + let result = humanize_ignore_errors("@if (cond1) { a } @else ifx (cond2) { b }"); + // Should produce a single IfBlock with two branches, not two independent blocks + assert_eq!(result[0], h!["IfBlock"]); + assert_eq!(result[1], h!["IfBlockBranch", "cond1"]); + assert_eq!(result[3], h!["IfBlockBranch", "cond2"]); + } } // ============================================================================