Skip to content

Commit cec9ef8

Browse files
fix(styles): treat newlines/tabs as CSS descendant combinators in encapsulation (#175)
1 parent 3a9605a commit cec9ef8

File tree

2 files changed

+72
-3
lines changed

2 files changed

+72
-3
lines changed

crates/oxc_angular_compiler/src/styles/encapsulation.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2255,7 +2255,9 @@ fn split_by_combinators(selector: &str) -> Vec<(&str, &str)> {
22552255
')' => paren_depth = paren_depth.saturating_sub(1),
22562256
'[' => bracket_depth += 1,
22572257
']' => bracket_depth = bracket_depth.saturating_sub(1),
2258-
' ' | '>' | '+' | '~' if paren_depth == 0 && bracket_depth == 0 => {
2258+
' ' | '\n' | '\t' | '\r' | '>' | '+' | '~'
2259+
if paren_depth == 0 && bracket_depth == 0 =>
2260+
{
22592261
// A space following an escaped hex value and followed by another hex character
22602262
// (ie: ".\fc ber" for ".über") is not a separator between 2 selectors
22612263
// 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)> {
22762278
// Collect the combinator (may include spaces around it)
22772279
let combinator_start = i;
22782280
while i < chars.len()
2279-
&& (chars[i] == ' ' || chars[i] == '>' || chars[i] == '+' || chars[i] == '~')
2281+
&& (chars[i] == ' '
2282+
|| chars[i] == '\n'
2283+
|| chars[i] == '\t'
2284+
|| chars[i] == '\r'
2285+
|| chars[i] == '>'
2286+
|| chars[i] == '+'
2287+
|| chars[i] == '~')
22802288
{
22812289
i += 1;
22822290
}
@@ -2420,7 +2428,15 @@ fn scope_after_host_with_context(selector: &str, ctx: &mut ScopingContext) -> St
24202428
// First part (pseudo-selector attached to host) - don't scope
24212429
scoped_after.push_str(part);
24222430
if !combinator.is_empty()
2423-
&& combinator.chars().any(|c| c == ' ' || c == '>' || c == '+' || c == '~')
2431+
&& combinator.chars().any(|c| {
2432+
c == ' '
2433+
|| c == '\n'
2434+
|| c == '\t'
2435+
|| c == '\r'
2436+
|| c == '>'
2437+
|| c == '+'
2438+
|| c == '~'
2439+
})
24242440
{
24252441
found_combinator = true;
24262442
}

crates/oxc_angular_compiler/tests/shadow_css_test.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,3 +911,56 @@ fn test_sidebar_row_layout_full_css_regression() {
911911
println!("Result length: {}", result.len());
912912
assert!(!result.is_empty());
913913
}
914+
915+
// ============================================================================
916+
// Regression: CSS comments before first selector must not break scoping
917+
// ============================================================================
918+
919+
#[test]
920+
fn test_scope_first_selector_after_comment_with_space() {
921+
// Comment followed by space then selector
922+
let css = "/* comment */ .foo { color: red; }";
923+
let expected = ".foo[contenta] { color: red; }";
924+
assert_css_eq!(shim(css, "contenta"), expected);
925+
}
926+
927+
#[test]
928+
fn test_scope_first_selector_after_comment_with_newline() {
929+
// Comment followed by newline then selector (the SCSS @import case)
930+
let css = "/* comment */\n.container { border-radius: 2px; }\n.container .tabs-group { width: 100%; }";
931+
let expected = ".container[contenta] { border-radius: 2px; }\n.container[contenta] .tabs-group[contenta] { width: 100%; }";
932+
assert_css_eq!(shim(css, "contenta"), expected);
933+
}
934+
935+
#[test]
936+
fn test_scope_first_selector_after_multiline_comment() {
937+
// Multi-line comment followed by selector
938+
let css = "/* multi\nline\ncomment */\n.root { padding: 16px; }\n.root .child { color: red; }";
939+
let expected =
940+
".root[contenta] { padding: 16px; }\n.root[contenta] .child[contenta] { color: red; }";
941+
assert_css_eq!(shim(css, "contenta"), expected);
942+
}
943+
944+
#[test]
945+
fn test_scope_first_selector_after_multiple_comments() {
946+
// Multiple comments before first selector
947+
let css = "/* comment 1 */ /* comment 2 */ .foo { color: red; }";
948+
let expected = ".foo[contenta] { color: red; }";
949+
assert_css_eq!(shim(css, "contenta"), expected);
950+
}
951+
952+
#[test]
953+
fn test_newline_as_descendant_combinator() {
954+
// Newline between selectors is a valid CSS descendant combinator
955+
let css = ".foo\n.bar { color: red; }";
956+
let expected = ".foo[contenta] .bar[contenta] { color: red; }";
957+
assert_css_eq!(shim(css, "contenta"), expected);
958+
}
959+
960+
#[test]
961+
fn test_host_pseudo_with_newline_combinator() {
962+
// :host with pseudo-selector followed by newline combinator to child
963+
let css = ":host(:hover)\n.child { color: red; }";
964+
let expected = "[hosta]:hover .child[contenta] { color: red; }";
965+
assert_css_eq!(shim_with_host(css, "contenta", "hosta"), expected);
966+
}

0 commit comments

Comments
 (0)