rsass 0.25.2

Sass implementation in pure rust (not complete yet)
Documentation
use super::{
    check, expected_to, get_checked, get_numeric, get_opt_check, get_va_list,
    is_not, is_special, CheckedArg, Error, FunctionMap, Scope,
};
use crate::css::{CallArgs, CssString, Value};
use crate::output::Format;
use crate::sass::Name;
use crate::value::{Number, Numeric, Quotes, Rational, Unit, UnitSet};
use crate::ScopeRef;
use std::cmp::Ordering;
use std::f64::consts::{E, PI};

/// Create the `sass:math` standard module.
///
/// Should conform to
/// [the specification](https://sass-lang.com/documentation/modules/math).
pub fn create_module() -> Scope {
    let mut f = Scope::builtin_module("sass:math");

    def!(f, div(number1, number2), |s| {
        let a = s.get(&name!(number1))?;
        let b = s.get(&name!(number2))?;
        use crate::value::Operator;
        match (a, b) {
            (Value::Color(a, _), Value::Numeric(b, _)) if b.is_no_unit() => {
                let bn = b
                    .as_ratio()
                    .map_err(|e| Error::BadValue(e.to_string()))?;
                Ok((a.to_rgba().as_ref() / bn).into())
            }
            (Value::Numeric(ref a, _), Value::Numeric(ref b, _)) => {
                Ok((a / b).into())
            }
            (a, b) => Ok(Value::BinOp(
                Box::new(a),
                false,
                Operator::Div,
                false,
                Box::new(b),
            )
            .format(Format::introspect())
            .to_string()
            .into()),
        }
    });
    // - - - Boundig Functions - - -
    def!(f, ceil(number), |s| {
        let val = get_numeric(s, "number")?;
        Ok(number(val.value.ceil(), val.unit))
    });
    def!(f, clamp(min, number, max), clamp_fn);
    def!(f, floor(number), |s| {
        let val = get_numeric(s, "number")?;
        Ok(number(val.value.floor(), val.unit))
    });
    def_va!(f, max(numbers), |s| {
        find_extreme(&get_va_list(s, name!(numbers))?, Ordering::Greater)
    });
    def_va!(f, min(numbers), |s| {
        find_extreme(&get_va_list(s, name!(numbers))?, Ordering::Less)
    });
    def!(f, round(number), |s| {
        let val = get_numeric(s, "number")?;
        Ok(number(val.value.round(), val.unit))
    });

    // - - - Distance Functions - - -
    def!(f, abs(number), |s| {
        let v = get_numeric(s, "number")?;
        Ok(number(v.value.abs(), v.unit))
    });
    def_va!(f, hypot(number), |s| match get_va_list(s, name!(number))?
        .as_slice()
    {
        [Value::Numeric(v, _)] =>
            Ok(number(v.value.clone().abs(), v.unit.clone())),
        [v] => Err(is_not(v, "a number")).named(name!(number)),
        v => {
            if let Some((first, rest)) = v.split_first() {
                let first = as_numeric(first)?;
                let mut sum = f64::from(first.value.clone()).powi(2);
                let unit = first.unit.clone();
                for (i, v) in rest.iter().enumerate() {
                    let num = as_numeric(v)?;
                    let scaled = num
                        .as_unitset(&unit)
                        .ok_or_else(|| {
                            diff_units_msg(&num, &first, "numbers[1]".into())
                        })
                        .named(format!("numbers[{}]", i + 2).into())?;
                    sum += f64::from(scaled).powi(2);
                }
                Ok(number(sum.sqrt(), unit))
            } else {
                Err(Error::error("At least one argument must be passed."))
            }
        }
    });

    // - - - Exponential Functions - - -
    def!(f, log(number, base = b"null"), |s| {
        let num = get_unitless(s, "number")?;
        let base = get_opt_check(s, name!(base), check::unitless)?
            .map(Into::into)
            .unwrap_or(E);
        Ok(Value::scalar(num.log(base)))
    });
    def!(f, pow(base, exponent), |s| {
        let base = get_unitless(s, "base")?;
        let exponent = get_unitless(s, "exponent")?;
        let result =
            if exponent.is_infinite() && (base.abs() - 1.0).abs() < 1e-7 {
                f64::NAN
            } else {
                base.powf(exponent)
            };
        Ok(Value::scalar(result))
    });
    def!(f, sqrt(number), |s| {
        Ok(Value::scalar(get_unitless(s, "number")?.sqrt()))
    });

    // - - - Trigonometric Functions - - -
    def!(f, cos(number), |s| {
        Ok(Value::scalar(get_radians(s, "number")?.cos()))
    });
    def!(f, sin(number), |s| {
        Ok(Value::scalar(get_radians(s, "number")?.sin()))
    });
    def!(f, tan(number), |s| {
        let ans = get_radians(s, "number")?.tan();
        let ans = if ans.abs() > 1e15 {
            ans.signum() * f64::INFINITY
        } else {
            ans
        };
        Ok(Value::scalar(ans))
    });

    def!(f, acos(number), |s| {
        Ok(deg_value(get_unitless(s, "number")?.acos()))
    });
    def!(f, asin(number), |s| {
        Ok(deg_value(get_unitless(s, "number")?.asin()))
    });
    def!(f, atan(number), |s| {
        Ok(deg_value(get_unitless(s, "number")?.atan()))
    });
    def!(f, atan2(y, x), |s| {
        let y = get_numeric(s, "y")?;
        let x = get_checked(s, name!(x), |v| {
            let v = check::numeric(v)?;
            v.as_unitset(&y.unit)
                .ok_or_else(|| diff_units_msg(&v, &y, name!(y)))
        })?;
        Ok(deg_value(f64::from(y.value).atan2(f64::from(x))))
    });

    // - - - Unit Functions - - -
    def!(f, compatible(number1, number2), |s| {
        let u1 = get_numeric(s, "number1")?.unit;
        let u2 = get_numeric(s, "number2")?.unit;
        Ok(u1.is_compatible(&u2).into())
    });
    def!(f, is_unitless(number), |s| {
        Ok((get_numeric(s, "number")?.is_no_unit()).into())
    });
    def!(f, unit(number), |s| {
        let mut unit = get_numeric(s, "number")?.unit;
        unit.simplify();
        Ok(CssString::new(unit.to_string(), Quotes::Double).into())
    });

    // - - - Other Functions - - -
    def!(f, percentage(number), |s| {
        let val = get_checked(s, name!(number), check::unitless)?;
        Ok(Numeric::new(val * 100, Unit::Percent).into())
    });
    def!(f, random(limit = b"null"), |s| {
        match get_opt_check(s, name!(limit), |v| {
            let v = check::int(v)?;
            if v > 0 {
                Ok(v)
            } else {
                Err(format!("Must be greater than 0, was {}.", v))
            }
        })? {
            None => {
                let rez = 1_000_000;
                Ok(Value::scalar(Rational::new(intrand(rez), rez)))
            }
            Some(bound) => Ok(Value::scalar(intrand(bound) + 1)),
        }
    });

    f.set_variable(name!(pi), Value::scalar(PI), false, false);
    f.set_variable(name!(e), Value::scalar(E), false, false);
    f
}

