use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use grass::{Options, from_string};
use super::{Asset, Environment, LayeredContext, MediaType, ProcessesAssets, ProcessingError};
impl From<Box<grass::Error>> for ProcessingError {
fn from(error: Box<grass::Error>) -> Self {
ProcessingError::Compilation {
message: error.to_string().into(),
}
}
}
const KITS_VIRTUAL_ROOT: &str = "/__aer_kits__";
pub struct ScssProcessor {}
impl ProcessesAssets for ScssProcessor {
fn process(
&self,
env: &Environment,
_context: &LayeredContext,
asset: &mut Asset,
) -> Result<bool, ProcessingError> {
if *asset.media_type() != MediaType::Scss {
return Ok(false);
}
tracing::trace!("scss: {}", asset.path());
let full_path = env.source_root.join(asset.path().as_str());
let kit_fs = KitFs {
kits: env.kit_imports.clone(),
};
let mut options = Options::default().fs(&kit_fs);
if let Some(parent) = full_path.parent() {
options = options.load_path(parent);
}
if !env.kit_imports.is_empty() {
options = options.load_path(KITS_VIRTUAL_ROOT);
}
let css = from_string(asset.as_text()?.to_string(), &options)?;
asset.replace_with_text(css.into(), MediaType::Css);
Ok(true)
}
}
#[derive(Debug)]
struct KitFs {
kits: BTreeMap<String, PathBuf>,
}
impl KitFs {
fn remap(&self, path: &Path) -> Option<PathBuf> {
let suffix = path.to_string_lossy();
let suffix = suffix.strip_prefix(KITS_VIRTUAL_ROOT)?.strip_prefix('/')?;
if let Some((kit_name, rest)) = suffix.split_once('/') {
let kit_dir = self.kits.get(kit_name)?;
Some(kit_dir.join(rest))
} else {
self.kits.get(suffix).cloned()
}
}
}
impl grass::Fs for KitFs {
fn is_dir(&self, path: &Path) -> bool {
if path == Path::new(KITS_VIRTUAL_ROOT) {
return !self.kits.is_empty();
}
if let Some(real) = self.remap(path) {
real.is_dir()
} else {
path.is_dir()
}
}
fn is_file(&self, path: &Path) -> bool {
if let Some(real) = self.remap(path) {
real.is_file()
} else {
path.is_file()
}
}
fn read(&self, path: &Path) -> std::io::Result<Vec<u8>> {
if let Some(real) = self.remap(path) {
std::fs::read(&real)
} else {
std::fs::read(path)
}
}
fn canonicalize(&self, path: &Path) -> std::io::Result<PathBuf> {
if let Some(real) = self.remap(path) {
std::fs::canonicalize(&real)
} else {
std::fs::canonicalize(path)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::proc::LayeredContext;
#[test]
fn processes_scss() {
let scss = r#"
$font-stack: Helvetica, sans-serif;
$primary-color: #333;
body {
font: 100% $font-stack;
color: $primary-color;
}
"#;
let mut asset = Asset::new("styles.scss".into(), scss.as_bytes().to_vec());
ScssProcessor {}
.process(
&Environment::test(),
&LayeredContext::from_flat(Default::default()),
&mut asset,
)
.unwrap();
assert_eq!(
"body {\n font: 100% Helvetica, sans-serif;\n color: #333;\n}\n",
asset.as_text().unwrap()
);
}
#[test]
fn processes_nested_scss() {
let scss = r#"
nav {
ul {
margin: 0;
padding: 0;
list-style: none;
}
li { display: inline-block; }
a {
display: block;
padding: 6px 12px;
text-decoration: none;
}
}
"#;
let mut asset = Asset::new("nav.scss".into(), scss.as_bytes().to_vec());
ScssProcessor {}
.process(
&Environment::test(),
&LayeredContext::from_flat(Default::default()),
&mut asset,
)
.unwrap();
assert_eq!(
"nav ul {\n margin: 0;\n padding: 0;\n list-style: none;\n}\nnav li {\n display: inline-block;\n}\nnav a {\n display: block;\n padding: 6px 12px;\n text-decoration: none;\n}\n",
asset.as_text().unwrap()
);
}
}