use std::sync::LazyLock;
use super::shared::{WRAPPER, count_newlines};
const INTERP_PLACEHOLDER: &str = "fallowinterp";
static CSS_IN_JS_TAG_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
crate::static_regex(
r"(?:\bstyled\.[A-Za-z_$][A-Za-z0-9_$]*|\bstyled\([^()`]*\)|\bcss|\bkeyframes|\bcreateGlobalStyle|\binjectGlobal)\s*`",
)
});
#[must_use]
pub fn css_in_js_virtual_stylesheet(source: &str) -> Option<String> {
if !source.contains('`') {
return None;
}
let bytes = source.as_bytes();
let mut out = String::new();
let mut current_line: usize = 1;
let mut found = false;
let mut search_from = 0;
while let Some(m) = CSS_IN_JS_TAG_RE.find_at(source, search_from) {
let backtick = m.end() - 1;
let Some((body, after)) = scan_template_body(bytes, backtick) else {
break;
};
let body_start = backtick + 1;
let block_line = 1 + count_newlines(&source[..body_start]);
while current_line < block_line {
out.push('\n');
current_line += 1;
}
out.push_str(WRAPPER);
out.push('{');
out.push_str(&body);
out.push('}');
current_line += count_newlines(&body);
found = true;
search_from = after;
}
found.then_some(out)
}
fn scan_template_body(bytes: &[u8], open: usize) -> Option<(String, usize)> {
let mut out: Vec<u8> = Vec::new();
let mut i = open + 1;
while i < bytes.len() {
match bytes[i] {
b'\\' => {
out.push(b'\\');
if i + 1 < bytes.len() {
out.push(bytes[i + 1]);
i += 2;
} else {
i += 1;
}
}
b'`' => return Some((String::from_utf8(out).unwrap_or_default(), i + 1)),
b'$' if i + 1 < bytes.len() && bytes[i + 1] == b'{' => {
let interp_end = scan_interpolation(bytes, i + 2)?;
let newlines =
count_newlines(std::str::from_utf8(&bytes[i..interp_end]).unwrap_or(""));
out.extend_from_slice(INTERP_PLACEHOLDER.as_bytes());
out.extend(std::iter::repeat_n(b'\n', newlines));
i = interp_end;
}
b => {
out.push(b);
i += 1;
}
}
}
None
}
fn scan_interpolation(bytes: &[u8], start: usize) -> Option<usize> {
let mut depth: usize = 1;
let mut i = start;
while i < bytes.len() {
match bytes[i] {
b'{' => {
depth += 1;
i += 1;
}
b'}' => {
depth -= 1;
i += 1;
if depth == 0 {
return Some(i);
}
}
b'`' => {
let (_, after) = scan_template_body(bytes, i)?;
i = after;
}
b'\'' | b'"' => {
i = skip_string(bytes, i)?;
}
b'\\' => i = i.saturating_add(2),
_ => i += 1,
}
}
None
}
fn skip_string(bytes: &[u8], open: usize) -> Option<usize> {
let quote = bytes[open];
let mut i = open + 1;
while i < bytes.len() {
match bytes[i] {
b'\\' => i = i.saturating_add(2),
b if b == quote => return Some(i + 1),
_ => i += 1,
}
}
None
}
#[cfg(all(test, not(miri)))]
mod tests {
use super::*;
use crate::compute_css_analytics;
#[test]
fn preserves_multibyte_utf8_in_lifted_body() {
let src = "const T = styled.div`\n\
content: \"café 日本 €\";\n\
font-family: \"Ñoño\";\n\
`;\n";
let vcss = css_in_js_virtual_stylesheet(src).expect("has a styled template");
assert!(
vcss.contains("café 日本 €"),
"multibyte content preserved: {vcss:?}"
);
assert!(
vcss.contains("Ñoño"),
"multibyte font-family preserved: {vcss:?}"
);
assert!(compute_css_analytics(&vcss).is_some(), "lifted CSS parses");
}
#[test]
fn lifts_styled_component_body_to_parseable_css() {
let src = "import styled from 'styled-components';\n\
export const Button = styled.button`\n\
color: white;\n\
padding: 8px 16px;\n\
`;\n";
let vcss = css_in_js_virtual_stylesheet(src).expect("has a styled template");
let analytics = compute_css_analytics(&vcss).expect("masked CSS must parse, not None");
assert!(
analytics.total_declarations >= 2,
"styled body declarations should be counted: {analytics:?}"
);
}
#[test]
fn none_without_any_css_in_js_template() {
assert!(css_in_js_virtual_stylesheet("const x = 1; function f() {}").is_none());
assert!(css_in_js_virtual_stylesheet("const s = `hello ${name}`;").is_none());
}
#[test]
fn interpolation_heavy_template_does_not_return_none_or_garble() {
let src = "const T = styled.div`\n\
color: ${theme.primary};\n\
padding: ${y}px;\n\
${mixin};\n\
margin: ${a} ${b};\n\
`;\n";
let vcss = css_in_js_virtual_stylesheet(src).expect("has a styled template");
let analytics =
compute_css_analytics(&vcss).expect("interpolation-masked CSS must parse, not None");
assert!(
analytics.important_declarations == 0,
"masking must not invent !important: {analytics:?}"
);
}
#[test]
fn emotion_css_and_keyframes_tags_are_lifted() {
let src = "import { css, keyframes } from '@emotion/react';\n\
const fade = keyframes`\n\
from { opacity: 0; }\n\
to { opacity: 1; }\n\
`;\n\
const box = css`\n\
display: flex;\n\
gap: 8px;\n\
`;\n";
let vcss = css_in_js_virtual_stylesheet(src).expect("has css/keyframes templates");
let analytics = compute_css_analytics(&vcss).expect("must parse");
assert!(
analytics.rule_count >= 1,
"rules should be counted: {analytics:?}"
);
}
#[test]
fn styled_call_form_is_lifted() {
let src = "const Primary = styled(Button)`\n\
font-weight: bold;\n\
`;\n";
let vcss = css_in_js_virtual_stylesheet(src).expect("styled(Component) is lifted");
assert!(vcss.contains("font-weight"), "vcss={vcss:?}");
}
#[test]
fn line_numbers_map_back_to_source() {
let src = "import styled from 'styled-components';\n\
\n\
const A = styled.div`\n\
color: red;\n\
`;\n";
let vcss = css_in_js_virtual_stylesheet(src).expect("has a template");
let color_pos = vcss.find("color").expect("color present");
let vcss_line = 1 + vcss[..color_pos].bytes().filter(|&b| b == b'\n').count();
let src_color = src.find("color: red").unwrap();
let src_line = 1 + src[..src_color].bytes().filter(|&b| b == b'\n').count();
assert_eq!(vcss_line, src_line, "vcss={vcss:?}");
}
#[test]
fn nested_template_in_interpolation_does_not_break_extent() {
let src = "const A = styled.div`\n\
color: ${(p) => css`color: ${p.c}`};\n\
border: 1px solid black;\n\
`;\n";
let vcss = css_in_js_virtual_stylesheet(src).expect("has a template");
assert!(
vcss.contains("border"),
"outer template extent must include the post-interpolation decl: {vcss:?}"
);
}
}