use crate::{Error, SourceFile, SourceName, SourcePos};
use std::borrow::Cow;
use std::path::{Path, PathBuf};
pub trait FileContext: Sized + std::fmt::Debug {
type File: std::io::Read;
#[tracing::instrument]
fn find_file_import(
&self,
url: &str,
from: SourcePos,
) -> Result<Option<SourceFile>, Error> {
let names: &[&dyn Fn(&str, &str) -> String] = &[
&|base, name| format!("{}{}.import.scss", base, name),
&|base, name| format!("{}_{}.import.scss", base, name),
&|base, name| format!("{}{}.scss", base, name),
&|base, name| format!("{}_{}.scss", base, name),
&|base, name| format!("{}{}/index.import.scss", base, name),
&|base, name| format!("{}{}/_index.import.scss", base, name),
&|base, name| format!("{}{}/index.scss", base, name),
&|base, name| format!("{}{}/_index.scss", base, name),
&|base, name| format!("{}{}.css", base, name),
&|base, name| format!("{}_{}.css", base, name),
];
let url = relative(from.file_url(), url);
if let Some((path, mut file)) = do_find_file(self, &url, names)? {
let source = SourceName::imported(path, from);
Ok(Some(SourceFile::read(&mut file, source)?))
} else {
Ok(None)
}
}
#[tracing::instrument]
fn find_file_use(
&self,
url: &str,
from: SourcePos,
) -> Result<Option<SourceFile>, Error> {
let names: &[&dyn Fn(&str, &str) -> String] = &[
&|base, name| format!("{}{}.scss", base, name),
&|base, name| format!("{}_{}.scss", base, name),
&|base, name| format!("{}{}/index.scss", base, name),
&|base, name| format!("{}{}/_index.scss", base, name),
&|base, name| format!("{}{}.css", base, name),
&|base, name| format!("{}_{}.css", base, name),
];
let url = relative(from.file_url(), url);
if let Some((path, mut file)) = do_find_file(self, &url, names)? {
let source = SourceName::used(path, from);
Ok(Some(SourceFile::read(&mut file, source)?))
} else {
Ok(None)
}
}
fn find_file(
&self,
url: &str,
) -> Result<Option<(String, Self::File)>, Error>;
}
fn relative<'a>(base: &str, url: &'a str) -> Cow<'a, str> {
base.rfind('/')
.map(|p| base.split_at(p + 1).0)
.map(|base| format!("{}{}", base, url).into())
.unwrap_or_else(|| url.into())
}
fn do_find_file<FC: FileContext>(
ctx: &FC,
url: &str,
names: &[&dyn Fn(&str, &str) -> String],
) -> Result<Option<(String, FC::File)>, Error> {
if let Some(result) = ctx.find_file(url)? {
return Ok(Some(result));
}
let (base, name) = url
.rfind('/')
.map(|p| url.split_at(p + 1))
.unwrap_or(("", url));
for name in names.iter().map(|f| f(base, name)) {
if let Some(result) = ctx.find_file(&name)? {
return Ok(Some(result));
}
}
Ok(None)
}
#[derive(Clone, Debug)]
pub struct FsFileContext {
path: Vec<PathBuf>,
}
impl FsFileContext {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
path: vec![PathBuf::new()],
}
}
pub fn push_path(&mut self, path: &Path) {
self.path.push(path.into());
}
pub fn for_path(path: &Path) -> Result<(Self, SourceFile), Error> {
let mut f = std::fs::File::open(&path)
.map_err(|e| Error::Input(path.display().to_string(), e))?;
let (path, name) = if let Some(base) = path.parent() {
(
vec![base.to_path_buf(), PathBuf::new()],
path.strip_prefix(base).unwrap(),
)
} else {
(vec![PathBuf::new()], path)
};
let ctx = Self { path };
let source = SourceName::root(name.display().to_string());
let source = SourceFile::read(&mut f, source)?;
Ok((ctx, source))
}
}
impl FileContext for FsFileContext {
type File = std::fs::File;
fn find_file(
&self,
name: &str,
) -> Result<Option<(String, Self::File)>, Error> {
if !name.is_empty() {
for base in &self.path {
let full = base.join(name);
if Path::new(&full).is_file() {
tracing::debug!(?full, "opening file");
return match Self::File::open(&full) {
Ok(file) => Ok(Some((name.to_string(), file))),
Err(e) => {
Err(Error::Input(full.display().to_string(), e))
}
};
}
tracing::trace!(?full, "Not found");
}
}
Ok(None)
}
}