Skip to content

Commit

Permalink
Implement css-style round function.
Browse files Browse the repository at this point in the history
  • Loading branch information
kaj committed Oct 2, 2023
1 parent ecd38b5 commit 821599f
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 170 deletions.
51 changes: 33 additions & 18 deletions rsass/src/sass/functions/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ use super::{
check, css_dim, expected_to, is_not, is_special, CallError, CheckedArg,
FunctionMap, ResolvedArgs, Scope,
};
use crate::css::{BinOp, CallArgs, CssString, Value};
use crate::css::{BinOp, CallArgs, CssString, InvalidCss, Value};
use crate::output::Format;
use crate::parser::input_span;
use crate::sass::functions::css_fn_arg;
use crate::sass::functions::{css_fn_arg, known_dim};
use crate::sass::Name;
use crate::value::{Number, Numeric, Quotes, Rational, Unit, UnitSet};
use std::cmp::Ordering;
use std::f64::consts::{E, PI};
use std::ops::Rem;

mod round;

/// Create the `sass:math` standard module.
///
/// Should conform to
Expand Down Expand Up @@ -47,10 +49,7 @@ pub fn create_module() -> Scope {
let numbers = s.get_map(name!(numbers), check::va_list)?;
find_extreme(&numbers, Ordering::Less)
});
def!(f, round(number), |s| {
let val: Numeric = s.get(name!(number))?;
Ok(number(val.value.round(), val.unit))
});
def!(f, round(number), round::sass_round);

