use crate::error::Error;
use crate::scanner::Pos;
use crate::value::{convert_factor, CalcNode, Number, SassStr, Value};
pub(super) fn try_call(
name: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Option<Result<Value, Error>> {
let lname = name.to_ascii_lowercase();
let name = lname.as_str();
if let Some(op) = unary_op(name) {
return Some(unary(name, pos_args, named, pos, op));
}
match name {
"round" => Some(round(pos_args, named, pos)),
"min" => Some(min_max(name, pos_args, named, pos, true)),
"max" => Some(min_max(name, pos_args, named, pos, false)),
"clamp" => Some(clamp(pos_args, named, pos)),
"sign" => Some(sign(pos_args, named, pos)),
"pow" => Some(pow(pos_args, named, pos)),
"sqrt" => Some(unitless_unary("sqrt", "number", pos_args, named, pos, f64::sqrt)),
"exp" => Some(unitless_unary("exp", "number", pos_args, named, pos, f64::exp)),
"log" => Some(log(pos_args, named, pos)),
"hypot" => Some(hypot(pos_args, named, pos)),
"sin" => Some(trig("sin", pos_args, named, pos, f64::sin)),
"cos" => Some(trig("cos", pos_args, named, pos, f64::cos)),
"tan" => Some(trig("tan", pos_args, named, pos, f64::tan)),
"asin" => Some(inverse_trig("asin", pos_args, named, pos, f64::asin)),
"acos" => Some(inverse_trig("acos", pos_args, named, pos, f64::acos)),
"atan" => Some(inverse_trig("atan", pos_args, named, pos, f64::atan)),
"atan2" => Some(atan2(pos_args, named, pos)),
"rem" => Some(remainder("rem", pos_args, named, pos, true)),
"mod" => Some(remainder("mod", pos_args, named, pos, false)),
"random" => Some(random(pos_args, named, pos)),
_ => None,
}
}
fn unary_op(name: &str) -> Option<fn(f64) -> f64> {
Some(match name {
"abs" => f64::abs,
"ceil" => f64::ceil,
"floor" => f64::floor,
_ => return None,
})
}
pub(super) fn module_div(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["number1", "number2"];
check_max_args(pos_args, named, 2, pos)?;
let a = super::require(¶ms, pos_args, named, 0, "div", pos)?.clone();
let b = super::require(¶ms, pos_args, named, 1, "div", pos)?.clone();
crate::eval::eval_div(a, b, false, pos)
}
fn check_max_args(
pos_args: &[Value],
named: &[(String, Value)],
max_args: usize,
pos: Pos,
) -> Result<(), Error> {
let total = pos_args.len() + named.len();
if total <= max_args {
return Ok(());
}
let noun = if max_args == 1 { "argument" } else { "arguments" };
Err(Error::at(
format!("Only {max_args} {noun} allowed, but {total} were passed."),
pos,
))
}
#[derive(Clone, Copy, PartialEq)]
enum RoundStrategy {
Nearest,
Up,
Down,
ToZero,
}
impl RoundStrategy {
fn from_str(s: &str) -> Option<Self> {
Some(match s {
"nearest" => RoundStrategy::Nearest,
"up" => RoundStrategy::Up,
"down" => RoundStrategy::Down,
"to-zero" => RoundStrategy::ToZero,
_ => return None,
})
}
}
fn round(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
if pos_args.is_empty() && named.len() == 1 && named[0].0 == "number" {
let n = as_num(&named[0].1, pos)?;
return Ok(num_value(n.copy_units(n.value.round())));
}
let args = all_args(pos_args, named);
match args.len() {
0 => Err(Error::at("Missing argument.", pos)),
1 => {
match round_operand(&args[0], pos)? {
Some(n) => Ok(num_value(n.copy_units(n.value.round()))),
None => Ok(preserved_round(&args)),
}
}
2 => {
if let Value::Str(s) = &args[0] {
if !s.quoted && RoundStrategy::from_str(&s.text.to_ascii_lowercase()).is_some() {
return match round_operand(&args[1], pos)? {
Some(_) => Err(Error::at("If strategy is not null, step is required.", pos)),
None => Ok(preserved_round(&args)),
};
}
}
if args.iter().any(is_quoted_str) {
return Err(Error::at("Only 1 argument allowed, but 2 were passed.", pos));
}
let number = round_operand(&args[0], pos)?;
let step = round_operand(&args[1], pos)?;
match (number, step) {
(Some(number), Some(step)) => {
round_with_step(RoundStrategy::Nearest, &number, &step, false, pos)
}
_ => Ok(preserved_round(&args)),
}
}
3 => {
let strategy = match &args[0] {
Value::Str(s) if !s.quoted => {
match RoundStrategy::from_str(&s.text.to_ascii_lowercase()) {
Some(st) => st,
None => return Ok(preserved_round(&args)),
}
}
Value::Str(s) => {
return Err(Error::at(
format!("Value \"{}\" can't be used in a calculation.", s.text),
pos,
))
}
other => {
return Err(Error::at(
format!(
"{} must be either nearest, up, down or to-zero.",
other.to_css(false)
),
pos,
))
}
};
let number = round_operand(&args[1], pos)?;
let step = round_operand(&args[2], pos)?;
match (number, step) {
(Some(number), Some(step)) => round_with_step(strategy, &number, &step, true, pos),
_ => Ok(preserved_round(&args)),
}
}
n => Err(Error::at(
format!("Only 3 arguments allowed, but {n} were passed."),
pos,
)),
}
}
fn is_quoted_str(v: &Value) -> bool {
matches!(v, Value::Str(s) if s.quoted)
}
fn round_operand(v: &Value, pos: Pos) -> Result<Option<Number>, Error> {
match v {
Value::Number(n) => Ok(Some(n.clone())),
Value::Calc(crate::value::CalcNode::Number(n)) => Ok(Some(n.clone())),
Value::Str(s) if !s.quoted => Ok(const_number(&s.text)),
Value::Calc(_) => Ok(None),
Value::Str(s) => Err(Error::at(
format!("Value \"{}\" can't be used in a calculation.", s.text),
pos,
)),
other => Err(Error::at(
format!("Value {} can't be used in a calculation.", other.to_css(false)),
pos,
)),
}
}
fn round_with_step(
strategy: RoundStrategy,
number: &Number,
step: &Number,
explicit: bool,
pos: Pos,
) -> Result<Value, Error> {
let unit = number.unit().to_string();
let with_unit = |value: f64| num_value(Number::with_unit(value, unit.clone()));
let step_v = match coerce_step(step, number, pos) {
StepCoercion::Value(v) => v,
StepCoercion::Preserve => {
return Ok(preserved_round_nums(strategy, number, step, explicit));
}
StepCoercion::Error(e) => return Err(e),
};
let nv = number.value;
if (nv.is_infinite() && step_v.is_infinite()) || step_v == 0.0 || nv.is_nan() || step_v.is_nan() {
return Ok(with_unit(f64::NAN));
}
if nv.is_infinite() {
return Ok(with_unit(nv));
}
if step_v.is_infinite() {
if nv == 0.0 {
return Ok(with_unit(nv));
}
let value = match strategy {
RoundStrategy::Nearest | RoundStrategy::ToZero => {
if nv > 0.0 {
0.0
} else {
-0.0
}
}
RoundStrategy::Up => {
if nv > 0.0 {
f64::INFINITY
} else {
-0.0
}
}
RoundStrategy::Down => {
if nv < 0.0 {
f64::NEG_INFINITY
} else {
0.0
}
}
};
return Ok(with_unit(value));
}
let q = nv / step_v;
let rounded = match strategy {
RoundStrategy::Nearest => q.round(),
RoundStrategy::Up => {
if step_v < 0.0 {
q.floor()
} else {
q.ceil()
}
}
RoundStrategy::Down => {
if step_v < 0.0 {
q.ceil()
} else {
q.floor()
}
}
RoundStrategy::ToZero => {
if nv < 0.0 {
q.ceil()
} else {
q.floor()
}
}
};
Ok(with_unit(rounded * step_v))
}
enum StepCoercion {
Value(f64),
Preserve,
Error(Error),
}
fn verify_no_complex_units(numbers: &[&Number], pos: Pos) -> Result<(), Error> {
for n in numbers {
if n.has_complex_units() {
return Err(Error::at(
format!(
"Number {} isn't compatible with CSS calculations.",
n.to_css(false)
),
pos,
));
}
}
Ok(())
}
fn verify_no_complex_args(args: &[Value], pos: Pos) -> Result<(), Error> {
for v in args {
if let Value::Number(n) = v {
verify_no_complex_units(&[n], pos)?;
}
}
Ok(())
}
fn coerce_step(step: &Number, number: &Number, pos: Pos) -> StepCoercion {
if step.has_complex_units() || number.has_complex_units() {
return match crate::value::unit_lists_factor(
(step.numer_units(), step.denom_units()),
(number.numer_units(), number.denom_units()),
) {
Some(factor) => StepCoercion::Value(step.value * factor),
None => match verify_no_complex_units(&[number, step], pos) {
Err(e) => StepCoercion::Error(e),
Ok(()) => StepCoercion::Preserve,
},
};
}
if step.unit().eq_ignore_ascii_case(number.unit()) {
return StepCoercion::Value(step.value);
}
if step.is_unitless() && number.is_unitless() {
return StepCoercion::Value(step.value);
}
if step.is_unitless() != number.is_unitless() {
return StepCoercion::Error(incompatible(number, step, pos));
}
if let Some(factor) = convert_factor(step.unit(), number.unit()) {
return StepCoercion::Value(step.value * factor);
}
if crate::value::calc_units_incompatible(number.unit(), step.unit()) {
return StepCoercion::Error(incompatible(number, step, pos));
}
StepCoercion::Preserve
}
fn combine_into(n: &Number, target: &Number, pos: Pos) -> StepCoercion {
coerce_step(n, target, pos)
}
fn preserved_round(args: &[Value]) -> Value {
preserved_call("round", args)
}
fn preserved_round_nums(strategy: RoundStrategy, number: &Number, step: &Number, explicit: bool) -> Value {
let kw = match strategy {
RoundStrategy::Nearest => "nearest",
RoundStrategy::Up => "up",
RoundStrategy::Down => "down",
RoundStrategy::ToZero => "to-zero",
};
let text = if strategy == RoundStrategy::Nearest && !explicit {
format!("round({}, {})", number.to_css(false), step.to_css(false))
} else {
format!("round({kw}, {}, {})", number.to_css(false), step.to_css(false))
};
Value::Str(SassStr { text, quoted: false })
}
fn unary(
fname: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
op: fn(f64) -> f64,
) -> Result<Value, Error> {
check_max_args(pos_args, named, 1, pos)?;
let n = require_num(&["number"], pos_args, named, 0, fname, pos)?;
Ok(num_value(n.copy_units(op(n.value))))
}
fn sign(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
check_max_args(pos_args, named, 1, pos)?;
let n = require_num(&["number"], pos_args, named, 0, "sign", pos)?;
if n.numer_units().iter().any(|u| u == "%") {
return Ok(preserved_call("sign", &all_args(pos_args, named)));
}
let s = if n.value > 0.0 {
1.0
} else if n.value < 0.0 {
-1.0
} else {
n.value
};
Ok(num_value(n.copy_units(s)))
}
fn pow(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
check_max_args(pos_args, named, 2, pos)?;
let base = require_num(&["base", "exponent"], pos_args, named, 0, "pow", pos)?;
let exp = require_num(&["base", "exponent"], pos_args, named, 1, "pow", pos)?;
no_unit(&base, pos)?;
no_unit(&exp, pos)?;
Ok(unitless(base.value.powf(exp.value)))
}
fn unitless_unary(
fname: &str,
param: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
op: fn(f64) -> f64,
) -> Result<Value, Error> {
check_max_args(pos_args, named, 1, pos)?;
let n = require_num(&[param], pos_args, named, 0, fname, pos)?;
no_unit(&n, pos)?;
Ok(unitless(op(n.value)))
}
fn log(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
check_max_args(pos_args, named, 2, pos)?;
let params = &["number", "base"];
let x = require_num(params, pos_args, named, 0, "log", pos)?;
no_unit(&x, pos)?;
match super::arg(params, pos_args, named, 1) {
Some(b) if !matches!(b, Value::Null) => {
let b = as_num(b, pos)?;
no_unit(&b, pos)?;
Ok(unitless(x.value.ln() / b.value.ln()))
}
_ => Ok(unitless(x.value.ln())),
}
}
fn hypot(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let nums = collect_nums("hypot", pos_args, named, pos)?;
let first = match nums.first() {
Some(n) => n.clone(),
None => return Err(Error::at("At least one argument must be passed.", pos)),
};
verify_no_complex_units(&nums.iter().collect::<Vec<_>>(), pos)?;
if nums.iter().any(is_percent) {
let args: Vec<Value> = nums.into_iter().map(Value::Number).collect();
return Ok(preserved_call("hypot", &args));
}
let mut sum = 0.0;
for n in &nums {
match combine_into(n, &first, pos) {
StepCoercion::Value(v) => sum += v * v,
StepCoercion::Preserve => {
let args: Vec<Value> = nums.into_iter().map(Value::Number).collect();
return Ok(preserved_call("hypot", &args));
}
StepCoercion::Error(e) => return Err(e),
}
}
Ok(num_value(first.copy_units(sum.sqrt())))
}
fn trig(
fname: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
op: fn(f64) -> f64,
) -> Result<Value, Error> {
check_max_args(pos_args, named, 1, pos)?;
let n = require_num(&["number"], pos_args, named, 0, fname, pos)?;
let radians = angle_to_radians(&n, pos)?;
Ok(unitless(op(radians)))
}
fn inverse_trig(
fname: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
op: fn(f64) -> f64,
) -> Result<Value, Error> {
check_max_args(pos_args, named, 1, pos)?;
let n = require_num(&["number"], pos_args, named, 0, fname, pos)?;
no_unit(&n, pos)?;
Ok(degrees(op(n.value).to_degrees()))
}
fn atan2(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
check_max_args(pos_args, named, 2, pos)?;
let params = &["y", "x"];
let y = require_num(params, pos_args, named, 0, "atan2", pos)?;
let x = require_num(params, pos_args, named, 1, "atan2", pos)?;
verify_no_complex_units(&[&y, &x], pos)?;
if is_percent(&y) || is_percent(&x) {
return Ok(preserved_call("atan2", &[Value::Number(y), Value::Number(x)]));
}
let xv = match combine_into(&x, &y, pos) {
StepCoercion::Value(v) => v,
StepCoercion::Preserve => return Ok(preserved_call("atan2", &[Value::Number(y), Value::Number(x)])),
StepCoercion::Error(e) => return Err(e),
};
Ok(degrees(y.value.atan2(xv).to_degrees()))
}
fn is_percent(n: &Number) -> bool {
n.unit() == "%"
}
fn remainder(
fname: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
truncated: bool,
) -> Result<Value, Error> {
check_max_args(pos_args, named, 2, pos)?;
let params = &["dividend", "modulus"];
let a = require_num(params, pos_args, named, 0, fname, pos)?;
let b = require_num(params, pos_args, named, 1, fname, pos)?;
let bv = match combine_into(&b, &a, pos) {
StepCoercion::Value(v) => v,
StepCoercion::Preserve => return Ok(preserved_call(fname, &[Value::Number(a), Value::Number(b)])),
StepCoercion::Error(e) => return Err(e),
};
let value = if bv == 0.0 {
f64::NAN
} else if truncated {
a.value % bv
} else {
a.value - bv * (a.value / bv).floor()
};
Ok(num_value(a.copy_units(value)))
}
fn min_max(
fname: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
is_min: bool,
) -> Result<Value, Error> {
let args: Vec<Value> = all_args(pos_args, named)
.into_iter()
.map(normalize_const)
.collect();
if args.is_empty() {
return Err(Error::at("Missing argument.", pos));
}
match reduce_min_max(&args, is_min, pos)? {
Some(n) => Ok(num_value(n)),
None => {
verify_no_complex_args(&args, pos)?;
if let Some(bad) = args
.iter()
.find(|v| !matches!(v, Value::Number(_)) && !value_is_calc_substitution(v))
{
return Err(Error::at(format!("{} is not a number.", bad.to_css(false)), pos));
}
Ok(preserved_call(fname, &args))
}
}
}
fn value_is_calc_substitution(v: &Value) -> bool {
match v {
Value::Calc(_) => true,
Value::Str(s) => {
let t = s.text.trim_start().to_ascii_lowercase();
t.starts_with("var(")
|| t.starts_with("calc(")
|| t.starts_with("env(")
|| t.starts_with("min(")
|| t.starts_with("max(")
|| t.starts_with("clamp(")
}
_ => false,
}
}
fn clamp(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let args: Vec<Value> = all_args(pos_args, named)
.into_iter()
.map(normalize_const)
.collect();
if args.len() == 1 && !matches!(args[0], Value::Number(_)) {
return Ok(preserved_call("clamp", &args));
}
if args.len() != 3 {
return Err(Error::at(
format!("3 arguments required, but {} were passed.", args.len()),
pos,
));
}
let (lo, val, hi) = match (&args[0], &args[1], &args[2]) {
(Value::Number(a), Value::Number(b), Value::Number(c)) => (a, b, c),
_ => {
verify_no_complex_args(&args, pos)?;
return Ok(preserved_call("clamp", &args));
}
};
if lo.has_complex_units() || val.has_complex_units() || hi.has_complex_units() {
let coerce = |n: &Number| {
crate::value::unit_lists_factor(
(n.numer_units(), n.denom_units()),
(lo.numer_units(), lo.denom_units()),
)
.map(|f| n.value * f)
};
return match (coerce(val), coerce(hi)) {
(Some(val_v), Some(hi_v)) => {
let winner = if val_v < lo.value {
lo
} else if val_v > hi_v {
hi
} else {
val
};
Ok(num_value(winner.clone()))
}
_ => {
verify_no_complex_units(&[lo, val, hi], pos)?;
Ok(preserved_call("clamp", &args))
}
};
}
if crate::value::calc_units_incompatible(lo.unit(), val.unit()) {
return Err(incompatible(lo, val, pos));
}
if crate::value::calc_units_incompatible(lo.unit(), hi.unit()) {
return Err(incompatible(lo, hi, pos));
}
let val_v = match try_coerce(val, lo) {
Some(v) => v,
None => return Ok(preserved_call("clamp", &args)),
};
let hi_v = match try_coerce(hi, lo) {
Some(v) => v,
None => return Ok(preserved_call("clamp", &args)),
};
let winner = if val_v < lo.value {
lo
} else if val_v > hi_v {
hi
} else {
val
};
Ok(num_value(winner.clone()))
}
pub(super) fn module_round(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
if pos_args.len() > 1 {
return Err(Error::at(
format!("Only 1 argument allowed, but {} were passed.", pos_args.len()),
pos,
));
}
let v = super::require(&["number"], pos_args, named, 0, "round", pos)?;
let n = as_num(v, pos)?;
Ok(num_value(n.copy_units(n.value.round())))
}
pub(super) fn module_min_max(
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
is_min: bool,
) -> Result<Value, Error> {
let args = all_args(pos_args, named);
if args.is_empty() {
return Err(Error::at("At least one argument must be passed.", pos));
}
for v in &args {
if !matches!(v, Value::Number(_)) {
return Err(Error::at(format!("{} is not a number.", v.to_css(false)), pos));
}
}
match reduce_min_max(&args, is_min, pos)? {
Some(n) => Ok(num_value(n)),
None => {
let nums: Vec<&Number> = args
.iter()
.filter_map(|v| match v {
Value::Number(n) => Some(n),
_ => None,
})
.collect();
for (i, a) in nums.iter().enumerate() {
for b in &nums[i + 1..] {
if try_coerce(b, a).is_none() {
return Err(incompatible(a, b, pos));
}
}
}
Ok(num_value((*nums[0]).clone()))
}
}
}
pub(super) fn module_clamp(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
let params = ["min", "number", "max"];
if pos_args.len() > params.len() {
return Err(Error::at(
format!(
"Only {} arguments allowed, but {} were passed.",
params.len(),
pos_args.len()
),
pos,
));
}
let want = |i: usize, label: &str| -> Result<Number, Error> {
let v = super::require(¶ms, pos_args, named, i, "clamp", pos)?;
match v {
Value::Number(n) => Ok(n.clone()),
other => Err(Error::at(
format!("${label}: {} is not a number.", other.to_css(false)),
pos,
)),
}
};
let min = want(0, "min")?;
let number = want(1, "number")?;
let max = want(2, "max")?;
let number_v = coerce_for_clamp(&min, &number, "min", "number", pos)?;
let max_v = coerce_for_clamp(&min, &max, "min", "max", pos)?;
let winner = if min.value >= max_v || number_v <= min.value {
min
} else if number_v >= max_v {
max
} else {
number
};
Ok(num_value(winner))
}
fn coerce_for_clamp(
base: &Number,
n: &Number,
base_label: &str,
n_label: &str,
pos: Pos,
) -> Result<f64, Error> {
let unitless_mix = base.is_unitless() != n.is_unitless();
if unitless_mix {
return Err(Error::at(
format!(
"${n_label}: {} and ${base_label}: {} have incompatible units (one has units and the other doesn't).",
n.to_css(false),
base.to_css(false)
),
pos,
));
}
match try_coerce(n, base) {
Some(v) => Ok(v),
None => Err(incompatible(base, n, pos)),
}
}
fn random(pos_args: &[Value], named: &[(String, Value)], pos: Pos) -> Result<Value, Error> {
check_max_args(pos_args, named, 1, pos)?;
let r = next_random();
match super::arg(&["limit"], pos_args, named, 0) {
None => Ok(unitless(round_to_precision(r))),
Some(Value::Null) => Ok(unitless(round_to_precision(r))),
Some(v) => {
let n = as_num(v, pos)?;
let rounded = n.value.round();
if (n.value - rounded).abs() >= 1e-11 {
return Err(Error::at(
format!("$limit: {} is not an int.", n.to_css(false)),
pos,
));
}
if rounded < 1.0 {
return Err(Error::at(
format!("$limit: Must be greater than 0, was {}.", n.to_css(false)),
pos,
));
}
let pick = (r * rounded).floor() + 1.0;
Ok(unitless(pick.min(rounded)))
}
}
}
fn round_to_precision(x: f64) -> f64 {
(x * 1e10).round() / 1e10
}
fn next_random() -> f64 {
use std::cell::Cell;
use std::time::{SystemTime, UNIX_EPOCH};
thread_local! {
static STATE: Cell<u64> = const { Cell::new(0) };
}
STATE.with(|s| {
let mut x = s.get();
if x == 0 {
let seed = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0x9E37_79B9_7F4A_7C15);
x = seed | 1;
}
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
s.set(x);
let v = x.wrapping_mul(0x2545_F491_4F6C_DD1D);
((v >> 11) as f64) / ((1u64 << 53) as f64)
})
}
fn all_args(pos_args: &[Value], named: &[(String, Value)]) -> Vec<Value> {
let mut v: Vec<Value> = pos_args.to_vec();
v.extend(named.iter().map(|(_, val)| val.clone()));
v
}
fn normalize_const(v: Value) -> Value {
if let Value::Str(s) = &v {
if !s.quoted {
if let Some(n) = const_number(&s.text) {
return Value::Number(n);
}
}
}
v
}
fn reduce_min_max(args: &[Value], is_min: bool, pos: Pos) -> Result<Option<Number>, Error> {
let mut best: Option<Number> = None;
let mut simplified = true;
for v in args {
let n = match v {
Value::Number(n) => n.clone(),
_ => {
simplified = false;
break;
}
};
match &mut best {
None => best = Some(n),
Some(b) => match try_coerce(&n, b) {
Some(nv) => {
let pick_n = if is_min { nv < b.value } else { nv > b.value };
if pick_n {
*b = n;
}
}
None => {
simplified = false;
break;
}
},
}
}
if simplified {
return Ok(best);
}
let nums: Vec<&Number> = args
.iter()
.filter_map(|v| match v {
Value::Number(n) => Some(n),
_ => None,
})
.collect();
for i in 0..nums.len() {
for j in i + 1..nums.len() {
let (a, b) = (nums[i], nums[j]);
if a.has_complex_units() || b.has_complex_units() {
continue; }
let (ua, ub) = (a.unit(), b.unit());
if a.is_unitless() != b.is_unitless() || crate::value::calc_units_incompatible(ua, ub) {
return Err(incompatible(a, b, pos));
}
}
}
Ok(None)
}
fn try_coerce(n: &Number, target: &Number) -> Option<f64> {
if n.has_complex_units() || target.has_complex_units() {
return crate::value::unit_lists_factor(
(n.numer_units(), n.denom_units()),
(target.numer_units(), target.denom_units()),
)
.map(|f| n.value * f);
}
if n.unit().eq_ignore_ascii_case(target.unit()) || n.is_unitless() || target.is_unitless() {
return Some(n.value);
}
convert_factor(n.unit(), target.unit()).map(|f| n.value * f)
}
fn angle_to_radians(n: &Number, pos: Pos) -> Result<f64, Error> {
use std::f64::consts::PI;
let deg = match n.unit().to_ascii_lowercase().as_str() {
"" | "rad" => return Ok(n.value),
"deg" => n.value,
"grad" => n.value * 9.0 / 10.0,
"turn" => n.value * 360.0,
_ => {
return Err(Error::at(
format!(
"$number: Expected {} to have an angle unit (deg, grad, rad, turn).",
n.to_css(false)
),
pos,
))
}
};
Ok(deg * PI / 180.0)
}
fn collect_nums(
fname: &str,
pos_args: &[Value],
named: &[(String, Value)],
pos: Pos,
) -> Result<Vec<Number>, Error> {
let args = all_args(pos_args, named);
if args.is_empty() {
return Err(Error::at(format!("Missing argument for {fname}()."), pos));
}
let mut out = Vec::with_capacity(args.len());
for v in &args {
out.push(as_num(v, pos)?);
}
Ok(out)
}
fn const_number(text: &str) -> Option<Number> {
let value = match text.to_ascii_lowercase().as_str() {
"infinity" => f64::INFINITY,
"-infinity" => f64::NEG_INFINITY,
"nan" => f64::NAN,
"pi" => std::f64::consts::PI,
"e" => std::f64::consts::E,
_ => return None,
};
Some(Number::unitless(value))
}
fn as_num(v: &Value, pos: Pos) -> Result<Number, Error> {
match v {
Value::Number(n) => Ok(n.clone()),
Value::Str(s) if !s.quoted => match const_number(&s.text) {
Some(n) => Ok(n),
None => Err(Error::at(format!("{} is not a number.", s.text), pos)),
},
other => Err(Error::at(
format!("{} is not a number.", other.to_css(false)),
pos,
)),
}
}
fn require_num(
params: &[&str],
pos_args: &[Value],
named: &[(String, Value)],
i: usize,
fname: &str,
pos: Pos,
) -> Result<Number, Error> {
let v = super::require(params, pos_args, named, i, fname, pos)?;
as_num(v, pos)
}
fn num_value(n: Number) -> Value {
if n.value.is_finite() {
Value::Number(n)
} else {
Value::Calc(crate::value::CalcNode::Number(n))
}
}
fn no_unit(n: &Number, pos: Pos) -> Result<(), Error> {
if n.is_unitless() {
Ok(())
} else {
Err(Error::at(
format!("Expected {} to have no units.", n.to_css(false)),
pos,
))
}
}
fn unitless(value: f64) -> Value {
num_value(Number::unitless(value))
}
fn degrees(value: f64) -> Value {
num_value(Number::with_unit(value, "deg".to_string()))
}
fn incompatible(a: &Number, b: &Number, pos: Pos) -> Error {
Error::at(
format!("{} and {} are incompatible.", a.to_css(false), b.to_css(false)),
pos,
)
}
fn preserved_call(name: &str, args: &[Value]) -> Value {
Value::Calc(CalcNode::Func {
name: name.to_string(),
args: args.iter().map(value_to_calc_node).collect(),
})
}
fn value_to_calc_node(v: &Value) -> CalcNode {
match v {
Value::Number(n) => CalcNode::Number(n.clone()),
Value::Calc(node) => node.clone(),
other => CalcNode::Str(other.to_css(false)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scanner::Pos;
fn pos() -> Pos {
Pos { line: 1, col: 1 }
}
fn n(value: f64, unit: &str) -> Value {
Value::Number(Number::with_unit(value, unit.to_string()))
}
fn call(name: &str, args: &[Value]) -> Value {
try_call(name, args, &[], pos())
.expect("name owned by math family")
.expect("no error")
}
fn err(name: &str, args: &[Value]) -> bool {
try_call(name, args, &[], pos()).expect("name owned").is_err()
}
#[test]
fn abs_keeps_unit_and_magnitude() {
assert_eq!(call("abs", &[n(-3.0, "px")]).to_css(false), "3px");
assert_eq!(call("abs", &[n(3.0, "")]).to_css(false), "3");
}
#[test]
fn ceil_and_floor_keep_unit() {
assert_eq!(call("ceil", &[n(4.2, "px")]).to_css(false), "5px");
assert_eq!(call("floor", &[n(4.8, "%")]).to_css(false), "4%");
}
#[test]
fn round_is_half_away_from_zero() {
assert_eq!(call("round", &[n(2.5, "")]).to_css(false), "3");
assert_eq!(call("round", &[n(-2.5, "deg")]).to_css(false), "-3deg");
assert_eq!(call("round", &[n(0.5, "")]).to_css(false), "1");
}
#[test]
fn round_strategies_with_step() {
let kw = |s: &str| {
Value::Str(SassStr {
text: s.to_string(),
quoted: false,
})
};
assert_eq!(
call("round", &[kw("nearest"), n(117.0, "px"), n(25.0, "px")]).to_css(false),
"125px"
);
assert_eq!(
call("round", &[kw("up"), n(101.0, "px"), n(25.0, "px")]).to_css(false),
"125px"
);
assert_eq!(
call("round", &[kw("down"), n(122.0, "px"), n(25.0, "px")]).to_css(false),
"100px"
);
assert_eq!(
call("round", &[kw("to-zero"), n(-120.0, "px"), n(-25.0, "px")]).to_css(false),
"-125px"
);
assert_eq!(call("round", &[n(117.0, ""), n(25.0, "")]).to_css(false), "125");
assert_eq!(
call("round", &[n(117.0, "cm"), n(25.0, "mm")]).to_css(false),
"117.5cm"
);
assert_eq!(
call("round", &[kw("nearest"), n(10.0, "px"), n(0.0, "px")]).to_css(false),
"calc(NaN * 1px)"
);
assert_eq!(
call("round", &[kw("nearest"), kw("infinity"), n(5.0, "")]).to_css(false),
"calc(infinity)"
);
assert!(err("round", &[kw("nearest"), n(5.0, "")]));
assert!(err("round", &[n(10.0, "px"), n(5.0, "")]));
assert!(err("round", &[]));
}
#[test]
fn min_max_pick_winning_argument_unit() {
assert_eq!(call("min", &[n(1.0, "px"), n(2.0, "px")]).to_css(false), "1px");
assert_eq!(
call("max", &[n(1.0, ""), n(2.0, ""), n(3.0, "")]).to_css(false),
"3"
);
assert_eq!(call("min", &[n(1.0, "in"), n(2.0, "cm")]).to_css(false), "2cm");
assert_eq!(call("min", &[n(1.0, "vw")]).to_css(false), "1vw");
}
#[test]
fn min_max_preserve_css_form_on_incompatible() {
assert_eq!(
call("min", &[n(1.0, "px"), n(2.0, "vw")]).to_css(false),
"min(1px, 2vw)"
);
let var = Value::Str(SassStr {
text: "var(--x)".into(),
quoted: false,
});
assert_eq!(
call("min", &[n(1.0, "px"), var]).to_css(false),
"min(1px, var(--x))"
);
}
#[test]
fn clamp_orders_min_value_max() {
assert_eq!(
call("clamp", &[n(1.0, "px"), n(5.0, "px"), n(3.0, "px")]).to_css(false),
"3px"
);
assert_eq!(
call("clamp", &[n(1.0, "px"), n(0.0, "px"), n(3.0, "px")]).to_css(false),
"1px"
);
let var = Value::Str(SassStr {
text: "var(--x)".into(),
quoted: false,
});
assert_eq!(
call("clamp", &[n(1.0, "px"), var, n(3.0, "px")]).to_css(false),
"clamp(1px, var(--x), 3px)"
);
}
#[test]
fn pow_sqrt_exp_log_are_unitless() {
assert_eq!(call("pow", &[n(2.0, ""), n(3.0, "")]).to_css(false), "8");
assert_eq!(call("sqrt", &[n(4.0, "")]).to_css(false), "2");
assert_eq!(call("exp", &[n(0.0, "")]).to_css(false), "1");
assert_eq!(call("log", &[n(8.0, ""), n(2.0, "")]).to_css(false), "3");
assert!(err("sqrt", &[n(4.0, "px")]));
assert!(err("pow", &[n(2.0, "px"), n(2.0, "")]));
}
#[test]
fn trig_accepts_angles_and_returns_unitless() {
assert_eq!(call("sin", &[n(30.0, "deg")]).to_css(false), "0.5");
assert_eq!(call("cos", &[n(0.0, "")]).to_css(false), "1");
assert_eq!(call("tan", &[n(45.0, "deg")]).to_css(false), "1");
assert_eq!(call("sin", &[n(100.0, "grad")]).to_css(false), "1");
assert_eq!(call("sin", &[n(0.25, "turn")]).to_css(false), "1");
assert!(err("sin", &[n(1.0, "px")]));
}
#[test]
fn inverse_trig_returns_degrees() {
assert_eq!(call("asin", &[n(0.5, "")]).to_css(false), "30deg");
assert_eq!(call("acos", &[n(1.0, "")]).to_css(false), "0deg");
assert_eq!(call("atan", &[n(1.0, "")]).to_css(false), "45deg");
assert_eq!(call("atan2", &[n(1.0, ""), n(1.0, "")]).to_css(false), "45deg");
}
#[test]
fn sign_keeps_units() {
assert_eq!(call("sign", &[n(-5.0, "px")]).to_css(false), "-1px");
assert_eq!(call("sign", &[n(0.0, "px")]).to_css(false), "0px");
assert_eq!(call("sign", &[n(3.0, "")]).to_css(false), "1");
assert_eq!(call("sign", &[n(7.0, "%")]).to_css(false), "sign(7%)");
}
#[test]
fn hypot_converts_onto_first_unit() {
assert_eq!(call("hypot", &[n(3.0, ""), n(4.0, "")]).to_css(false), "5");
assert_eq!(call("hypot", &[n(3.0, "px"), n(4.0, "px")]).to_css(false), "5px");
assert!(err("hypot", &[n(3.0, "px"), n(4.0, "")]));
}
#[test]
fn rem_and_mod_signs() {
assert_eq!(call("rem", &[n(10.0, ""), n(3.0, "")]).to_css(false), "1");
assert_eq!(call("rem", &[n(-10.0, ""), n(3.0, "")]).to_css(false), "-1");
assert_eq!(call("mod", &[n(-10.0, ""), n(3.0, "")]).to_css(false), "2");
assert_eq!(call("rem", &[n(10.0, "px"), n(3.0, "pt")]).to_css(false), "2px");
assert!(err("rem", &[n(10.0, "px"), n(3.0, "")]));
}
#[test]
fn rejects_non_numbers_and_unknown_names() {
let e = try_call("abs", &[Value::Bool(true)], &[], pos());
assert!(e.is_some());
assert!(e.expect("some").is_err());
assert!(try_call("definitely-not-math", &[n(4.0, "")], &[], pos()).is_none());
}
}