mod color;
mod color_ext;
mod colorspace;
mod list;
mod map;
mod math;
mod meta;
mod selector;
mod string;
use crate::error::Error;
use crate::scanner::Pos;
use crate::value::{Color, Number, SassStr, Value};
pub(crate) use meta::{inspect_element, inspect_value};
pub(crate) use color::convert_modern;
pub(crate) use colorspace::{hwb_to_srgb, srgb_to_hsl};
pub(crate) fn call(
name: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Result<Value, Error> {
if let Some(r) = color::try_call(name, pos_args, named, pos) {
return r;
}
if let Some(r) = color_ext::try_call(name, pos_args, named, pos) {
return r;
}
if let Some(r) = math::try_call(name, pos_args, named, pos) {
return r;
}
if let Some(r) = string::try_call(name, pos_args, named, pos) {
return r;
}
if let Some(r) = map::try_call(name, pos_args, named, pos) {
return r;
}
if let Some(r) = list::try_call(name, pos_args, named, pos) {
return r;
}
if let Some(r) = meta::try_call(name, pos_args, named, pos) {
return r;
}
if let Some(r) = selector::try_call(name, pos_args, named, pos) {
return r;
}
plain_css_function(name, pos_args, named, pos)
}
pub(crate) fn is_builtin(name: &str) -> bool {
let pos: &[Value] = &[];
let named: &[(String, Value)] = &[];
let p = Pos { line: 1, col: 1 };
color::try_call(name, pos, named, p).is_some()
|| color_ext::try_call(name, pos, named, p).is_some()
|| math::try_call(name, pos, named, p).is_some()
|| string::try_call(name, pos, named, p).is_some()
|| map::try_call(name, pos, named, p).is_some()
|| list::try_call(name, pos, named, p).is_some()
|| meta::try_call(name, pos, named, p).is_some()
|| selector::try_call(name, pos, named, p).is_some()
}
pub(super) fn arg<'v>(
params: &[&str],
pos_args: &'v [Value],
named: &'v [(String, Value)],
i: usize,
) -> Option<&'v Value> {
if let Some(v) = pos_args.get(i) {
return Some(v);
}
let pname = params.get(i)?;
named.iter().find(|(n, _)| n == pname).map(|(_, v)| v)
}
pub(super) fn require<'v>(
params: &[&str],
pos_args: &'v [Value],
named: &'v [(String, Value)],
i: usize,
fname: &str,
pos: Pos,
) -> Result<&'v Value, Error> {
arg(params, pos_args, named, i).ok_or_else(|| {
let pname = params.get(i).copied().unwrap_or("");
Error::at(format!("Missing argument ${pname} for {fname}()."), pos)
})
}
pub(super) fn max_positional(pos_args: &[Value], max: usize, pos: Pos) -> Result<(), Error> {
let n = pos_args.len();
if n > max {
let noun = if max == 1 { "argument" } else { "arguments" };
let verb = if n == 1 { "was" } else { "were" };
return Err(Error::at(
format!("Only {max} {noun} allowed, but {n} {verb} passed."),
pos,
));
}
Ok(())
}
pub(super) fn num(v: &Value, pos: Pos) -> Result<f64, Error> {
match v {
Value::Number(n) => Ok(n.value),
other => Err(Error::at(
format!("{} is not a number.", other.to_css(false)),
pos,
)),
}
}
pub(super) fn as_color(v: &Value, pos: Pos) -> Result<Color, Error> {
match v {
Value::Color(c) => Ok(c.clone()),
other => Err(Error::at(format!("{} is not a color.", other.to_css(false)), pos)),
}
}
pub(super) fn require_legacy_color(c: &Color, fname: &str, pos: Pos) -> Result<(), Error> {
if c.modern.as_ref().is_some_and(|m| !m.space.is_legacy()) {
return Err(Error::at(
format!(
"{fname}() is only supported for legacy colors. Please use color.adjust() \
instead with an explicit $space argument."
),
pos,
));
}
Ok(())
}
pub(super) fn channel(v: &Value, pos: Pos) -> Result<f64, Error> {
match v {
Value::Number(n) => {
if n.unit() == "%" {
Ok((n.value / 100.0 * 255.0).clamp(0.0, 255.0))
} else {
Ok(n.value.clamp(0.0, 255.0))
}
}
other => Err(Error::at(
format!("{} is not a number.", other.to_css(false)),
pos,
)),
}
}
pub(super) fn clamp01(v: f64) -> f64 {
v.clamp(0.0, 1.0)
}
fn plain_css_function(
name: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Result<Value, Error> {
if !named.is_empty() {
return Err(Error::at(
"Plain CSS functions don't support keyword arguments.",
pos,
));
}
let mut parts: Vec<String> = Vec::with_capacity(pos_args.len());
for v in pos_args {
match v {
Value::List(l) if l.items.is_empty() && !l.bracketed => {
return Err(Error::at("() isn't a valid CSS value.", pos));
}
Value::Map(_) => {
return Err(Error::at(
format!("{} isn't a valid CSS value.", v.to_css(false)),
pos,
));
}
_ => parts.push(v.to_css(false)),
}
}
Ok(Value::Str(SassStr {
text: format!("{name}({})", parts.join(", ")),
quoted: false,
}))
}
pub(crate) fn is_module(module: &str) -> bool {
matches!(
module,
"math" | "color" | "list" | "map" | "string" | "selector" | "meta"
)
}
pub(crate) fn module_member_to_global(module: &str, member: &str) -> Option<&'static str> {
match module {
"math" => match member {
"abs" => Some("abs"),
"ceil" => Some("ceil"),
"floor" => Some("floor"),
"sqrt" => Some("sqrt"),
"pow" => Some("pow"),
"exp" => Some("exp"),
"log" => Some("log"),
"sin" => Some("sin"),
"cos" => Some("cos"),
"tan" => Some("tan"),
"asin" => Some("asin"),
"acos" => Some("acos"),
"atan" => Some("atan"),
"atan2" => Some("atan2"),
"hypot" => Some("hypot"),
"sign" => Some("sign"),
"percentage" => Some("percentage"),
"unit" => Some("unit"),
"is-unitless" => Some("unitless"),
"compatible" => Some("comparable"),
"random" => Some("random"),
_ => None,
},
"map" => match member {
"get" => Some("map-get"),
"merge" => Some("map-merge"),
"remove" => Some("map-remove"),
"keys" => Some("map-keys"),
"values" => Some("map-values"),
"has-key" => Some("map-has-key"),
_ => None,
},
"string" => match member {
"length" => Some("str-length"),
"insert" => Some("str-insert"),
"index" => Some("str-index"),
"slice" => Some("str-slice"),
"quote" => Some("quote"),
"unquote" => Some("unquote"),
"to-upper-case" => Some("to-upper-case"),
"to-lower-case" => Some("to-lower-case"),
"unique-id" => Some("unique-id"),
_ => None,
},
"list" => match member {
"length" => Some("length"),
"nth" => Some("nth"),
"set-nth" => Some("set-nth"),
"append" => Some("append"),
"join" => Some("join"),
"zip" => Some("zip"),
"index" => Some("index"),
"separator" => Some("list-separator"),
"is-bracketed" => Some("is-bracketed"),
_ => None,
},
"selector" => match member {
"nest" => Some("selector-nest"),
"append" => Some("selector-append"),
"extend" => Some("selector-extend"),
"replace" => Some("selector-replace"),
"unify" => Some("selector-unify"),
"parse" => Some("selector-parse"),
"is-superselector" => Some("is-superselector"),
"simple-selectors" => Some("simple-selectors"),
_ => None,
},
"color" => match member {
"adjust" => Some("adjust-color"),
"scale" => Some("scale-color"),
"change" => Some("change-color"),
"red" => Some("red"),
"green" => Some("green"),
"blue" => Some("blue"),
"hue" => Some("hue"),
"saturation" => Some("saturation"),
"lightness" => Some("lightness"),
"whiteness" => Some("whiteness"),
"blackness" => Some("blackness"),
"alpha" => Some("alpha"),
"opacity" => Some("opacity"),
"grayscale" => Some("grayscale"),
"complement" => Some("complement"),
"invert" => Some("invert"),
"mix" => Some("mix"),
"ie-hex-str" => Some("ie-hex-str"),
"space" => Some("color-space"),
"channel" => Some("color-channel"),
"to-space" => Some("color-to-space"),
"is-legacy" => Some("color-is-legacy"),
"is-missing" => Some("color-is-missing"),
"is-in-gamut" => Some("color-is-in-gamut"),
"is-powerless" => Some("color-is-powerless"),
"to-gamut" => Some("color-to-gamut"),
"same" => Some("color-same"),
_ => None,
},
"meta" => match member {
"type-of" => Some("type-of"),
"inspect" => Some("inspect"),
"feature-exists" => Some("feature-exists"),
"calc-name" => Some("calc-name"),
"calc-args" => Some("calc-args"),
_ => None,
},
_ => None,
}
}
pub(crate) const META_FUNCTION_NAMES: &[&str] = &[
"accepts-content",
"calc-args",
"calc-name",
"call",
"content-exists",
"feature-exists",
"function-exists",
"get-function",
"get-mixin",
"global-variable-exists",
"inspect",
"keywords",
"mixin-exists",
"module-functions",
"module-mixins",
"module-variables",
"type-of",
"variable-exists",
];
pub(crate) const META_MIXIN_NAMES: &[&str] = &["apply", "load-css"];
pub(crate) fn module_has_member(module: &str, member: &str) -> bool {
if module == "meta" && META_FUNCTION_NAMES.contains(&member) {
return true;
}
if module == "math" && matches!(member, "div" | "clamp" | "min" | "max" | "round") {
return true;
}
if module == "map" && matches!(member, "set" | "deep-merge" | "deep-remove") {
return true;
}
if module == "string" && member == "split" {
return true;
}
if module == "color" && member == "hwb" {
return true;
}
module_member_to_global(module, member).is_some()
}
pub(crate) fn call_module(
module: &str,
member: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Result<Value, Error> {
let normalized;
let member = if member.contains('_') {
normalized = member.replace('_', "-");
normalized.as_str()
} else {
member
};
if module == "math" && member == "div" {
return math::module_div(pos_args, named, pos);
}
if module == "math" {
match member {
"clamp" => return math::module_clamp(pos_args, named, pos),
"min" => return math::module_min_max(pos_args, named, pos, true),
"max" => return math::module_min_max(pos_args, named, pos, false),
"round" => return math::module_round(pos_args, named, pos),
_ => {}
}
}
if module == "map" {
if let Some(r) = map::call_module_member(member, pos_args, named, pos) {
return r;
}
}
if module == "string" {
if let Some(r) = string::call_module_member(member, pos_args, named, pos) {
return r;
}
}
if module == "list" {
if let Some(r) = list::call_module_member(member, pos_args, named, pos) {
return r;
}
}
if module == "color" {
if let Some(r) = color::call_module_member(member, pos_args, named, pos) {
return r;
}
if matches!(member, "grayscale" | "opacity") {
let arg = pos_args
.first()
.or_else(|| named.iter().find(|(n, _)| n == "color").map(|(_, v)| v));
if let Some(v @ Value::Str(s)) = arg {
if !s.quoted {
return Err(Error::at(
format!("$color: {} is not a color.", v.to_css(false)),
pos,
));
}
}
}
}
match module_member_to_global(module, member) {
Some(global) => call(global, pos_args, named, pos),
None => Err(Error::at("Undefined function.".to_string(), pos)),
}
}
pub(crate) fn module_var(module: &str, name: &str, pos: Pos) -> Result<Value, Error> {
let number = |value: f64| Ok(Value::Number(Number::unitless(value)));
if module == "math" {
return match name {
"pi" => number(std::f64::consts::PI),
"e" => number(std::f64::consts::E),
"epsilon" => number(f64::EPSILON),
"max-safe-integer" => number(9_007_199_254_740_991.0),
"min-safe-integer" => number(-9_007_199_254_740_991.0),
"max-number" => number(f64::MAX),
"min-number" => number(f64::from_bits(1)),
_ => Err(Error::at("Undefined variable.".to_string(), pos)),
};
}
Err(Error::at("Undefined variable.".to_string(), pos))
}