// - - - Distance Functions - - -
def!(f, abs(number), |s| {
Expand Down Expand Up @@ -248,7 +247,6 @@ pub fn expose(m: &Scope, global: &mut FunctionMap) {
(name!(floor), name!(floor)),
(name!(max), name!(max)),
(name!(min), name!(min)),
(name!(round), name!(round)),
// - - - Distance Functions - - -
(name!(abs), name!(abs)),
// - - - Exponential functions - - -
Expand All @@ -275,6 +273,31 @@ pub fn expose(m: &Scope, global: &mut FunctionMap) {
}

// Functions behave somewhat differently in the global scope vs in the math module.
def!(global, clamp(min, number = b"null", max = b"null"), |s| {
clamp_fn(s).or_else(|_| {
let mut args = vec![s.get::<Value>(name!(min))?];
if let Some(b) = s.get_opt(name!(number))? {
args.push(b);
}
if let Some(c) = s.get_opt(name!(max))? {
args.push(c);
}
if let Some((a, rest)) = args.split_first() {
if let Some(adim) = css_dim(a) {
for b in rest {
if let Some(bdim) = css_dim(b) {
if adim != bdim {
return Err(CallError::incompatible_values(
a, b,
));
}
}
}
}
}
Ok(Value::Call("clamp".into(), CallArgs::from_list(args)))
})
});
def!(global, atan2(y, x), |s| {
fn real_atan2(s: &ResolvedArgs) -> Result<Value, CallError> {
let y: Numeric = s.get(name!(y))?;
Expand Down Expand Up @@ -345,6 +368,7 @@ pub fn expose(m: &Scope, global: &mut FunctionMap) {
fallback2a(s, "mod", name!(y), name!(x)).map_err(|_| e)
})
});
def_va!(global, round(kwargs), round::css_round);
def!(global, sign(v), |s| {
fn real_sign(s: &ResolvedArgs) -> Result<Value, CallError> {
let v: Numeric = s.get(name!(v))?;
Expand Down Expand Up @@ -524,11 +548,7 @@ fn find_extreme(v: &[Value], pref: Ordering) -> Result<Value, CallError> {
if a_dim.is_empty() || b_dim.is_empty() || a_dim == b_dim {
Ok(as_call())
} else {
Err(CallError::msg(format!(
"{} and {} have incompatible units.",
a.format(Format::introspect()),
b.format(Format::introspect()),
)))
Err(CallError::msg(InvalidCss::Incompat(a, b)))
}
}
Err(_) => Ok(as_call()),
Expand Down Expand Up @@ -593,14 +613,9 @@ fn diff_units_msg(

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

Expand Down
196 changes: 196 additions & 0 deletions rsass/src/sass/functions/math/round.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
use super::{
diff_units_msg2, known_dim, number, CallArgs, CallError, ResolvedArgs,
};
use crate::css::{CssString, Value};
use crate::output::Format;
use crate::sass::functions::color::eval_inner;
use crate::sass::FormalArgs;
use crate::value::{Number, Numeric, Quotes};

pub fn sass_round(s: &ResolvedArgs) -> Result<Value, CallError> {
let val: Numeric = s.get(name!(number))?;
Ok(number(val.value.round(), val.unit))
}

pub fn css_round(s: &ResolvedArgs) -> Result<Value, CallError> {
let args = s.get_map(name!(kwargs), CallArgs::from_value)?;
if !args.named.is_empty() {
let fa = FormalArgs::new(vec![one_arg!(number)]);
return sass_round(&eval_inner(&name!(round), &fa, s, args)?);
}
if args.positional.len() > 3 {
return Err(CallError::msg(format!(
"Only 3 arguments allowed, but {} were passed.",
args.positional.len(),
)));
}
let mut args = args.positional.into_iter();
let (strategy, number, step) =
match (args.next(), args.next(), args.next()) {
(Some(v0), Some(v1), v2) => {
match (Strategy::try_from(&v0), v1, v2) {
(Ok(_), num, None) if num.type_name() == "number" => {
return Err(CallError::msg(
"If strategy is not null, step is required.",
))
}
(Ok(s), num, None) => (Some(s), num, None),
(Ok(s), num, Some(step)) => (Some(s), num, Some(step)),
(Err(()), num, Some(step)) => {
if v0.type_name() == "variable" {
return fallback(Some(v0), num, Some(step));
} else {
return Err(CallError::msg(format!(
"{} must be either nearest, up, down or to-zero.",
v0.format(Format::introspect()),
)));
};
}
(Err(()), step, None) => (None, v0, Some(step)),
}
}
(Some(v), None, _) => (None, v, None),
(None, ..) => {
return Err(CallError::msg("Missing argument."));
}
};
real_round(strategy.unwrap_or_default(), &number, step.as_ref())
.unwrap_or_else(|| fallback(strategy.map(Value::from), number, step))
}

fn real_round(
strategy: Strategy,
num: &Value,
step: Option<&Value>,
) -> Option<Result<Value, CallError>> {
let val = Numeric::try_from(num.clone()).ok()?;
let step = match step.cloned().map(Numeric::try_from) {
Some(Ok(v)) => {
if let Some(step) = v.as_unitset(&val.unit) {
Some(step)
} else if known_dim(&val)
.and_then(|dim1| known_dim(&v).map(|dim2| dim1 == dim2))
.unwrap_or(true)
{
return None;
} else {
return Some(Err(CallError::msg(diff_units_msg2(&val, &v))));
}
}
Some(Err(_)) => return None,
None => None,
};
let (val, unit) = (val.value, val.unit);
Some(Ok(number(
if let Some(step) = step {
if step.is_finite() {
if (strategy == Strategy::ToZero) && step.is_negative() {
(&val / &step).abs().ceil() * step.abs() * val.signum()
} else {
strategy.apply(val / step.abs()) * step.abs()
}
} else if val.is_finite() {
if strategy == Strategy::Up && val > 0.into() {
Number::from(f64::INFINITY)
} else if strategy == Strategy::Down && val < 0.into() {
Number::from(f64::NEG_INFINITY)
} else {
val.signum() / Number::from(f64::INFINITY)
}
} else {
Number::from(f64::NAN)
}
} else {
strategy.apply(val)
},
unit,
)))
}

fn fallback(
strategy: Option<Value>,
number: Value,
step: Option<Value>,
) -> Result<Value, CallError> {
let mut args = Vec::new();
let is_single = strategy.is_none() && step.is_none();
if let Some(v) = strategy {
args.push(v)
}
if is_single && matches!(&number, Value::BinOp(_)) {
return Err(CallError::msg(format!(
"Single argument {} expected to be simplifiable.",
number.format(Format::introspect()),
)));
}
args.push(super::css_fn_arg(number)?);
if let Some(step) = step {
args.push(super::css_fn_arg(step)?);
}
Ok(Value::Call("round".into(), CallArgs::from_list(args)))
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Strategy {
Nearest,
Up,
ToZero,
Down,
}

impl Strategy {
fn apply(&self, val: Number) -> Number {
match self {
Self::Nearest => val.round(),
Self::Up => val.ceil(),
Self::ToZero => val.trunc(),
Self::Down => val.floor(),
}
}
}

impl Default for Strategy {
fn default() -> Self {
Self::Nearest
}
}

impl TryFrom<&CssString> for Strategy {
type Error = ();

fn try_from(value: &CssString) -> Result<Self, Self::Error> {
if value.quotes() != Quotes::None {
return Err(());
}
match value.value() {
"nearest" => Ok(Self::Nearest),
"up" => Ok(Self::Up),
"to-zero" | "to_zero" => Ok(Self::ToZero),
"down" => Ok(Self::Down),
_ => Err(()),
}
}
}

impl TryFrom<&Value> for Strategy {
type Error = ();

fn try_from(value: &Value) -> Result<Self, Self::Error> {
if let Value::Literal(s) = value {
s.try_into()
} else {
Err(())
}
}
}

impl From<Strategy> for Value {
fn from(value: Strategy) -> Self {
Value::from(match value {
Strategy::Nearest => "nearest",
Strategy::Up => "up",
Strategy::ToZero => "to-zero",
Strategy::Down => "down",
})
}
}
47 changes: 10 additions & 37 deletions rsass/src/sass/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use super::{Call, Closure, FormalArgs, Name};
use crate::css::{self, is_not, BinOp, CallArgs, CssString, Value};
use crate::input::SourcePos;
use crate::output::{Format, Formatted};
use crate::value::{CssDimensionSet, Operator, Quotes};
use crate::value::{CssDimensionSet, Numeric, Operator, Quotes};
use crate::{Scope, ScopeRef};
use lazy_static::lazy_static;
use std::collections::BTreeMap;
Expand Down Expand Up @@ -301,34 +301,6 @@ lazy_static! {
Ok(Value::Call("calc".into(), CallArgs::from_single(arg)))
}
});
def!(f, clamp(min, number = b"null", max = b"null"), |s| {
self::math::clamp_fn(s).or_else(|_| {
let mut args = vec![s.get::<Value>(name!(min))?];
if let Some(b) = s.get_opt(name!(number))? {
args.push(b);
}
if let Some(c) = s.get_opt(name!(max))? {
args.push(c);
}
if let Some((a, rest)) = args.split_first() {
if let Some(adim) = css_dim(a) {
for b in rest {
if let Some(bdim) = css_dim(b) {
if adim != bdim {
return Err(
CallError::incompatible_values(a, b),
);
}
}
}
}
}
Ok(css::Value::Call(
"clamp".into(),
css::CallArgs::from_list(args),
))
})
});
color::expose(MODULES.get("sass:color").unwrap(), &mut f);
list::expose(MODULES.get("sass:list").unwrap(), &mut f);
map::expose(MODULES.get("sass:map").unwrap(), &mut f);
Expand Down Expand Up @@ -387,17 +359,18 @@ fn css_fn_arg(v: Value) -> Result<Value, CallError> {
fn css_dim(v: &Value) -> Option<CssDimensionSet> {
match v {
// TODO: Handle BinOp recursively (again) (or let in_calc return (Value, CssDimension)?)
Value::Numeric(num, _) => {
let u = &num.unit;
if u.is_known() && !u.is_percent() {
Some(u.css_dimension())
} else {
None
}
}
Value::Numeric(num, _) => known_dim(num),
_ => None,
}
}
fn known_dim(v: &Numeric) -> Option<CssDimensionSet> {
let u = &v.unit;
if u.is_known() && !u.is_percent() {
Some(u.css_dimension())
} else {
None
}
}

// argument helpers for the actual functions

Expand Down
Loading

0 comments on commit 821599f

Please sign in to comment.