use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
use sasso::{compile, FsImporter, Options, WarnEvent};
fn scratch(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("sasso_plaincss_{tag}_{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
dir
}
fn compile_in(dir: &std::path::Path, name: &str, src: &str) -> String {
let entry = dir.join(name);
std::fs::write(&entry, src).unwrap();
let url = entry.to_string_lossy().into_owned();
let imp = FsImporter::new(Vec::new());
let sink: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
let seen = Rc::clone(&sink);
let opts = Options::default()
.with_importer(&imp)
.with_url(&url)
.with_warn_handler(Rc::new(move |ev: &WarnEvent<'_>| {
seen.borrow_mut().push(ev.message.to_string());
}));
compile(src, &opts).expect("compile")
}
#[test]
fn a_loaded_files_charset_is_dropped() {
let dir = scratch("charset");
std::fs::write(
dir.join("_theme.css"),
"@charset \"utf-8\";\n.t { color: red; }\n",
)
.unwrap();
std::fs::write(
dir.join("_lib.scss"),
"@charset \"utf-8\";\n.s { content: \"é\"; }\n",
)
.unwrap();
assert_eq!(
compile_in(&dir, "use.scss", "@use \"theme\";\n@use \"lib\";\na { b: c }\n"),
"@charset \"UTF-8\";\n.t {\n color: red;\n}\n\n.s {\n content: \"é\";\n}\n\na {\n b: c;\n}"
);
assert_eq!(
compile_in(
&dir,
"imp.scss",
"a { b: c }\n@import \"theme\";\n@import \"lib\";\n"
),
"@charset \"UTF-8\";\na {\n b: c;\n}\n\n.t {\n color: red;\n}\n\n.s {\n content: \"é\";\n}"
);
assert_eq!(
compile_in(&dir, "only.scss", "@import \"theme\";\n"),
".t {\n color: red;\n}"
);
std::fs::write(
dir.join("_nested.css"),
"@media (min-width: 1px) {\n @charset \"utf-8\";\n .t { color: red; }\n}\n.u { @charset \"utf-8\"; color: blue; }\n",
)
.unwrap();
assert_eq!(
compile_in(&dir, "usenested.scss", "@use \"nested\";\n"),
"@media (min-width: 1px) {\n @charset \"utf-8\";\n .t {\n color: red;\n }\n}\n.u {\n @charset \"utf-8\";\n color: blue;\n}"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn nested_rules_keep_their_selector_lines() {
let dir = scratch("sel");
std::fs::write(
dir.join("_sel.css"),
"a,\nb {\n x: 1;\n}\nc, d {\n x: 2;\n}\n.p {\n q: 3;\n e,\n f {\n y: 1;\n }\n g, h {\n y: 2;\n }\n i, j,\n k {\n y: 3;\n }\n}\n",
)
.unwrap();
let expected = "a,\nb {\n x: 1;\n}\n\nc, d {\n x: 2;\n}\n\n.p {\n q: 3;\n e,\n f {\n y: 1;\n }\n g, h {\n y: 2;\n }\n i, j,\n k {\n y: 3;\n }\n}";
assert_eq!(compile_in(&dir, "use.scss", "@use \"sel\";\n"), expected);
assert_eq!(compile_in(&dir, "imp.scss", "@import \"sel\";\n"), expected);
std::fs::write(
dir.join("_amp.css"),
".p {\n .child,\n & {\n y: 1;\n }\n &,\n .kid {\n y: 2;\n }\n .a, &.b,\n .c {\n y: 3;\n }\n}\n",
)
.unwrap();
let body = "\n .child,\n & {\n y: 1;\n }\n &,\n .kid {\n y: 2;\n }\n .a, &.b,\n .c {\n y: 3;\n }\n}";
assert_eq!(
compile_in(&dir, "useamp.scss", "@use \"amp\";\n"),
format!(".p {{{body}")
);
assert_eq!(
compile_in(&dir, "nestamp.scss", "x {\n @import \"amp\";\n}\n"),
format!("x .p {{{body}")
);
std::fs::remove_dir_all(&dir).ok();
}
fn compile_compressed_in(dir: &std::path::Path, name: &str, src: &str) -> String {
let entry = dir.join(name);
std::fs::write(&entry, src).unwrap();
let url = entry.to_string_lossy().into_owned();
let imp = FsImporter::new(Vec::new());
let opts = Options::default()
.with_importer(&imp)
.with_url(&url)
.with_style(sasso::OutputStyle::Compressed);
compile(src, &opts).expect("compile")
}
#[test]
fn a_loaded_files_values_serialize_for_the_output_style() {
let dir = scratch("values");
let case = |file: &str, decl: &str, expanded: &str, compressed: &str| {
std::fs::write(dir.join(format!("_{file}.css")), format!(".a {{ b: {decl}; }}\n")).unwrap();
let src = format!("@use \"{file}\";\n");
assert_eq!(
compile_in(&dir, &format!("entry_{file}.scss"), &src),
format!(".a {{\n b: {expanded};\n}}"),
"expanded {decl}"
);
assert_eq!(
compile_compressed_in(&dir, &format!("entry_{file}_c.scss"), &src),
format!(".a{{b:{compressed}}}"),
"compressed {decl}"
);
};
case("num", "0.5px", "0.5px", ".5px");
case("neg", "-0.5px", "-0.5px", "-0.5px");
case("list", "1px 0.5px", "1px 0.5px", "1px .5px");
case("clist", "1px, 0.5px", "1px, 0.5px", "1px,.5px");
case("hex6", "#cccccc", "#cccccc", "#ccc");
case("hex6u", "#CCCCCC", "#CCCCCC", "#ccc");
case("hex8", "#ccccccff", "#cccccc", "#ccc");
case("hexname", "#ff0000", "#ff0000", "red");
case(
"shorthand",
"1px solid #cccccc",
"1px solid #cccccc",
"1px solid #ccc",
);
case(
"calc",
"calc(100% - 2 * var(--x))",
"calc(100% - 2 * var(--x))",
"calc(100% - 2*var(--x))",
);
}
#[test]
fn a_loaded_files_colour_keywords_stay_identifiers() {
let dir = scratch("keywords");
let case = |file: &str, decl: &str, want: &str| {
std::fs::write(dir.join(format!("_{file}.css")), format!(".a {{ b: {decl}; }}\n")).unwrap();
let src = format!("@use \"{file}\";\n");
assert_eq!(
compile_compressed_in(&dir, &format!("entry_{file}.scss"), &src),
format!(".a{{b:{want}}}"),
"{decl}"
);
};
for (i, (decl, want)) in [
("white", "white"),
("black", "black"),
("magenta", "magenta"),
("rebeccapurple", "rebeccapurple"),
("transparent", "transparent"),
("WHITE", "WHITE"),
("1px solid white", "1px solid white"),
("white, black", "white,black"),
]
.iter()
.enumerate()
{
case(&format!("kw{i}"), decl, want);
}
let dir2 = scratch("keywords_scss");
assert_eq!(
compile_compressed_in(&dir2, "s.scss", ".a { b: white; }"),
".a{b:#fff}"
);
}
#[test]
fn a_loaded_files_function_call_serializes_in_the_default_style() {
let dir = scratch("calls");
let case = |file: &str, decl: &str, want: &str| {
std::fs::write(dir.join(format!("_{file}.css")), format!(".a {{ b: {decl}; }}\n")).unwrap();
let src = format!("@use \"{file}\";\n");
assert_eq!(
compile_compressed_in(&dir, &format!("entry_{file}.scss"), &src),
format!(".a{{b:{want}}}"),
"{decl}"
);
};
case("rgb", "rgb(255,255,255)", "rgb(255, 255, 255)");
case("rgba", "rgba(0, 0, 0, 0.15)", "rgba(0, 0, 0, 0.15)");
case("unk", "unknownfn(0.5px,2px)", "unknownfn(0.5px, 2px)");
case("nest", "nested(inner(0.5), 2)", "nested(inner(0.5), 2)");
case("tr", "translate(0.5px,-0.5px)", "translate(0.5px, -0.5px)");
}
#[test]
fn a_loaded_files_comment_only_rule_vanishes_when_compressed() {
let dir = scratch("emptyrule");
let case = |file: &str, css: &str, want: &str| {
std::fs::write(dir.join(format!("_{file}.css")), css).unwrap();
assert_eq!(
compile_compressed_in(
&dir,
&format!("entry_{file}.scss"),
&format!("@use \"{file}\";\n")
),
want,
"{css}"
);
};
case("only", ".a {\n .b {\n /* c */\n }\n}\n", "");
case(
"deep",
".a {\n .b {\n .c {\n /* c */\n }\n }\n}\n",
"",
);
case("at", ".a {\n @media x {\n /* c */\n }\n}\n", "");
case(
"sibling",
".a {\n c: 1;\n .b {\n /* c */\n }\n}\n",
".a{c:1}",
);
case("loud", ".a {\n .b {\n /*! c */\n }\n}\n", ".a{.b{/*! c */}}");
}
#[test]
fn a_loaded_files_escaped_selectors_are_not_structure() {
let dir = scratch("escaped");
let load = |css: &str| std::fs::write(dir.join("_vendor.css"), css).unwrap();
load(".a\\,b, .c { d: 1; }\n");
assert_eq!(
compile_in(&dir, "use.scss", "@use \"vendor\";\n"),
".a\\,b, .c {\n d: 1;\n}"
);
load(".a\\[b, .c { d: 1; }\n");
assert_eq!(
compile_in(&dir, "use2.scss", "@use \"vendor\";\n"),
".a\\[b, .c {\n d: 1;\n}"
);
load(".a\\&b { c: 1; }\n");
assert_eq!(
compile_in(&dir, "use3.scss", "@use \"vendor\";\n"),
".a\\&b {\n c: 1;\n}"
);
load(".a\\&b { c: 1; }\n");
assert_eq!(
compile_in(&dir, "imp.scss", ".p { @import \"vendor\"; }\n"),
".p .a\\&b {\n c: 1;\n}"
);
load(".a\\&b { c: 1; }\n& .d { e: 1; }\n");
assert_eq!(
compile_in(&dir, "imp2.scss", ".p { @import \"vendor\"; }\n"),
".p {\n & .d {\n e: 1;\n }\n}\n.p .a\\&b {\n c: 1;\n}"
);
load(".a\\,b, .c { d: 1; }\n");
assert_eq!(
compile_in(&dir, "imp3.scss", ".p { @import \"vendor\"; }\n"),
".p .a\\,b, .p .c {\n d: 1;\n}"
);
load("[x=\"&\"] { c: 1; }\n");
assert_eq!(
compile_in(&dir, "imp4.scss", ".p { @import \"vendor\"; }\n"),
".p [x=\"&\"] {\n c: 1;\n}"
);
std::fs::remove_dir_all(&dir).ok();
}