pub fn expose(m: &Scope, global: &mut FunctionMap) {
    for (gname, lname) in &[
        // - - - Boundig Functions - - -
        (name!(ceil), name!(ceil)),
        (name!(floor), name!(floor)),
        (name!(max), name!(max)),
        (name!(min), name!(min)),
        (name!(round), name!(round)),
        // - - - Distance Functions - - -
        (name!(abs), name!(abs)),
        // Exponential and trigonometric functions are not exposed

        // - - - Unit Functions - - -
        (name!(comparable), name!(compatible)),
        (name!(unitless), name!(is_unitless)),
        (name!(unit), name!(unit)),
        // - - - Other Functions - - -
        (name!(percentage), name!(percentage)),
        (name!(random), name!(random)),
    ] {
        global.insert(gname.clone(), m.get_lfunction(lname));
    }
}

fn get_radians(s: &Scope, name: &str) -> Result<f64, Error> {
    get_checked(s, name.into(), |v| {
        let v = check::numeric(v)?;
        v.as_unit_def(Unit::Rad).map(Into::into).ok_or_else(|| {
            expected_to(&v, "have an angle unit (deg, grad, rad, turn)")
        })
    })
}

fn get_unitless(s: &Scope, name: &str) -> Result<f64, Error> {
    get_checked(s, name.into(), |v| Ok(check::unitless(v)?.into()))
}

