mod arena;
mod ast;
mod builtins;
mod deprecation;
mod diag;
mod emit;
mod error;
mod eval;
mod fxhash;
mod musl_math;
mod parser;
mod ryu;
mod sass_parser;
mod scanner;
mod selector;
mod sourcemap;
mod value;
pub use arena::{set_arena_bytes, ScopedAlloc};
pub use error::Error;
pub use sourcemap::SourceMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OutputStyle {
#[default]
Expanded,
Compressed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Syntax {
#[default]
Scss,
Sass,
Css,
}
pub trait Importer {
fn resolve(&self, path: &str) -> Option<String>;
fn resolve_module(&self, path: &str) -> Option<(String, String)> {
self.resolve(path).map(|src| (path.to_string(), src))
}
fn resolve_with_syntax(&self, path: &str) -> Option<(String, Syntax)> {
self.resolve(path).map(|src| (src, Syntax::Scss))
}
fn resolve_module_with_syntax(&self, path: &str) -> Option<(String, String, Syntax)> {
self.resolve_module(path)
.map(|(key, src)| (key, src, Syntax::Scss))
}
fn resolve_module_with_syntax_in(
&self,
path: &str,
_base_dir: Option<&str>,
) -> Option<(String, String, Syntax)> {
self.resolve_module_with_syntax(path)
}
fn resolve_import_with_path(
&self,
path: &str,
_base_dir: Option<&str>,
) -> Option<(String, String, Syntax)> {
self.resolve_with_syntax(path)
.map(|(src, syntax)| (String::new(), src, syntax))
}
}
pub struct Options<'a> {
pub style: OutputStyle,
pub syntax: Syntax,
pub importer: Option<&'a dyn Importer>,
pub url: Option<&'a str>,
pub unicode: bool,
pub source_map_include_sources: bool,
}
impl Default for Options<'_> {
fn default() -> Self {
Options {
style: OutputStyle::default(),
syntax: Syntax::default(),
importer: None,
url: None,
unicode: true,
source_map_include_sources: false,
}
}
}
impl<'a> Options<'a> {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_style(mut self, style: OutputStyle) -> Self {
self.style = style;
self
}
#[must_use]
pub fn with_syntax(mut self, syntax: Syntax) -> Self {
self.syntax = syntax;
self
}
#[must_use]
pub fn with_importer(mut self, importer: &'a dyn Importer) -> Self {
self.importer = Some(importer);
self
}
#[must_use]
pub fn with_url(mut self, url: &'a str) -> Self {
self.url = Some(url);
self
}
#[must_use]
pub fn with_unicode(mut self, unicode: bool) -> Self {
self.unicode = unicode;
self
}
#[must_use]
pub fn with_source_map_include_sources(mut self, include: bool) -> Self {
self.source_map_include_sources = include;
self
}
}
pub fn compile(source: &str, options: &Options<'_>) -> Result<String, Error> {
let guard = arena::Scope::enter();
let result = compile_inner(source, options);
let outermost = arena::leave_no_reset();
let owned = result.clone();
drop(result);
if outermost {
arena::reset();
}
std::mem::forget(guard);
owned
}
#[derive(Clone, Debug)]
pub struct CompileResult {
pub css: String,
pub source_map: SourceMap,
}
pub fn compile_with_source_map(source: &str, options: &Options<'_>) -> Result<CompileResult, Error> {
let guard = arena::Scope::enter();
let result = compile_inner_sm(source, options);
let outermost = arena::leave_no_reset();
let owned = result.clone();
drop(result);
if outermost {
arena::reset();
}
std::mem::forget(guard);
owned
}
fn basename(url: &str) -> &str {
url.rsplit('/').next().unwrap_or(url)
}
fn compile_inner_sm(source: &str, options: &Options<'_>) -> Result<CompileResult, Error> {
let glyphs = if options.unicode {
diag::GlyphSet::Unicode
} else {
diag::GlyphSet::Ascii
};
let sheet = match options.syntax {
Syntax::Scss => parser::parse(source),
Syntax::Css => parser::parse_plain_css(source),
Syntax::Sass => sass_parser::parse(source),
};
let sheet = match sheet {
Ok(s) => s,
Err(mut e) => {
if let Some(url) = options.url {
if e.rendered.is_none() && e.has_position() {
let span = diag::Span {
line: e.line,
col: e.col,
length: e.length,
};
e.rendered = Some(diag::render_error(&e.message, source, url, span, glyphs));
}
}
return Err(e);
}
};
eval::validate_declarations(&sheet)?;
let entry_name = options.url.unwrap_or("stdin");
let mut ev = eval::Evaluator::new(eval::EvalOptions {
style: options.style,
importer: options.importer,
source,
url: entry_name,
glyphs,
});
let mut out = Vec::new();
ev.eval_sheet(&sheet, &mut out)?;
let (css, body_off, collector) = emit::emit_with_map(&out, options.style);
let mappings = collector.finalize(&css, body_off).encode();
let (sources, sources_content) = ev.source_table(entry_name, options.source_map_include_sources);
let source_map = SourceMap {
file: Some(basename(entry_name).to_string()),
sources,
sources_content,
mappings,
};
Ok(CompileResult { css, source_map })
}
fn compile_inner(source: &str, options: &Options<'_>) -> Result<String, Error> {
let glyphs_for = || {
if options.unicode {
diag::GlyphSet::Unicode
} else {
diag::GlyphSet::Ascii
}
};
let sheet = match options.syntax {
Syntax::Scss => parser::parse(source),
Syntax::Css => parser::parse_plain_css(source),
Syntax::Sass => sass_parser::parse(source),
};
let sheet = match sheet {
Ok(s) => s,
Err(mut e) => {
if let Some(url) = options.url {
if e.rendered.is_none() && e.has_position() {
let span = diag::Span {
line: e.line,
col: e.col,
length: e.length,
};
e.rendered = Some(diag::render_error(&e.message, source, url, span, glyphs_for()));
}
}
return Err(e);
}
};
eval::validate_declarations(&sheet)?;
let (diag_source, diag_url) = match options.url {
Some(url) => (source, url),
None => ("", ""),
};
let glyphs = if options.unicode {
diag::GlyphSet::Unicode
} else {
diag::GlyphSet::Ascii
};
let mut ev = eval::Evaluator::new(eval::EvalOptions {
style: options.style,
importer: options.importer,
source: diag_source,
url: diag_url,
glyphs,
});
let mut out = Vec::new();
ev.eval_sheet(&sheet, &mut out)?;
Ok(emit::emit(&out, options.style))
}
pub struct FsImporter {
load_paths: Vec<PathBuf>,
}
impl FsImporter {
pub fn new(load_paths: Vec<PathBuf>) -> Self {
FsImporter { load_paths }
}
}
impl Importer for FsImporter {
fn resolve(&self, path: &str) -> Option<String> {
for base in &self.load_paths {
match resolve_in_base(base, path, true) {
Resolution::Found(p) => {
if let Ok(src) = std::fs::read_to_string(&p) {
return Some(src);
}
}
Resolution::Ambiguous => return None,
Resolution::NotFound => {}
}
}
None
}
fn resolve_module(&self, path: &str) -> Option<(String, String)> {
self.resolve_module_with_syntax(path)
.map(|(key, src, _)| (key, src))
}
fn resolve_with_syntax(&self, path: &str) -> Option<(String, Syntax)> {
for base in &self.load_paths {
match resolve_in_base(base, path, true) {
Resolution::Found(p) => {
if let Ok(src) = std::fs::read_to_string(&p) {
return Some((src, syntax_for_path(&p)));
}
}
Resolution::Ambiguous => return None,
Resolution::NotFound => {}
}
}
None
}
fn resolve_module_with_syntax(&self, path: &str) -> Option<(String, String, Syntax)> {
self.resolve_module_with_syntax_in(path, None)
}
fn resolve_module_with_syntax_in(
&self,
path: &str,
base_dir: Option<&str>,
) -> Option<(String, String, Syntax)> {
let bases = base_dir
.map(|b| vec![PathBuf::from(b)])
.unwrap_or_default()
.into_iter()
.chain(self.load_paths.iter().cloned());
for base in bases {
match resolve_in_base(&base, path, false) {
Resolution::Found(p) => {
if let Ok(src) = std::fs::read_to_string(&p) {
let key = std::fs::canonicalize(&p)
.map(|c| c.to_string_lossy().into_owned())
.unwrap_or_else(|_| p.to_string_lossy().into_owned());
return Some((key, src, syntax_for_path(&p)));
}
}
Resolution::Ambiguous => return None,
Resolution::NotFound => {}
}
}
None
}
fn resolve_import_with_path(
&self,
path: &str,
base_dir: Option<&str>,
) -> Option<(String, String, Syntax)> {
let bases = base_dir
.map(|b| vec![PathBuf::from(b)])
.unwrap_or_default()
.into_iter()
.chain(self.load_paths.iter().cloned());
for base in bases {
match resolve_in_base(&base, path, true) {
Resolution::Found(p) => {
if let Ok(src) = std::fs::read_to_string(&p) {
let key = std::fs::canonicalize(&p)
.map(|c| c.to_string_lossy().into_owned())
.unwrap_or_else(|_| p.to_string_lossy().into_owned());
return Some((key, src, syntax_for_path(&p)));
}
}
Resolution::Ambiguous => return None,
Resolution::NotFound => {}
}
}
None
}
}
fn syntax_for_path(p: &Path) -> Syntax {
match p.extension().and_then(|e| e.to_str()) {
Some(e) if e.eq_ignore_ascii_case("sass") => Syntax::Sass,
Some(e) if e.eq_ignore_ascii_case("css") => Syntax::Css,
_ => Syntax::Scss,
}
}
enum Resolution {
Found(PathBuf),
Ambiguous,
NotFound,
}
fn resolve_in_base(base: &Path, path: &str, allow_import_only: bool) -> Resolution {
let normalized = lexical_normalize(path);
let path = normalized.as_str();
let p = Path::new(path);
let dir = match p.parent() {
Some(par) if !par.as_os_str().is_empty() => base.join(par),
_ => base.to_path_buf(),
};
let file = p.file_name().and_then(|s| s.to_str()).unwrap_or(path);
if let Some(stem) = file.strip_suffix(".css") {
return match tier_exact(&dir, stem, &["css"], false) {
Tier::One(p) => Resolution::Found(p),
Tier::Many => Resolution::Ambiguous,
Tier::None => Resolution::NotFound,
};
}
let explicit_ext = [".scss", ".sass"].into_iter().find(|ext| file.ends_with(ext));
if let Some(ext) = explicit_ext {
let stem = &file[..file.len() - ext.len()];
let ext = &ext[1..];
if allow_import_only {
match tier_exact(&dir, stem, &[ext], true) {
Tier::One(p) => return Resolution::Found(p),
Tier::Many => return Resolution::Ambiguous,
Tier::None => {}
}
}
return match tier_exact(&dir, stem, &[ext], false) {
Tier::One(p) => Resolution::Found(p),
Tier::Many => Resolution::Ambiguous,
Tier::None => Resolution::NotFound,
};
}
let mut non_index: Vec<(&str, bool)> = Vec::with_capacity(2);
if allow_import_only {
non_index.push((file, true));
}
non_index.push((file, false));
for (stem, import_only) in &non_index {
match tier_with_extensions(&dir, stem, *import_only) {
Tier::One(p) => return Resolution::Found(p),
Tier::Many => return Resolution::Ambiguous,
Tier::None => {}
}
}
match tier_exact(&dir, file, &["css"], false) {
Tier::One(p) => return Resolution::Found(p),
Tier::Many => return Resolution::Ambiguous,
Tier::None => {}
}
let index_dir = dir.join(file);
let index_modes: &[bool] = if allow_import_only {
&[true, false]
} else {
&[false]
};
for import_only in index_modes {
match tier_with_extensions(&index_dir, "index", *import_only) {
Tier::One(p) => return Resolution::Found(p),
Tier::Many => return Resolution::Ambiguous,
Tier::None => {}
}
}
Resolution::NotFound
}
fn lexical_normalize(path: &str) -> String {
let mut out: Vec<&str> = Vec::new();
for seg in path.split('/') {
match seg {
"" | "." => {}
".." => {
if matches!(out.last(), Some(&s) if s != "..") {
out.pop();
} else {
out.push("..");
}
}
s => out.push(s),
}
}
let mut s = out.join("/");
if path.starts_with('/') {
s.insert(0, '/');
}
if s.is_empty() {
s.push('.');
}
s
}
enum Tier {
None,
One(PathBuf),
Many,
}
impl Tier {
fn from(mut found: Vec<PathBuf>) -> Tier {
match found.len() {
0 => Tier::None,
1 => Tier::One(found.pop().unwrap_or_default()),
_ => Tier::Many,
}
}
}
fn tier_with_extensions(dir: &Path, stem: &str, import_only: bool) -> Tier {
tier_exact(dir, stem, &["scss", "sass"], import_only)
}
fn tier_exact(dir: &Path, stem: &str, exts: &[&str], import_only: bool) -> Tier {
let mut found = Vec::new();
let suffix = if import_only { ".import" } else { "" };
for ext in exts {
for name in [format!("_{stem}{suffix}.{ext}"), format!("{stem}{suffix}.{ext}")] {
let cand = dir.join(&name);
if cand.is_file() {
found.push(cand);
}
}
}
Tier::from(found)
}