diff --git a/crates/oxc_angular_compiler/src/styles/encapsulation.rs b/crates/oxc_angular_compiler/src/styles/encapsulation.rs index 17e1ed6c9..5bb92883b 100644 --- a/crates/oxc_angular_compiler/src/styles/encapsulation.rs +++ b/crates/oxc_angular_compiler/src/styles/encapsulation.rs @@ -2255,7 +2255,9 @@ fn split_by_combinators(selector: &str) -> Vec<(&str, &str)> { ')' => paren_depth = paren_depth.saturating_sub(1), '[' => bracket_depth += 1, ']' => bracket_depth = bracket_depth.saturating_sub(1), - ' ' | '>' | '+' | '~' if paren_depth == 0 && bracket_depth == 0 => { + ' ' | '\n' | '\t' | '\r' | '>' | '+' | '~' + if paren_depth == 0 && bracket_depth == 0 => + { // A space following an escaped hex value and followed by another hex character // (ie: ".\fc ber" for ".über") is not a separator between 2 selectors // Check: if the part ends with an escape placeholder AND next char is hex @@ -2276,7 +2278,13 @@ fn split_by_combinators(selector: &str) -> Vec<(&str, &str)> { // Collect the combinator (may include spaces around it) let combinator_start = i; while i < chars.len() - && (chars[i] == ' ' || chars[i] == '>' || chars[i] == '+' || chars[i] == '~') + && (chars[i] == ' ' + || chars[i] == '\n' + || chars[i] == '\t' + || chars[i] == '\r' + || chars[i] == '>' + || chars[i] == '+' + || chars[i] == '~') { i += 1; } @@ -2420,7 +2428,15 @@ fn scope_after_host_with_context(selector: &str, ctx: &mut ScopingContext) -> St // First part (pseudo-selector attached to host) - don't scope scoped_after.push_str(part); if !combinator.is_empty() - && combinator.chars().any(|c| c == ' ' || c == '>' || c == '+' || c == '~') + && combinator.chars().any(|c| { + c == ' ' + || c == '\n' + || c == '\t' + || c == '\r' + || c == '>' + || c == '+' + || c == '~' + }) { found_combinator = true; } diff --git a/crates/oxc_angular_compiler/tests/shadow_css_test.rs b/crates/oxc_angular_compiler/tests/shadow_css_test.rs index 23ee7df72..a1922393c 100644 --- a/crates/oxc_angular_compiler/tests/shadow_css_test.rs +++ b/crates/oxc_angular_compiler/tests/shadow_css_test.rs @@ -911,3 +911,56 @@ fn test_sidebar_row_layout_full_css_regression() { println!("Result length: {}", result.len()); assert!(!result.is_empty()); } + +// ============================================================================ +// Regression: CSS comments before first selector must not break scoping +// ============================================================================ + +#[test] +fn test_scope_first_selector_after_comment_with_space() { + // Comment followed by space then selector + let css = "/* comment */ .foo { color: red; }"; + let expected = ".foo[contenta] { color: red; }"; + assert_css_eq!(shim(css, "contenta"), expected); +} + +#[test] +fn test_scope_first_selector_after_comment_with_newline() { + // Comment followed by newline then selector (the SCSS @import case) + let css = "/* comment */\n.container { border-radius: 2px; }\n.container .tabs-group { width: 100%; }"; + let expected = ".container[contenta] { border-radius: 2px; }\n.container[contenta] .tabs-group[contenta] { width: 100%; }"; + assert_css_eq!(shim(css, "contenta"), expected); +} + +#[test] +fn test_scope_first_selector_after_multiline_comment() { + // Multi-line comment followed by selector + let css = "/* multi\nline\ncomment */\n.root { padding: 16px; }\n.root .child { color: red; }"; + let expected = + ".root[contenta] { padding: 16px; }\n.root[contenta] .child[contenta] { color: red; }"; + assert_css_eq!(shim(css, "contenta"), expected); +} + +#[test] +fn test_scope_first_selector_after_multiple_comments() { + // Multiple comments before first selector + let css = "/* comment 1 */ /* comment 2 */ .foo { color: red; }"; + let expected = ".foo[contenta] { color: red; }"; + assert_css_eq!(shim(css, "contenta"), expected); +} + +#[test] +fn test_newline_as_descendant_combinator() { + // Newline between selectors is a valid CSS descendant combinator + let css = ".foo\n.bar { color: red; }"; + let expected = ".foo[contenta] .bar[contenta] { color: red; }"; + assert_css_eq!(shim(css, "contenta"), expected); +} + +#[test] +fn test_host_pseudo_with_newline_combinator() { + // :host with pseudo-selector followed by newline combinator to child + let css = ":host(:hover)\n.child { color: red; }"; + let expected = "[hosta]:hover .child[contenta] { color: red; }"; + assert_css_eq!(shim_with_host(css, "contenta", "hosta"), expected); +}