// Only used by hypot function, which treats arguments as unnamed.
fn as_numeric(v: &Value) -> Result<Numeric, Error> {
    check::numeric(v.clone()).map_err(Error::error)
}

fn number(v: impl Into<Number>, unit: impl Into<UnitSet>) -> Value {
    Numeric::new(v.into(), unit).into()
}

/// convert f64 in radians (used by rust) to numeric Value in degrees
/// (used by sass).
fn deg_value(rad: f64) -> Value {
    number(rad.to_degrees(), Unit::Deg)
}

fn find_extreme(v: &[Value], pref: Ordering) -> Result<Value, Error> {
    let as_call = || {
        Value::Call(
            if pref == Ordering::Greater {
                "max"
            } else {
                "min"
            }
            .into(),
            CallArgs::from_list(v.to_vec()),
        )
    };
    if v.iter().any(is_special) {
        return Ok(as_call());
    }
    match find_extreme_inner(v, pref) {
        Ok(Some(v)) => Ok(v.into()),
        Ok(None) => {
            Err(Error::error("At least one argument must be passed."))
        }
        Err(ExtremeError::NonNumeric(v)) => {
            if v.type_name() == "unknown" {
                Ok(as_call())
            } else {
                Err(Error::error(is_not(&v, "a number")))
            }
        }
        Err(ExtremeError::Incompatible(a, b)) => {
            let a_dim = a.unit.css_dimension();
            let b_dim = b.unit.css_dimension();
            if a_dim.is_empty() || b_dim.is_empty() || a_dim == b_dim {
                Ok(as_call())
            } else {
                Err(Error::error(format!(
                    "{} and {} have incompatible units.",
                    a.format(Format::introspect()),
                    b.format(Format::introspect()),
                )))
            }
        }
        Err(_) => Ok(as_call()),
    }
}

fn find_extreme_inner(
    v: &[Value],
    pref: Ordering,
) -> Result<Option<Numeric>, ExtremeError> {
    if let Some((first, rest)) = v.split_first() {
        let va = check::numeric(first.clone())
            .map_err(|_| ExtremeError::NonNumeric(first.clone()))?;
        if let Some(vb) = find_extreme_inner(rest, pref)? {
            if let Some(o) = va.partial_cmp(&vb) {
                Ok(Some(if o == pref { va } else { vb }))
            } else if va.is_no_unit() || vb.is_no_unit() {
                if let Some(o) = va.value.partial_cmp(&vb.value) {
                    Ok(Some(if o == pref { va } else { vb }))
                } else {
                    Err(ExtremeError::Incomparable(va, vb))
                }
            } else {
                Err(ExtremeError::Incompatible(va, vb))
            }
        } else {
            Ok(Some(va))
        }
    } else {
        Ok(None)
    }
}

#[derive(Debug)]
enum ExtremeError {
    NonNumeric(Value),
    Incompatible(Numeric, Numeric),
    Incomparable(Numeric, Numeric),
}

fn intrand(lim: i64) -> i64 {
    fastrand::i64(0..lim)
}

fn diff_units_msg(
    one: &Numeric,
    other: &Numeric,
    other_name: Name,
) -> String {
    format!(
        "{} and ${}: {} have incompatible units{}.",
        one.format(Format::introspect()),
        other_name,
        other.format(Format::introspect()),
        if one.is_no_unit() || other.is_no_unit() {
            " (one has units and the other doesn't)"
        } else {
            ""
        }
    )
}

pub(crate) fn clamp_fn(s: &ScopeRef) -> Result<Value, Error> {
    let min_v = get_numeric(s, "min")?;
    let check_numeric_compat_unit = |v: Value| -> Result<Numeric, String> {
        let v = check::numeric(v)?;
        if (v.is_no_unit() != min_v.is_no_unit())
            || !v.unit.is_compatible(&min_v.unit)
        {
            return Err(diff_units_msg(&v, &min_v, name!(min)));
        }
        Ok(v)
    };
    let mut num = get_checked(s, name!(number), check_numeric_compat_unit)?;
    let max_v = get_checked(s, name!(max), check_numeric_compat_unit)?;

    if num >= max_v {
        num = max_v;
    }
    if num <= min_v {
        num = min_v;
    }
    Ok(Value::Numeric(num, true))
}