use super::{
CargoLoader, FsLoader, LoadError, Loader, SourceFile, SourceKind,
SourcePos,
};
use crate::output::{handle_parsed, CssData, Format};
use crate::{Error, ScopeRef};
use std::{borrow::Cow, collections::BTreeMap, fmt, path::Path};
use tracing::{span, Level};
type Combine = &'static dyn Fn(&str, &str) -> String;
pub struct Context<Loader> {
loader: Loader,
scope: Option<ScopeRef>,
loading: BTreeMap<String, SourceKind>,
}
pub type FsContext = Context<FsLoader>;
impl FsContext {
pub fn for_cwd() -> Self {
Self::for_loader(FsLoader::for_cwd())
}
pub fn for_path(path: &Path) -> Result<(Self, SourceFile), LoadError> {
let (file_context, file) = FsLoader::for_path(path)?;
Ok((Self::for_loader(file_context), file))
}
pub fn push_path(&mut self, path: &Path) {
self.loader.push_path(path);
}
}
pub type CargoContext = Context<CargoLoader>;
impl CargoContext {
pub fn for_crate() -> Result<Self, LoadError> {
Ok(Self::for_loader(CargoLoader::for_crate()?))
}
pub fn for_path(path: &Path) -> Result<(Self, SourceFile), LoadError> {
let (file_context, file) = CargoLoader::for_path(path)?;
Ok((Self::for_loader(file_context), file))
}
pub fn push_path(&mut self, path: &Path) -> Result<(), LoadError> {
self.loader.push_path(path)
}
}
impl<AnyLoader: Loader> Context<AnyLoader> {
pub fn for_loader(loader: AnyLoader) -> Self {
Self {
loader,
scope: None,
loading: Default::default(),
}
}
pub fn transform(mut self, file: SourceFile) -> Result<Vec<u8>, Error> {
let scope = self
.scope
.clone()
.unwrap_or_else(|| ScopeRef::new_global(Default::default()));
self.lock_loading(&file, false)?;
let mut css = CssData::new();
let format = scope.get_format();
handle_parsed(file.parse()?, &mut css, scope, &mut self)?;
self.unlock_loading(&file);
css.into_buffer(format)
}
pub fn with_format(mut self, format: Format) -> Self {
self.scope = Some(ScopeRef::new_global(format));
self
}
pub fn get_scope(&mut self) -> ScopeRef {
self.scope
.get_or_insert_with(|| ScopeRef::new_global(Default::default()))
.clone()
}
pub fn find_file(
&mut self,
url: &str,
from: SourceKind,
) -> Result<Option<SourceFile>, Error> {
let span = span!(Level::TRACE, "find_file", ?self, url, %from);
let _span = span.enter();
let names: &[Combine] = if from.is_import() {
&[
&|base, name| format!("{base}{name}.import.scss"),
&|base, name| format!("{base}_{name}.import.scss"),
&|base, name| format!("{base}{name}.scss"),
&|base, name| format!("{base}_{name}.scss"),
&|base, name| format!("{base}{name}/index.import.scss"),
&|base, name| format!("{base}{name}/_index.import.scss"),
&|base, name| format!("{base}{name}/index.scss"),
&|base, name| format!("{base}{name}/_index.scss"),
&|base, name| format!("{base}{name}.css"),
&|base, name| format!("{base}_{name}.css"),
]
} else {
&[
&|base, name| format!("{base}{name}.scss"),
&|base, name| format!("{base}_{name}.scss"),
&|base, name| format!("{base}{name}/index.scss"),
&|base, name| format!("{base}{name}/_index.scss"),
&|base, name| format!("{base}{name}.css"),
&|base, name| format!("{base}_{name}.css"),
]
};
let url = relative(&from, url);
if let Some((path, mut file)) = self.do_find_file(&url, names)? {
let is_module = !from.is_import();
let source = from.url(&path);
let file = SourceFile::read(&mut file, source)?;
self.lock_loading(&file, is_module)?;
Ok(Some(file))
} else {
Ok(None)
}
}
fn do_find_file(
&self,
url: &str,
names: &[Combine],
) -> Result<Option<(String, AnyLoader::File)>, LoadError> {
if url.ends_with(".css")
|| url.ends_with(".sass")
|| url.ends_with(".scss")
{
self.loader
.find_file(url)
.map(|file| file.map(|file| (url.into(), file)))
} else {
let (base, name) =
url.rfind('/').map_or(("", url), |p| url.split_at(p + 1));
for name in names.iter().map(|f| f(base, name)) {
if let Some(result) = self.loader.find_file(&name)? {
return Ok(Some((name, result)));
}
}
Ok(None)
}
}
pub(crate) fn lock_loading(
&mut self,
file: &SourceFile,
as_module: bool,
) -> Result<(), Error> {
let name = file.source().name();
let pos = &file.source().imported;
if let Some(old) = self.loading.insert(name.into(), pos.clone()) {
Err(Error::ImportLoop(
as_module,
pos.next().unwrap().clone(),
old.next().cloned(),
))
} else {
Ok(())
}
}
pub fn unlock_loading(&mut self, file: &SourceFile) {
self.loading.remove(file.path());
}
}
fn relative<'a>(base: &SourceKind, url: &'a str) -> Cow<'a, str> {
base.next()
.map(SourcePos::file_url)
.and_then(|base| {
base.rfind('/')
.map(|p| base.split_at(p + 1).0)
.map(|base| format!("{base}{url}").into())
})
.unwrap_or_else(|| url.into())
}
impl<T: fmt::Debug> fmt::Debug for Context<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Context")
.field("loader", &self.loader)
.field(
"scope",
&if self.scope.is_some() { "loaded" } else { "no" },
)
.field("locked", &self.loading.keys())
.finish()
}
}