#[cfg(feature = "native")]
use lightningcss::printer::PrinterOptions;
#[cfg(feature = "native")]
use lightningcss::stylesheet::{ParserFlags, ParserOptions, StyleSheet};
#[cfg(feature = "native")]
use lightningcss::targets::{Browsers, Targets};
use serde::{Deserialize, Serialize};
use vize_carton::{Bump, BumpVec};
use crate::types::SfcStyleBlock;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CssCompileOptions {
#[serde(default)]
pub scope_id: Option<String>,
#[serde(default)]
pub scoped: bool,
#[serde(default)]
pub minify: bool,
#[serde(default)]
pub source_map: bool,
#[serde(default)]
pub targets: Option<CssTargets>,
#[serde(default)]
pub filename: Option<String>,
#[serde(default)]
pub custom_media: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CssTargets {
#[serde(default)]
pub chrome: Option<u32>,
#[serde(default)]
pub firefox: Option<u32>,
#[serde(default)]
pub safari: Option<u32>,
#[serde(default)]
pub edge: Option<u32>,
#[serde(default)]
pub ios: Option<u32>,
#[serde(default)]
pub android: Option<u32>,
}
#[cfg(feature = "native")]
impl CssTargets {
fn to_lightningcss_targets(&self) -> Targets {
let mut browsers = Browsers::default();
if let Some(v) = self.chrome {
browsers.chrome = Some(version_to_u32(v));
}
if let Some(v) = self.firefox {
browsers.firefox = Some(version_to_u32(v));
}
if let Some(v) = self.safari {
browsers.safari = Some(version_to_u32(v));
}
if let Some(v) = self.edge {
browsers.edge = Some(version_to_u32(v));
}
if let Some(v) = self.ios {
browsers.ios_saf = Some(version_to_u32(v));
}
if let Some(v) = self.android {
browsers.android = Some(version_to_u32(v));
}
Targets::from(browsers)
}
}
#[cfg(feature = "native")]
fn version_to_u32(major: u32) -> u32 {
major << 16
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CssCompileResult {
pub code: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub map: Option<String>,
#[serde(default)]
pub css_vars: Vec<String>,
#[serde(default)]
pub errors: Vec<String>,
#[serde(default)]
pub warnings: Vec<String>,
}
#[cfg(feature = "native")]
pub fn compile_css(css: &str, options: &CssCompileOptions) -> CssCompileResult {
let bump = Bump::new();
let filename = options.filename.as_deref().unwrap_or("style.css");
let (processed_css, css_vars) = extract_and_transform_v_bind(&bump, css);
let scoped_css = if options.scoped {
if let Some(ref scope_id) = options.scope_id {
apply_scoped_css(&bump, processed_css, scope_id)
} else {
processed_css
}
} else {
processed_css
};
let targets = options
.targets
.as_ref()
.map(|t| t.to_lightningcss_targets())
.unwrap_or_default();
let (code, errors) = compile_css_internal(
scoped_css,
filename,
options.minify,
targets,
options.custom_media,
);
CssCompileResult {
code,
map: None,
css_vars,
errors,
warnings: vec![],
}
}
#[cfg(not(feature = "native"))]
pub fn compile_css(css: &str, options: &CssCompileOptions) -> CssCompileResult {
let bump = Bump::new();
let (processed_css, css_vars) = extract_and_transform_v_bind(&bump, css);
let scoped_css = if options.scoped {
if let Some(ref scope_id) = options.scope_id {
apply_scoped_css(&bump, processed_css, scope_id)
} else {
processed_css
}
} else {
processed_css
};
CssCompileResult {
code: scoped_css.to_string(),
map: None,
css_vars,
errors: vec![],
warnings: vec![],
}
}
#[cfg(feature = "native")]
fn compile_css_internal(
css: &str,
filename: &str,
minify: bool,
targets: Targets,
custom_media: bool,
) -> (String, Vec<String>) {
let mut flags = ParserFlags::NESTING | ParserFlags::DEEP_SELECTOR_COMBINATOR;
if custom_media {
flags |= ParserFlags::CUSTOM_MEDIA;
}
let parser_options = ParserOptions {
filename: filename.to_string(),
flags,
..Default::default()
};
let mut stylesheet = match StyleSheet::parse(css, parser_options) {
Ok(ss) => ss,
Err(e) => {
let mut errors = Vec::with_capacity(1);
let mut message = String::from("CSS parse error: ");
message.push_str(&e.to_string());
errors.push(message);
return (css.to_string(), errors);
}
};
if minify {
if let Err(e) = stylesheet.minify(lightningcss::stylesheet::MinifyOptions {
targets,
..Default::default()
}) {
let mut errors = Vec::with_capacity(1);
let mut message = String::from("CSS minify error: ");
use std::fmt::Write as _;
let _ = write!(&mut message, "{:?}", e);
errors.push(message);
return (css.to_string(), errors);
}
}
let printer_options = PrinterOptions {
minify,
targets,
..Default::default()
};
match stylesheet.to_css(printer_options) {
Ok(result) => (result.code, vec![]),
Err(e) => {
let mut errors = Vec::with_capacity(1);
let mut message = String::from("CSS print error: ");
use std::fmt::Write as _;
let _ = write!(&mut message, "{:?}", e);
errors.push(message);
(css.to_string(), errors)
}
}
}
pub fn compile_style_block(style: &SfcStyleBlock, options: &CssCompileOptions) -> CssCompileResult {
let mut opts = options.clone();
opts.scoped = style.scoped || opts.scoped;
compile_css(&style.content, &opts)
}
fn extract_and_transform_v_bind<'a>(bump: &'a Bump, css: &str) -> (&'a str, Vec<String>) {
let css_bytes = css.as_bytes();
let mut vars = Vec::new();
let mut result = BumpVec::with_capacity_in(css_bytes.len() * 2, bump);
let mut pos = 0;
while pos < css_bytes.len() {
if let Some(rel_pos) = find_bytes(&css_bytes[pos..], b"v-bind(") {
let actual_pos = pos + rel_pos;
let start = actual_pos + 7;
if let Some(end) = find_byte(&css_bytes[start..], b')') {
result.extend_from_slice(&css_bytes[pos..actual_pos]);
let expr_bytes = &css_bytes[start..start + end];
let expr_str = unsafe { std::str::from_utf8_unchecked(expr_bytes) }.trim();
let expr_str = expr_str.trim_matches(|c| c == '"' || c == '\'');
vars.push(expr_str.to_string());
result.extend_from_slice(b"var(--");
write_v_bind_hash(&mut result, expr_str);
result.push(b')');
pos = start + end + 1;
} else {
result.extend_from_slice(&css_bytes[pos..]);
break;
}
} else {
result.extend_from_slice(&css_bytes[pos..]);
break;
}
}
let result_str = unsafe { std::str::from_utf8_unchecked(bump.alloc_slice_copy(&result)) };
(result_str, vars)
}
fn write_v_bind_hash(out: &mut BumpVec<u8>, expr: &str) {
let hash: u32 = expr
.bytes()
.fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32));
write_hex_u32(out, hash);
out.push(b'-');
for b in expr.bytes() {
match b {
b'.' | b'[' | b']' | b'(' | b')' => out.push(b'_'),
_ => out.push(b),
}
}
}
fn write_hex_u32(out: &mut BumpVec<u8>, val: u32) {
const HEX: &[u8; 16] = b"0123456789abcdef";
out.push(HEX[((val >> 28) & 0xF) as usize]);
out.push(HEX[((val >> 24) & 0xF) as usize]);
out.push(HEX[((val >> 20) & 0xF) as usize]);
out.push(HEX[((val >> 16) & 0xF) as usize]);
out.push(HEX[((val >> 12) & 0xF) as usize]);
out.push(HEX[((val >> 8) & 0xF) as usize]);
out.push(HEX[((val >> 4) & 0xF) as usize]);
out.push(HEX[(val & 0xF) as usize]);
}
fn apply_scoped_css<'a>(bump: &'a Bump, css: &str, scope_id: &str) -> &'a str {
let css_bytes = css.as_bytes();
let mut attr_selector = BumpVec::with_capacity_in(scope_id.len() + 2, bump);
attr_selector.push(b'[');
attr_selector.extend_from_slice(scope_id.as_bytes());
attr_selector.push(b']');
let attr_selector = bump.alloc_slice_copy(&attr_selector);
let mut output = BumpVec::with_capacity_in(css_bytes.len() * 2, bump);
let mut chars = css.char_indices().peekable();
let mut in_selector = true;
let mut in_string = false;
let mut string_char = b'"';
let mut in_comment = false;
let mut brace_depth = 0u32;
let mut last_selector_end = 0usize;
let mut in_at_rule = false;
let mut at_rule_depth = 0u32;
let mut pending_keyframes = false;
let mut keyframes_brace_depth: Option<u32> = None;
let mut saved_at_rule_depth: Option<u32> = None;
while let Some((i, c)) = chars.next() {
if in_comment {
if c == '*' {
if let Some(&(_, '/')) = chars.peek() {
chars.next();
in_comment = false;
}
}
continue;
}
if in_string {
if c as u8 == string_char {
let prev_byte = if i > 0 { css_bytes[i - 1] } else { 0 };
if prev_byte != b'\\' {
in_string = false;
}
}
if !in_selector && !in_at_rule {
output.extend_from_slice(c.encode_utf8(&mut [0; 4]).as_bytes());
}
continue;
}
match c {
'"' | '\'' => {
in_string = true;
string_char = c as u8;
if !in_selector && !in_at_rule {
output.push(c as u8);
}
}
'/' => {
if let Some(&(_, '*')) = chars.peek() {
chars.next();
in_comment = true;
} else if !in_selector && !in_at_rule {
output.push(b'/');
}
}
'@' if in_selector => {
in_at_rule = true;
in_selector = false;
let remaining = &css[i + 1..];
pending_keyframes = remaining.starts_with("keyframes")
|| remaining.starts_with("-webkit-keyframes")
|| remaining.starts_with("-moz-keyframes")
|| remaining.starts_with("-o-keyframes");
}
'@' => {
output.push(b'@');
}
';' if in_at_rule => {
let stmt = &css_bytes[last_selector_end..=i];
let stmt_str = unsafe { std::str::from_utf8_unchecked(stmt) }.trim();
output.extend_from_slice(stmt_str.as_bytes());
output.push(b'\n');
in_at_rule = false;
in_selector = true;
pending_keyframes = false;
last_selector_end = i + 1;
}
'{' => {
brace_depth += 1;
if in_at_rule {
in_at_rule = false;
let at_rule_header = &css_bytes[last_selector_end..i];
let at_rule_str =
unsafe { std::str::from_utf8_unchecked(at_rule_header) }.trim();
output.extend_from_slice(at_rule_str.as_bytes());
output.push(b'{');
if pending_keyframes {
saved_at_rule_depth = Some(at_rule_depth);
keyframes_brace_depth = Some(brace_depth);
pending_keyframes = false;
}
at_rule_depth = brace_depth;
in_selector = true;
last_selector_end = i + 1;
} else if keyframes_brace_depth.is_some_and(|d| brace_depth > d) {
let kf_part = &css_bytes[last_selector_end..i];
let kf_str = unsafe { std::str::from_utf8_unchecked(kf_part) }.trim();
output.extend_from_slice(kf_str.as_bytes());
output.push(b'{');
in_selector = false;
last_selector_end = i + 1;
} else if in_selector
&& (brace_depth == 1 || (at_rule_depth > 0 && brace_depth > at_rule_depth))
{
let selector_bytes = &css_bytes[last_selector_end..i];
let selector_str =
unsafe { std::str::from_utf8_unchecked(selector_bytes) }.trim();
scope_selector(&mut output, selector_str, attr_selector);
output.push(b'{');
in_selector = false;
last_selector_end = i + 1;
} else {
output.push(b'{');
}
}
'}' => {
brace_depth = brace_depth.saturating_sub(1);
output.push(b'}');
if keyframes_brace_depth.is_some_and(|d| brace_depth < d) {
keyframes_brace_depth = None;
if let Some(saved) = saved_at_rule_depth.take() {
at_rule_depth = saved;
}
}
if brace_depth == 0 {
in_selector = true;
last_selector_end = i + 1;
at_rule_depth = 0;
} else if at_rule_depth > 0 && brace_depth >= at_rule_depth {
in_selector = true;
last_selector_end = i + 1;
}
}
_ if in_selector || in_at_rule => {
}
_ => {
output.extend_from_slice(c.encode_utf8(&mut [0; 4]).as_bytes());
}
}
}
if in_selector && last_selector_end < css_bytes.len() {
output.extend_from_slice(&css_bytes[last_selector_end..]);
}
unsafe { std::str::from_utf8_unchecked(bump.alloc_slice_copy(&output)) }
}
fn scope_selector(out: &mut BumpVec<u8>, selector: &str, attr_selector: &[u8]) {
if selector.is_empty() {
return;
}
if selector.starts_with('@') {
out.extend_from_slice(selector.as_bytes());
return;
}
let mut first = true;
for part in selector.split(',') {
if !first {
out.extend_from_slice(b", ");
}
first = false;
scope_single_selector(out, part.trim(), attr_selector);
}
}
fn scope_single_selector(out: &mut BumpVec<u8>, selector: &str, attr_selector: &[u8]) {
if selector.is_empty() {
return;
}
if let Some(pos) = selector.find(":deep(") {
transform_deep(out, selector, pos, attr_selector);
return;
}
if let Some(pos) = selector.find(":slotted(") {
transform_slotted(out, selector, pos, attr_selector);
return;
}
if let Some(pos) = selector.find(":global(") {
transform_global(out, selector, pos);
return;
}
let parts: Vec<&str> = selector.split_whitespace().collect();
if parts.is_empty() {
out.extend_from_slice(selector.as_bytes());
return;
}
for (i, part) in parts.iter().enumerate() {
if i > 0 {
out.push(b' ');
}
if i == parts.len() - 1 {
add_scope_to_element(out, part, attr_selector);
} else {
out.extend_from_slice(part.as_bytes());
}
}
}
fn add_scope_to_element(out: &mut BumpVec<u8>, selector: &str, attr_selector: &[u8]) {
let bytes = selector.as_bytes();
if let Some(pseudo_pos) = find_bytes(bytes, b"::") {
out.extend_from_slice(&bytes[..pseudo_pos]);
out.extend_from_slice(attr_selector);
out.extend_from_slice(&bytes[pseudo_pos..]);
return;
}
if let Some(pseudo_pos) = rfind_byte(bytes, b':') {
if pseudo_pos > 0 && bytes[pseudo_pos - 1] != b'\\' {
out.extend_from_slice(&bytes[..pseudo_pos]);
out.extend_from_slice(attr_selector);
out.extend_from_slice(&bytes[pseudo_pos..]);
return;
}
}
out.extend_from_slice(bytes);
out.extend_from_slice(attr_selector);
}
fn transform_deep(out: &mut BumpVec<u8>, selector: &str, start: usize, attr_selector: &[u8]) {
let before = &selector[..start];
let after = &selector[start + 6..];
if let Some(end) = find_matching_paren(after) {
let inner = &after[..end];
let rest = &after[end + 1..];
if before.is_empty() {
out.extend_from_slice(attr_selector);
} else {
out.extend_from_slice(before.trim().as_bytes());
out.extend_from_slice(attr_selector);
}
out.push(b' ');
out.extend_from_slice(inner.as_bytes());
out.extend_from_slice(rest.as_bytes());
} else {
out.extend_from_slice(selector.as_bytes());
}
}
fn transform_slotted(out: &mut BumpVec<u8>, selector: &str, start: usize, attr_selector: &[u8]) {
let after = &selector[start + 9..];
if let Some(end) = find_matching_paren(after) {
let inner = &after.as_bytes()[..end];
let rest = &after.as_bytes()[end + 1..];
out.extend_from_slice(inner);
if attr_selector.last() == Some(&b']') {
out.extend_from_slice(&attr_selector[..attr_selector.len() - 1]);
out.extend_from_slice(b"-s]");
} else {
out.extend_from_slice(attr_selector);
out.extend_from_slice(b"-s");
}
out.extend_from_slice(rest);
} else {
out.extend_from_slice(selector.as_bytes());
}
}
fn transform_global(out: &mut BumpVec<u8>, selector: &str, start: usize) {
let before = &selector[..start];
let after = &selector[start + 8..];
if let Some(end) = find_matching_paren(after) {
let inner = &after[..end];
let rest = &after[end + 1..];
out.extend_from_slice(before.as_bytes());
out.extend_from_slice(inner.as_bytes());
out.extend_from_slice(rest.as_bytes());
} else {
out.extend_from_slice(selector.as_bytes());
}
}
fn find_matching_paren(s: &str) -> Option<usize> {
let mut depth = 1u32;
for (i, c) in s.char_indices() {
match c {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
}
None
}
#[inline]
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack.windows(needle.len()).position(|w| w == needle)
}
#[inline]
fn find_byte(haystack: &[u8], needle: u8) -> Option<usize> {
haystack.iter().position(|&b| b == needle)
}
#[inline]
fn rfind_byte(haystack: &[u8], needle: u8) -> Option<usize> {
haystack.iter().rposition(|&b| b == needle)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compile_simple_css() {
let css = ".foo { color: red; }";
let result = compile_css(css, &CssCompileOptions::default());
assert!(result.errors.is_empty());
assert!(result.code.contains(".foo"));
assert!(result.code.contains("color"));
}
#[test]
fn test_compile_scoped_css() {
let css = ".foo { color: red; }";
let result = compile_css(
css,
&CssCompileOptions {
scoped: true,
scope_id: Some("data-v-123".to_string()),
..Default::default()
},
);
assert!(result.errors.is_empty());
assert!(result.code.contains("[data-v-123]"));
}
#[test]
#[cfg(feature = "native")]
fn test_compile_minified_css() {
let css = ".foo {\n color: red;\n background: blue;\n}";
let result = compile_css(
css,
&CssCompileOptions {
minify: true,
..Default::default()
},
);
assert!(result.errors.is_empty());
assert!(!result.code.contains('\n') || result.code.lines().count() == 1);
}
#[test]
fn test_v_bind_extraction() {
let bump = Bump::new();
let css = ".foo { color: v-bind(color); background: v-bind('bgColor'); }";
let (transformed, vars) = extract_and_transform_v_bind(&bump, css);
assert_eq!(vars.len(), 2);
assert!(vars.contains(&"color".to_string()));
assert!(vars.contains(&"bgColor".to_string()));
assert!(transformed.contains("var(--"));
}
#[test]
fn test_scope_deep() {
let bump = Bump::new();
let mut out = BumpVec::new_in(&bump);
transform_deep(&mut out, ":deep(.child)", 0, b"[data-v-123]");
let result = unsafe { std::str::from_utf8_unchecked(&out) };
assert_eq!(result, "[data-v-123] .child");
}
#[test]
fn test_scope_global() {
let bump = Bump::new();
let mut out = BumpVec::new_in(&bump);
transform_global(&mut out, ":global(.foo)", 0);
let result = unsafe { std::str::from_utf8_unchecked(&out) };
assert_eq!(result, ".foo");
}
#[test]
fn test_scope_slotted() {
let bump = Bump::new();
let mut out = BumpVec::new_in(&bump);
transform_slotted(&mut out, ":slotted(.child)", 0, b"[data-v-123]");
let result = unsafe { std::str::from_utf8_unchecked(&out) };
assert_eq!(result, ".child[data-v-123-s]");
}
#[test]
fn test_scope_slotted_with_pseudo() {
let bump = Bump::new();
let mut out = BumpVec::new_in(&bump);
transform_slotted(&mut out, ":slotted(.child):hover", 0, b"[data-v-abc]");
let result = unsafe { std::str::from_utf8_unchecked(&out) };
assert_eq!(result, ".child[data-v-abc-s]:hover");
}
#[test]
fn test_scope_slotted_complex() {
let bump = Bump::new();
let mut out = BumpVec::new_in(&bump);
transform_slotted(&mut out, ":slotted(div.foo)", 0, b"[data-v-12345678]");
let result = unsafe { std::str::from_utf8_unchecked(&out) };
assert_eq!(result, "div.foo[data-v-12345678-s]");
}
#[test]
fn test_scope_with_pseudo_element() {
let bump = Bump::new();
let mut out = BumpVec::new_in(&bump);
add_scope_to_element(&mut out, ".foo::before", b"[data-v-123]");
let result = unsafe { std::str::from_utf8_unchecked(&out) };
assert_eq!(result, ".foo[data-v-123]::before");
}
#[test]
fn test_scope_with_pseudo_class() {
let bump = Bump::new();
let mut out = BumpVec::new_in(&bump);
add_scope_to_element(&mut out, ".foo:hover", b"[data-v-123]");
let result = unsafe { std::str::from_utf8_unchecked(&out) };
assert_eq!(result, ".foo[data-v-123]:hover");
}
#[test]
#[cfg(feature = "native")]
fn test_compile_with_targets() {
let css = ".foo { display: flex; }";
let result = compile_css(
css,
&CssCompileOptions {
targets: Some(CssTargets {
chrome: Some(80),
..Default::default()
}),
..Default::default()
},
);
assert!(result.errors.is_empty());
assert!(result.code.contains("flex"));
}
#[test]
fn test_scoped_css_with_quoted_font_family() {
let css = ".foo { font-family: 'JetBrains Mono', monospace; }";
let result = compile_css(
css,
&CssCompileOptions {
scoped: true,
scope_id: Some("data-v-123".to_string()),
..Default::default()
},
);
println!("Result: {}", result.code);
assert!(result.errors.is_empty());
assert!(
result.code.contains("JetBrains Mono"),
"Expected font name in: {}",
result.code
);
assert!(result.code.contains("monospace"));
}
#[test]
fn test_apply_scoped_css_at_media() {
let bump = Bump::new();
let css = ".foo { color: red; }\n@media (max-width: 768px) { .foo { color: blue; } }";
let result = apply_scoped_css(&bump, css, "data-v-123");
println!("@media result: {}", result);
assert!(
result.contains("@media (max-width: 768px)"),
"Expected @media query preserved in: {}",
result
);
assert_eq!(
result.matches("[data-v-123]").count(),
2,
"Expected 2 scope attributes in: {}",
result
);
}
#[test]
fn test_apply_scoped_css_at_media_custom_media() {
let bump = Bump::new();
let css = ".a { color: red; }\n@media (--mobile) { .a { font-size: 12px; } }";
let result = apply_scoped_css(&bump, css, "data-v-abc");
println!("Custom media result: {}", result);
assert!(
result.contains("@media (--mobile)"),
"Expected @media (--mobile) preserved in: {}",
result
);
assert_eq!(
result.matches("[data-v-abc]").count(),
2,
"Expected 2 scope attributes in: {}",
result
);
}
#[test]
fn test_apply_scoped_css_multiple_selectors_in_media() {
let bump = Bump::new();
let css = "@media (--mobile) { .a { color: red; } .b { color: blue; } }";
let result = apply_scoped_css(&bump, css, "data-v-xyz");
println!("Multi selector result: {}", result);
assert!(result.contains("@media (--mobile)"));
assert_eq!(
result.matches("[data-v-xyz]").count(),
2,
"Expected 2 scope attributes in: {}",
result
);
}
#[test]
fn test_apply_scoped_css_with_quoted_string() {
let bump = Bump::new();
let css = ".foo { font-family: 'JetBrains Mono', monospace; }";
let result = apply_scoped_css(&bump, css, "data-v-123");
println!("Scoped result: {}", result);
assert!(
result.contains("'JetBrains Mono'"),
"Expected quoted font name in: {}",
result
);
assert!(result.contains("monospace"));
}
#[test]
fn test_apply_scoped_css_at_import() {
let bump = Bump::new();
let css = "@import \"~/assets/styles/custom-media-query.css\";\n\nfooter { width: 100%; }";
let result = apply_scoped_css(&bump, css, "data-v-123");
println!("@import result: {}", result);
assert!(
result.contains("@import \"~/assets/styles/custom-media-query.css\";"),
"Expected @import preserved in: {}",
result
);
assert!(
result.contains("footer[data-v-123]"),
"Expected footer scoped in: {}",
result
);
}
#[test]
fn test_apply_scoped_css_at_import_with_nested_css() {
let bump = Bump::new();
let css = "@import \"custom.css\";\n\nfooter {\n width: 100%;\n @media (--mobile) {\n padding: 1rem;\n }\n}";
let result = apply_scoped_css(&bump, css, "data-v-abc");
println!("@import + nesting result: {}", result);
assert!(
result.contains("@import \"custom.css\";"),
"Expected @import preserved in: {}",
result
);
assert!(
result.contains("footer[data-v-abc]"),
"Expected footer scoped in: {}",
result
);
assert!(
result.contains("@media (--mobile)"),
"Expected nested @media preserved in: {}",
result
);
}
}