Skip to content

Commit fb30ecf

Browse files
committed
Add testcase
1 parent d2cef82 commit fb30ecf

File tree

1 file changed

+112
-8
lines changed

1 file changed

+112
-8
lines changed

libs/extractor/src/css_utils.rs

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -254,16 +254,67 @@ pub fn css_to_style(
254254

255255
if input.contains('{') {
256256
while let Some(start) = input.find('{') {
257-
let rest = &input[start + 1..];
257+
// Check if there are properties before the selector
258+
let before_brace = &input[..start].trim();
259+
260+
// Split by semicolon to find the last part which should be the selector
261+
let parts: Vec<&str> = before_brace.split(';').map(|s| s.trim()).collect();
262+
263+
// Find the selector part (the last part that doesn't contain ':')
264+
// or if all parts contain ':', then the last part is the selector
265+
let (plain_props, selector_part) = if parts.len() > 1 {
266+
// Check if any part doesn't contain ':' (which would be a selector)
267+
let mut selector_idx = parts.len();
268+
for (i, part) in parts.iter().enumerate().rev() {
269+
if !part.contains(':') || part.starts_with('&') || part.starts_with('@') {
270+
selector_idx = i;
271+
break;
272+
}
273+
}
258274

259-
let end = if selector.is_none() {
260-
rest.rfind('}').unwrap()
275+
if selector_idx < parts.len() {
276+
let (props, sel) = parts.split_at(selector_idx);
277+
(props.join(";"), sel.join(";"))
278+
} else {
279+
// All parts contain ':', so treat the last one as selector
280+
let (props, sel) = parts.split_at(parts.len() - 1);
281+
(props.join(";"), sel.join(";"))
282+
}
261283
} else {
262-
rest.find('}').unwrap()
284+
("".to_string(), before_brace.to_string())
263285
};
286+
287+
// Process plain properties if any
288+
if !plain_props.is_empty() {
289+
styles.extend(css_to_style_block(&plain_props, level, selector));
290+
}
291+
292+
let rest = &input[start + 1..];
293+
294+
// Find the matching closing brace by counting braces
295+
let mut brace_count = 1;
296+
let mut end = 0;
297+
for (i, ch) in rest.char_indices() {
298+
match ch {
299+
'{' => brace_count += 1,
300+
'}' => {
301+
brace_count -= 1;
302+
if brace_count == 0 {
303+
end = i;
304+
break;
305+
}
306+
}
307+
_ => {}
308+
}
309+
}
310+
311+
// If we didn't find a matching brace, use the first '}' as fallback
312+
if brace_count > 0 {
313+
end = rest.find('}').unwrap_or(rest.len());
314+
}
264315
let block = &rest[..end];
265316
let sel = &if let Some(StyleSelector::Media { query, .. }) = selector {
266-
let local_sel = input[..start].trim().to_string();
317+
let local_sel = selector_part.trim().to_string();
267318
Some(StyleSelector::Media {
268319
query: query.clone(),
269320
selector: if local_sel == "&" {
@@ -273,13 +324,15 @@ pub fn css_to_style(
273324
},
274325
})
275326
} else {
276-
let sel = input[..start].trim().to_string();
327+
let sel = selector_part.trim().to_string();
277328
if sel.starts_with("@media") {
278329
Some(StyleSelector::Media {
279330
query: sel.replace(" ", "").replace("and(", "and (")["@media".len()..]
280331
.to_string(),
281332
selector: None,
282333
})
334+
} else if sel.is_empty() {
335+
selector.clone()
283336
} else {
284337
Some(StyleSelector::Selector(sel))
285338
}
@@ -289,10 +342,34 @@ pub fn css_to_style(
289342
} else {
290343
css_to_style_block(block, level, sel)
291344
};
292-
let input_end = input.rfind('}').unwrap() + 1;
293345

294-
input = &input[start + end + 2..input_end];
346+
// Find the matching closing brace
347+
let closing_brace_pos = start + 1 + end;
348+
349+
// Process the block
295350
styles.extend(block);
351+
352+
// Update input to continue processing after the closing brace
353+
// Check if there's more content after the closing brace
354+
if closing_brace_pos + 1 < input.len() {
355+
let remaining = &input[closing_brace_pos + 1..].trim();
356+
if !remaining.is_empty() {
357+
// If there's remaining text after the closing brace, process it
358+
// This handles cases like "} color: blue;"
359+
if remaining.contains('{') {
360+
// If it contains '{', continue the loop
361+
input = remaining;
362+
} else {
363+
// If it doesn't contain '{', process it as a block and break
364+
styles.extend(css_to_style_block(remaining, level, selector));
365+
break;
366+
}
367+
} else {
368+
break;
369+
}
370+
} else {
371+
break;
372+
}
296373
}
297374
} else {
298375
styles.extend(css_to_style_block(input, level, selector));
@@ -669,6 +746,33 @@ mod tests {
669746
("background-color", "red", Some(StyleSelector::Selector("&:hover".to_string()))),
670747
]
671748
)]
749+
#[case(
750+
"`background-color: red; &:hover { background-color: red; } color: blue;`",
751+
vec![
752+
("background-color", "red", None),
753+
("background-color", "red", Some(StyleSelector::Selector("&:hover".to_string()))),
754+
("color", "blue", None),
755+
]
756+
)]
757+
#[case(
758+
"`background-color: red; &:hover { background-color: red; } color: blue; &:active { background-color: blue; }`",
759+
vec![
760+
("background-color", "red", None),
761+
("background-color", "red", Some(StyleSelector::Selector("&:hover".to_string()))),
762+
("color", "blue", None),
763+
("background-color", "blue", Some(StyleSelector::Selector("&:active".to_string()))),
764+
]
765+
)]
766+
#[case(
767+
"`background-color: red; &:hover { background-color: red; } color: blue; &:active { background-color: blue; } transform: rotate(90deg);`",
768+
vec![
769+
("background-color", "red", None),
770+
("background-color", "red", Some(StyleSelector::Selector("&:hover".to_string()))),
771+
("color", "blue", None),
772+
("background-color", "blue", Some(StyleSelector::Selector("&:active".to_string()))),
773+
("transform", "rotate(90deg)", None),
774+
]
775+
)]
672776
fn test_css_to_style_literal(
673777
#[case] input: &str,
674778
#[case] expected: Vec<(&str, &str, Option<StyleSelector>)>,

0 commit comments

Comments
 (0)