use std::{
hash::{Hash, Hasher},
iter::once,
};
use rustc_hash::FxHasher;
use serde::Deserialize;
use oxc_allocator::{TakeIn, Vec as ArenaVec};
use oxc_ast::{AstBuilder, NONE, ast::*};
use oxc_data_structures::{inline_string::InlineString, slice_iter::SliceIter};
use oxc_semantic::SymbolId;
use oxc_span::SPAN;
use oxc_traverse::{Ancestor, Traverse};
use crate::{context::TraverseCtx, state::TransformState};
#[derive(Debug, Clone, Deserialize)]
#[serde(default, rename_all = "camelCase", deny_unknown_fields)]
pub struct StyledComponentsOptions {
#[serde(default = "default_as_true")]
pub display_name: bool,
#[serde(default = "default_as_true")]
pub file_name: bool,
#[serde(default = "default_as_true")]
pub ssr: bool,
#[serde(default)]
pub transpile_template_literals: bool,
#[serde(default = "default_as_true")]
pub minify: bool,
#[serde(default = "default_as_true")]
pub css_prop: bool,
#[serde(default)]
pub pure: bool,
#[serde(default)]
pub namespace: Option<String>,
#[serde(default = "default_for_meaningless_file_names")]
pub meaningless_file_names: Vec<String>,
#[serde(default)]
pub top_level_import_paths: Vec<String>,
}
const fn default_as_true() -> bool {
true
}
fn default_for_meaningless_file_names() -> Vec<String> {
vec![String::from("index")]
}
impl Default for StyledComponentsOptions {
fn default() -> Self {
Self {
display_name: true,
file_name: true,
ssr: true,
transpile_template_literals: false,
pure: false,
minify: true,
namespace: None,
css_prop: true,
meaningless_file_names: default_for_meaningless_file_names(),
top_level_import_paths: vec![],
}
}
}
#[derive(Default)]
struct StyledComponentsBinding {
namespace: Option<SymbolId>,
styled: Option<SymbolId>,
helpers: [Option<SymbolId>; 6],
}
impl StyledComponentsBinding {
fn helper_symbol_id(&self, helper: StyledComponentsHelper) -> Option<SymbolId> {
self.helpers[helper as usize]
}
fn set_helper_symbol_id(&mut self, helper: StyledComponentsHelper, symbol_id: SymbolId) {
self.helpers[helper as usize] = Some(symbol_id);
}
}
#[derive(Copy, Clone)]
#[repr(u8)]
enum StyledComponentsHelper {
CreateGlobalStyle = 0,
Css = 1,
Keyframes = 2,
UseTheme = 3,
WithTheme = 4,
InjectGlobal = 5,
}
impl StyledComponentsHelper {
fn from_str(name: &str) -> Option<Self> {
if name == "injectGlobal" { Some(Self::InjectGlobal) } else { Self::pure_from_str(name) }
}
fn pure_from_str(name: &str) -> Option<Self> {
match name {
"createGlobalStyle" => Some(Self::CreateGlobalStyle),
"css" => Some(Self::Css),
"keyframes" => Some(Self::Keyframes),
"useTheme" => Some(Self::UseTheme),
"withTheme" => Some(Self::WithTheme),
_ => None,
}
}
}
pub struct StyledComponents<'a> {
pub options: StyledComponentsOptions,
styled_bindings: StyledComponentsBinding,
component_count: usize,
component_id_prefix: Option<String>,
block_name: Option<Str<'a>>,
}
impl StyledComponents<'_> {
pub fn new(options: StyledComponentsOptions) -> Self {
Self {
options,
styled_bindings: StyledComponentsBinding::default(),
component_id_prefix: None,
component_count: 0,
block_name: None,
}
}
}
impl<'a> Traverse<'a, TransformState<'a>> for StyledComponents<'a> {
fn enter_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
self.collect_styled_bindings(program, ctx);
}
fn enter_variable_declarator(
&mut self,
variable_declarator: &mut VariableDeclarator<'a>,
ctx: &mut TraverseCtx<'a>,
) {
self.handle_pure_annotation(variable_declarator, ctx);
}
#[inline] fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
if matches!(expr, Expression::TaggedTemplateExpression(_)) {
self.transform_tagged_template_expression(expr, ctx);
}
}
fn enter_call_expression(&mut self, call: &mut CallExpression<'a>, ctx: &mut TraverseCtx<'a>) {
if (self.options.display_name || self.options.ssr)
&& !matches!(
ctx.parent(),
Ancestor::CallExpressionCallee(_) | Ancestor::StaticMemberExpressionObject(_) | Ancestor::ComputedMemberExpressionObject(_)
)
{
self.add_display_name_and_component_id(&mut call.callee, ctx);
}
}
}
impl<'a> StyledComponents<'a> {
fn transform_tagged_template_expression(
&mut self,
expr: &mut Expression<'a>,
ctx: &TraverseCtx<'a>,
) {
let Expression::TaggedTemplateExpression(tagged) = expr else {
unreachable!();
};
let is_styled = if self.options.display_name || self.options.ssr {
self.add_display_name_and_component_id(&mut tagged.tag, ctx)
} else {
self.is_styled(&tagged.tag, ctx)
};
if !is_styled
&& !matches!(&tagged.tag, Expression::Identifier(ident) if self.is_helper(ident, ctx))
{
return;
}
if self.options.minify {
minify_template_literal(&mut tagged.quasi, ctx.ast);
}
if self.options.transpile_template_literals {
*expr = Self::transpile_template_literals(tagged, ctx);
}
}
fn handle_pure_annotation(
&self,
declarator: &mut VariableDeclarator<'a>,
ctx: &TraverseCtx<'a>,
) {
if !self.options.pure {
return;
}
if let Some(Expression::CallExpression(call)) = &mut declarator.init
&& (matches!(&call.callee, Expression::Identifier(ident) if self.is_pure_helper(ident, ctx))
|| self.is_styled(&call.callee, ctx))
{
call.pure = true;
}
}
fn transpile_template_literals(
expr: &mut TaggedTemplateExpression<'a>,
ctx: &TraverseCtx<'a>,
) -> Expression<'a> {
let TaggedTemplateExpression {
span,
tag,
quasi: TemplateLiteral { span: quasi_span, quasis, expressions, .. },
type_arguments,
..
} = expr.take_in(ctx.ast);
let quasis_elements = ctx.ast.vec_from_iter(quasis.into_iter().map(|quasi| {
ArrayExpressionElement::from(ctx.ast.expression_string_literal(
quasi.span,
quasi.value.raw,
None,
))
}));
let quasis = Argument::from(ctx.ast.expression_array(quasi_span, quasis_elements));
let arguments =
ctx.ast.vec_from_iter(once(quasis).chain(expressions.into_iter().map(Argument::from)));
ctx.ast.expression_call(span, tag, type_arguments, arguments, false)
}
fn add_display_name_and_component_id(
&mut self,
expr: &mut Expression<'a>,
ctx: &TraverseCtx<'a>,
) -> bool {
if let Some(call) = Self::get_with_config(expr) {
if let Expression::StaticMemberExpression(member) = &call.callee
&& self.is_styled(&member.object, ctx)
&& let Some(Argument::ObjectExpression(object)) = call.arguments.first_mut()
&& !object.properties.iter().any(|prop| {
matches!(prop, ObjectPropertyKind::ObjectProperty(property)
if matches!(&property.key, PropertyKey::StaticIdentifier(ident)
if matches!(ident.name.as_str(), "displayName" | "componentId"))
)
})
{
self.add_properties(&mut object.properties, ctx);
}
} else if self.is_styled(expr, ctx) {
let mut properties = ctx.ast.vec_with_capacity(
usize::from(self.options.display_name) + usize::from(self.options.ssr),
);
self.add_properties(&mut properties, ctx);
let object = ctx.ast.alloc_object_expression(SPAN, properties);
let arguments = ctx.ast.vec1(Argument::ObjectExpression(object));
let object = expr.take_in(ctx.ast);
let property = ctx.ast.identifier_name(SPAN, "withConfig");
let callee =
Expression::from(ctx.ast.member_expression_static(SPAN, object, property, false));
let call = ctx.ast.expression_call(SPAN, callee, NONE, arguments, false);
*expr = call;
} else {
return false;
}
true
}
fn collect_styled_bindings(&mut self, program: &Program<'a>, _ctx: &mut TraverseCtx<'a>) {
for statement in &program.body {
let Statement::ImportDeclaration(import) = &statement else { continue };
let Some(specifiers) = &import.specifiers else { continue };
if !is_valid_styled_component_source(&import.source.value) {
continue;
}
for specifier in specifiers {
match specifier {
ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
let symbol_id = specifier.local.symbol_id();
let imported_name = specifier.imported.name();
match imported_name.as_str() {
"default" | "styled" => {
self.styled_bindings.styled = Some(symbol_id);
}
name => {
if let Some(helper) = StyledComponentsHelper::from_str(name) {
self.styled_bindings.set_helper_symbol_id(helper, symbol_id);
}
}
}
}
ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
self.styled_bindings.styled = Some(specifier.local.symbol_id());
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
self.styled_bindings.namespace = Some(specifier.local.symbol_id());
}
}
}
}
}
fn get_with_config<'b>(expr: &'b mut Expression<'a>) -> Option<&'b mut CallExpression<'a>> {
let mut current = expr;
loop {
match current {
Expression::CallExpression(call) => {
if let Expression::StaticMemberExpression(member) = &call.callee
&& member.property.name == "withConfig"
{
return Some(call);
}
current = &mut call.callee;
}
Expression::StaticMemberExpression(member) => {
current = &mut member.object;
}
_ => return None,
}
}
}
fn add_properties(
&mut self,
properties: &mut ArenaVec<'a, ObjectPropertyKind<'a>>,
ctx: &TraverseCtx<'a>,
) {
if self.options.display_name {
let value = self.get_display_name(ctx);
properties.push(Self::create_object_property("displayName", value, ctx));
}
if self.options.ssr {
let value = self.get_component_id(ctx);
properties.push(Self::create_object_property("componentId", value, ctx));
}
}
fn get_component_name(ctx: &TraverseCtx<'a>) -> Option<Str<'a>> {
let mut assignment_name = None;
for ancestor in ctx.ancestors() {
match ancestor {
Ancestor::AssignmentExpressionRight(assignment) => {
assignment_name = match assignment.left() {
AssignmentTarget::AssignmentTargetIdentifier(ident) => {
Some(ident.name.into())
}
AssignmentTarget::StaticMemberExpression(member) => {
Some(member.property.name.into())
}
_ => return None,
};
}
Ancestor::VariableDeclaratorInit(declarator) => {
return if let BindingPattern::BindingIdentifier(ident) = &declarator.id() {
Some(ident.name.into())
} else {
None
};
}
Ancestor::ObjectPropertyValue(property) => {
return if let PropertyKey::StaticIdentifier(ident) = property.key() {
Some(ident.name.into())
} else {
None
};
}
Ancestor::PropertyDefinitionValue(property) => {
return if let PropertyKey::StaticIdentifier(ident) = property.key() {
Some(ident.name.into())
} else {
None
};
}
_ => {
if ancestor.is_parent_of_statement() {
return assignment_name;
}
}
}
}
unreachable!()
}
fn get_component_id(&mut self, ctx: &TraverseCtx<'a>) -> Str<'a> {
let prefix = if let Some(prefix) = self.component_id_prefix.as_deref() {
prefix
} else {
const HASH_LEN: usize = 6;
const PREFIX_LEN: usize = "sc-".len() + HASH_LEN + "-".len();
const NAMESPACED_PREFIX_LEN: usize = "__".len() + PREFIX_LEN;
let mut prefix = if let Some(namespace) = &self.options.namespace {
let mut prefix = String::with_capacity(namespace.len() + NAMESPACED_PREFIX_LEN);
prefix.extend([namespace, "__"]);
prefix
} else {
String::with_capacity(PREFIX_LEN)
};
prefix.extend(["sc-", Self::get_file_hash(&ctx.state).as_str(), "-"]);
self.component_id_prefix = Some(prefix);
self.component_id_prefix.as_deref().unwrap()
};
let mut buffer = itoa::Buffer::new();
let count = buffer.format(self.component_count);
self.component_count += 1;
ctx.ast.str_from_strs_array([prefix, count])
}
fn get_file_hash(state: &TransformState<'a>) -> InlineString<7, u8> {
#[inline]
fn base36_encode(mut num: u64) -> InlineString<7, u8> {
const BASE36_BYTES: &[u8; 36] = b"abcdefghijklmnopqrstuvwxyz0123456789";
num %= 36_u64.pow(6);
let mut str = InlineString::new();
while num != 0 {
unsafe { str.push_unchecked(BASE36_BYTES[(num % 36) as usize]) };
num /= 36;
}
str
}
let mut hasher = FxHasher::default();
if state.source_path.is_absolute() {
state.source_path.hash(&mut hasher);
} else {
state.source_text.hash(&mut hasher);
}
base36_encode(hasher.finish())
}
fn get_block_name(&mut self, ctx: &TraverseCtx<'a>) -> Option<Str<'a>> {
if !self.options.file_name {
return None;
}
let file_stem = ctx.state.source_path.file_stem().and_then(|stem| stem.to_str())?;
Some(*self.block_name.get_or_insert_with(|| {
let block_name =
if self.options.meaningless_file_names.iter().any(|name| name == file_stem) {
ctx.state
.source_path
.parent()
.and_then(|parent| parent.file_name())
.and_then(|name| name.to_str())
.unwrap_or(file_stem)
} else {
file_stem
};
ctx.ast.str(block_name)
}))
}
fn get_display_name(&mut self, ctx: &TraverseCtx<'a>) -> Str<'a> {
let component_name = Self::get_component_name(ctx);
let Some(block_name) = self.get_block_name(ctx) else {
return component_name.unwrap_or(Str::from(""));
};
if let Some(component_name) = component_name {
if block_name == component_name {
component_name
} else {
ctx.ast.str_from_strs_array([&block_name, "__", &component_name])
}
} else {
block_name
}
}
fn is_styled(&self, callee: &Expression<'a>, ctx: &TraverseCtx<'a>) -> bool {
match callee.without_parentheses() {
Expression::StaticMemberExpression(member) => {
if let Expression::Identifier(ident) = &member.object {
StyledComponentsHelper::from_str(&member.property.name).is_none()
&& Self::is_reference_of_styled(self.styled_bindings.styled, ident, ctx)
} else if let Expression::StaticMemberExpression(static_member) = &member.object {
static_member.property.name == "default"
&& matches!(&static_member.object, Expression::Identifier(ident)
if Self::is_reference_of_styled(self.styled_bindings.namespace, ident, ctx))
} else {
false
}
}
Expression::CallExpression(call) => match &call.callee {
Expression::Identifier(ident) => {
Self::is_reference_of_styled(self.styled_bindings.styled, ident, ctx)
}
Expression::StaticMemberExpression(member) => self.is_styled(&member.object, ctx),
Expression::SequenceExpression(sequence) => {
if let Some(last) = sequence.expressions.last() {
match last {
Expression::Identifier(ident) => Self::is_reference_of_styled(
self.styled_bindings.styled,
ident,
ctx,
),
Expression::StaticMemberExpression(member) => {
self.is_styled(&member.object, ctx)
}
_ => false,
}
} else {
false
}
}
_ => false,
},
_ => false,
}
}
fn is_helper(&self, ident: &IdentifierReference<'a>, ctx: &TraverseCtx<'a>) -> bool {
StyledComponentsHelper::from_str(&ident.name)
.is_some_and(|helper| self.is_specific_helper(ident, helper, ctx))
}
fn is_pure_helper(&self, ident: &IdentifierReference<'a>, ctx: &TraverseCtx<'a>) -> bool {
StyledComponentsHelper::pure_from_str(&ident.name)
.is_some_and(|helper| self.is_specific_helper(ident, helper, ctx))
}
fn is_specific_helper(
&self,
ident: &IdentifierReference<'a>,
helper: StyledComponentsHelper,
ctx: &TraverseCtx<'a>,
) -> bool {
self.styled_bindings.helper_symbol_id(helper).is_some_and(|symbol_id| {
let reference_id = ident.reference_id();
ctx.scoping()
.get_reference(reference_id)
.symbol_id()
.is_some_and(|reference_symbol_id| reference_symbol_id == symbol_id)
})
}
fn is_reference_of_styled(
styled_binding: Option<SymbolId>,
ident: &IdentifierReference<'a>,
ctx: &TraverseCtx<'a>,
) -> bool {
styled_binding.is_some_and(|styled_binding| {
let reference_id = ident.reference_id();
ctx.scoping()
.get_reference(reference_id)
.symbol_id()
.is_some_and(|reference_symbol_id| reference_symbol_id == styled_binding)
})
}
fn create_object_property(
key: &'static str,
value: Str<'a>,
ctx: &TraverseCtx<'a>,
) -> ObjectPropertyKind<'a> {
let key = ctx.ast.property_key_static_identifier(SPAN, key);
let value = ctx.ast.expression_string_literal(SPAN, value, None);
ctx.ast.object_property_kind_object_property(
SPAN,
PropertyKind::Init,
key,
value,
false,
false,
false,
)
}
}
fn is_valid_styled_component_source(source: &str) -> bool {
matches!(
source,
"styled-components"
| "styled-components/no-tags"
| "styled-components/native"
| "styled-components/primitives"
)
}
fn minify_template_literal<'a>(lit: &mut TemplateLiteral<'a>, ast: AstBuilder<'a>) {
const NOT_IN_STRING: u8 = 0;
const REMOVE_SENTINEL: Span = Span::new(u32::MAX, u32::MAX);
debug_assert!(lit.quasis.len() == lit.expressions.len() + 1);
let mut comment_type = None;
let mut string_quote = NOT_IN_STRING;
let mut paren_depth: isize = 0;
let mut delete_some = false;
let mut is_first_output = true;
let mut output = Vec::new();
let quasis = &mut lit.quasis[..];
for quasi_index in 0..quasis.len() {
let mut bytes = quasis[quasi_index].value.raw.as_str().as_bytes();
if quasi_index > 0 {
if let Some(is_block_comment) = comment_type {
quasis[quasi_index].span.start = quasis[quasi_index - 1].span.start;
quasis[quasi_index - 1].span = REMOVE_SENTINEL;
delete_some = true;
let start_index = if is_block_comment {
let Some(pos) = bytes.windows(2).position(|q| q == b"*/") else {
continue;
};
pos + 2 } else {
let Some(pos) = bytes.iter().position(|&b| matches!(b, b'\n' | b'\r')) else {
continue;
};
pos + 1 };
insert_space_if_required(&mut output, is_first_output);
bytes = &bytes[start_index..];
comment_type = None;
} else {
let output_str = unsafe { std::str::from_utf8_unchecked(&output) };
quasis[quasi_index - 1].value.raw = ast.str(output_str);
output.clear();
is_first_output = false;
}
}
let mut i = 0;
while i < bytes.len() {
let cur_byte = bytes[i];
match cur_byte {
b'"' | b'\'' => {
if string_quote == NOT_IN_STRING {
string_quote = cur_byte;
} else if cur_byte == string_quote {
string_quote = NOT_IN_STRING;
}
}
b'\\' => {
if let Some(&next_byte) = bytes.get(i + 1) {
match next_byte {
b'\\' | b'"' | b'\'' => {
output.extend([b'\\', next_byte]);
i += 2;
continue;
}
b'n' | b'r' if string_quote == NOT_IN_STRING => {
insert_space_if_required(&mut output, is_first_output);
i += 2;
continue;
}
_ => {}
}
}
}
_ if string_quote != NOT_IN_STRING => {}
b'(' => paren_depth += 1,
b')' => paren_depth -= 1,
b'/' => {
if let Some(&next_byte) = bytes.get(i + 1) {
match next_byte {
b'*' => {
match bytes.get(i + 2) {
None => {
comment_type = Some(true);
break;
}
Some(&b) if b != b'!' => {
let end_index =
bytes[i + 2..].windows(2).position(|q| q == b"*/");
if let Some(end_index) = end_index {
insert_space_if_required(&mut output, is_first_output);
i += end_index + 4; continue;
}
comment_type = Some(true);
break;
}
_ => {}
}
}
b'/' if paren_depth == 0 && (i == 0 || bytes[i - 1] != b':') => {
let end_index =
bytes[i + 2..].iter().position(|&b| matches!(b, b'\n' | b'\r'));
if let Some(end_index) = end_index {
insert_space_if_required(&mut output, is_first_output);
i += end_index + 3; continue;
}
comment_type = Some(false);
break;
}
_ => {}
}
}
}
_ if cur_byte.is_ascii_whitespace() => {
insert_space_if_required(&mut output, is_first_output);
i += 1;
continue;
}
b'{' | b'}' | b',' | b';' if output.last() == Some(&b' ') => {
output.pop();
}
_ => {}
}
output.push(cur_byte);
i += 1;
}
}
if output.last() == Some(&b' ') {
output.pop();
}
let output_str = unsafe { std::str::from_utf8_unchecked(&output) };
quasis.last_mut().unwrap().value.raw = ast.str(output_str);
if delete_some {
assert!(quasis.len() >= lit.expressions.len());
let mut quasis_iter = quasis.iter();
lit.expressions.retain(|_| {
let quasi = unsafe { quasis_iter.next_unchecked() };
quasi.span != REMOVE_SENTINEL
});
lit.quasis.retain(|quasi| quasi.span != REMOVE_SENTINEL);
}
}
fn insert_space_if_required(output: &mut Vec<u8>, is_first_output: bool) {
if let Some(&last) = output.last() {
if matches!(last, b' ' | b':' | b'{' | b'}' | b',' | b';') {
return;
}
} else {
if is_first_output {
return;
}
}
output.push(b' ');
}
#[cfg(test)]
mod tests {
use super::*;
use oxc_allocator::Allocator;
use oxc_ast::{AstBuilder, ast::TemplateElementValue};
use oxc_span::SPAN;
fn minify_raw(input: &str) -> String {
let allocator = Allocator::default();
let ast = AstBuilder::new(&allocator);
let mut lit = ast.template_literal(
SPAN,
ast.vec1(ast.template_element(
SPAN,
TemplateElementValue { raw: ast.str(input), cooked: Some(ast.str(input)) },
true,
)),
ast.vec(),
);
minify_template_literal(&mut lit, ast);
assert!(lit.quasis.len() == 1);
lit.quasis[0].value.raw.to_string()
}
mod strip_line_comment {
use super::*;
#[test]
fn splits_line_by_potential_comment_starts() {
let actual = minify_raw("abc def//ghi//jkl");
debug_assert_eq!(actual, "abc def");
}
#[test]
fn ignores_comment_markers_inside_strings() {
let actual1 = minify_raw(r#"abc def"//"ghi'//'jkl//the end"#);
debug_assert_eq!(actual1, r#"abc def"//"ghi'//'jkl"#);
let actual2 = minify_raw(r#"abc def"//""#);
debug_assert_eq!(actual2, r#"abc def"//""#);
}
#[test]
fn ignores_comment_markers_inside_parentheses() {
let actual = minify_raw("bla (//) bla//the end");
debug_assert_eq!(actual, "bla (//) bla");
}
#[test]
fn ignores_even_unescaped_urls() {
let actual = minify_raw("https://test.com// comment//");
debug_assert_eq!(actual, "https://test.com");
}
}
mod minify_raw {
use super::*;
#[test]
fn removes_multi_line_comments() {
let input = "this is a/* ignore me please */test";
let expected = "this is a test";
let actual = minify_raw(input);
debug_assert_eq!(actual, expected);
}
#[test]
fn joins_all_lines_of_code() {
let input = "this\nis\na/* ignore me \n please */\ntest";
let expected = "this is a test";
let actual = minify_raw(input);
debug_assert_eq!(actual, expected);
}
#[test]
fn removes_line_comments_filling_entire_line() {
let input = "line one\n// remove this comment\nline two";
let expected = "line one line two";
let actual = minify_raw(input);
debug_assert_eq!(actual, expected);
}
#[test]
fn removes_line_comments_at_end_of_lines() {
let input = "valid line with // a comment\nout comments";
let expected = "valid line with out comments";
let actual = minify_raw(input);
debug_assert_eq!(actual, expected);
}
#[test]
fn preserves_multi_line_comments_starting_with_bang() {
let input = "this is a /*! dont ignore me please */ test/* but you can ignore me */";
let expected = "this is a /*! dont ignore me please */ test";
let actual = minify_raw(input);
debug_assert_eq!(actual, expected);
}
mod minify_raw_specific {
use super::*;
#[test]
fn works_with_raw_escape_codes() {
let input = "this\\nis\\na/* ignore me \\n please */\\ntest";
let expected = "this is a test";
let actual = minify_raw(input);
debug_assert_eq!(actual, expected);
}
}
mod compress_symbols {
use super::*;
#[test]
fn removes_spaces_around_symbols() {
let input = "; : { } , ; ";
let expected = ";:{},;";
let actual = minify_raw(input);
debug_assert_eq!(actual, expected);
}
#[test]
fn ignores_symbols_inside_strings() {
let input = r#"; " : " ' : ' ;"#;
let expected = r#";" : " ' : ';"#;
let actual = minify_raw(input);
debug_assert_eq!(actual, expected);
}
#[test]
fn preserves_whitespace_preceding_colons() {
let input = "& :last-child { color: blue; }";
let expected = "& :last-child{color:blue;}";
let actual = minify_raw(input);
debug_assert_eq!(actual, expected);
}
}
}
}