implement builtin sass:math function clamp

This commit is contained in:
Connor Skees 2020-07-26 13:12:35 -04:00
parent 3fae0a9621
commit eeb0b0a924
7 changed files with 216 additions and 57 deletions

View File

@ -7,7 +7,6 @@ use crate::{
common::{Identifier, QuoteKind}, common::{Identifier, QuoteKind},
error::SassResult, error::SassResult,
parse::Parser, parse::Parser,
unit::Unit,
value::{SassFunction, Value}, value::{SassFunction, Value},
}; };
@ -73,12 +72,7 @@ pub(crate) fn type_of(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult
pub(crate) fn unitless(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn unitless(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
#[allow(clippy::match_same_arms)] Ok(Value::bool(args.get_err(0, "number")?.unitless()))
Ok(match args.get_err(0, "number")? {
Value::Dimension(_, Unit::None, _) => Value::True,
Value::Dimension(..) => Value::False,
_ => Value::True,
})
} }
pub(crate) fn inspect(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn inspect(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {

View File

@ -1,3 +1,5 @@
use std::cmp::Ordering;
use crate::{ use crate::{
args::CallArgs, args::CallArgs,
builtin::{ builtin::{
@ -5,6 +7,7 @@ use crate::{
meta::{unit, unitless}, meta::{unit, unitless},
modules::Module, modules::Module,
}, },
common::Op,
error::SassResult, error::SassResult,
parse::Parser, parse::Parser,
unit::Unit, unit::Unit,
@ -16,7 +19,84 @@ use crate::builtin::math::random;
fn clamp(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { fn clamp(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
todo!() let span = args.span();
let min = match args.get_err(0, "min")? {
v @ Value::Dimension(..) => v,
v => {
return Err((
format!("$min: {} is not a number.", v.inspect(args.span())?),
span,
)
.into())
}
};
let number = match args.get_err(1, "number")? {
v @ Value::Dimension(..) => v,
v => {
return Err((
format!("$number: {} is not a number.", v.inspect(span)?),
span,
)
.into())
}
};
let max = match args.get_err(2, "max")? {
v @ Value::Dimension(..) => v,
v => return Err((format!("$max: {} is not a number.", v.inspect(span)?), span).into()),
};
// ensure that `min` and `max` are compatible
min.cmp(&max, span, Op::LessThan)?;
let min_unit = match min {
Value::Dimension(_, ref u, _) => u,
_ => unreachable!(),
};
let number_unit = match number {
Value::Dimension(_, ref u, _) => u,
_ => unreachable!(),
};
let max_unit = match max {
Value::Dimension(_, ref u, _) => u,
_ => unreachable!(),
};
if min_unit == &Unit::None && number_unit != &Unit::None {
return Err((
format!(
"$min is unitless but $number has unit {}. Arguments must all have units or all be unitless.",
number_unit
), span).into());
} else if min_unit != &Unit::None && number_unit == &Unit::None {
return Err((
format!(
"$min has unit {} but $number is unitless. Arguments must all have units or all be unitless.",
min_unit
), span).into());
} else if min_unit != &Unit::None && max_unit == &Unit::None {
return Err((
format!(
"$min has unit {} but $max is unitless. Arguments must all have units or all be unitless.",
min_unit
), span).into());
}
match min.cmp(&number, span, Op::LessThan)? {
Ordering::Greater => return Ok(min),
Ordering::Equal => return Ok(number),
Ordering::Less => {}
}
match max.cmp(&number, span, Op::GreaterThan)? {
Ordering::Less => return Ok(max),
Ordering::Equal => return Ok(number),
Ordering::Greater => {}
}
Ok(number)
} }
fn hypot(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { fn hypot(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
@ -84,6 +164,7 @@ pub(crate) fn declare(f: &mut Module) {
f.insert_builtin("is-unitless", unitless); f.insert_builtin("is-unitless", unitless);
f.insert_builtin("unit", unit); f.insert_builtin("unit", unit);
f.insert_builtin("percentage", percentage); f.insert_builtin("percentage", percentage);
f.insert_builtin("clamp", clamp);
#[cfg(feature = "random")] #[cfg(feature = "random")]
f.insert_builtin("random", random); f.insert_builtin("random", random);

View File

@ -735,53 +735,9 @@ impl<'a, 'b: 'a> ValueVisitor<'a, 'b> {
HigherIntermediateValue::Literal(v) => v, HigherIntermediateValue::Literal(v) => v,
v => panic!("{:?}", v), v => panic!("{:?}", v),
}; };
let ordering = match left {
Value::Dimension(num, unit, _) => match &right { let ordering = left.cmp(&right, self.span, op)?;
Value::Dimension(num2, unit2, _) => {
if !unit.comparable(unit2) {
return Err((
format!("Incompatible units {} and {}.", unit2, unit),
self.span,
)
.into());
}
if &unit == unit2 || unit == Unit::None || unit2 == &Unit::None {
num.cmp(num2)
} else {
num.cmp(
&(num2.clone()
* UNIT_CONVERSION_TABLE[unit.to_string().as_str()]
[unit2.to_string().as_str()]
.clone()),
)
}
}
v => {
return Err((
format!(
"Undefined operation \"{} {} {}\".",
v.inspect(self.span)?,
op,
right.inspect(self.span)?
),
self.span,
)
.into())
}
},
_ => {
return Err((
format!(
"Undefined operation \"{} {} {}\".",
left.inspect(self.span)?,
op,
right.inspect(self.span)?
),
self.span,
)
.into())
}
};
Ok(match op { Ok(match op {
Op::GreaterThan => match ordering { Op::GreaterThan => match ordering {
Ordering::Greater => Value::True, Ordering::Greater => Value::True,

View File

@ -1,10 +1,12 @@
use std::cmp::Ordering;
use peekmore::PeekMore; use peekmore::PeekMore;
use codemap::{Span, Spanned}; use codemap::{Span, Spanned};
use crate::{ use crate::{
color::Color, color::Color,
common::{Brackets, ListSeparator, QuoteKind}, common::{Brackets, ListSeparator, Op, QuoteKind},
error::SassResult, error::SassResult,
parse::Parser, parse::Parser,
selector::Selector, selector::Selector,
@ -322,6 +324,63 @@ impl Value {
} }
} }
pub fn cmp(&self, other: &Self, span: Span, op: Op) -> SassResult<Ordering> {
Ok(match self {
Value::Dimension(num, unit, _) => match &other {
Value::Dimension(num2, unit2, _) => {
if !unit.comparable(unit2) {
return Err(
(format!("Incompatible units {} and {}.", unit2, unit), span).into(),
);
}
if unit == unit2 || unit == &Unit::None || unit2 == &Unit::None {
num.cmp(num2)
} else {
num.cmp(
&(num2.clone()
* UNIT_CONVERSION_TABLE[unit.to_string().as_str()]
[unit2.to_string().as_str()]
.clone()),
)
}
}
v => {
return Err((
format!(
"Undefined operation \"{} {} {}\".",
v.inspect(span)?,
op,
other.inspect(span)?
),
span,
)
.into())
}
},
_ => {
return Err((
format!(
"Undefined operation \"{} {} {}\".",
self.inspect(span)?,
op,
other.inspect(span)?
),
span,
)
.into())
}
})
}
pub fn unitless(&self) -> bool {
#[allow(clippy::match_same_arms)]
match self {
Value::Dimension(_, Unit::None, _) => true,
Value::Dimension(..) => false,
_ => true,
}
}
pub fn not_equals(&self, other: &Self) -> bool { pub fn not_equals(&self, other: &Self) -> bool {
match self { match self {
Value::String(s1, ..) => match other { Value::String(s1, ..) => match other {

View File

@ -16,7 +16,7 @@ mod integer;
const PRECISION: usize = 10; const PRECISION: usize = 10;
#[derive(Clone, Eq, PartialEq, Ord)] #[derive(Clone, Eq, PartialEq)]
pub(crate) enum Number { pub(crate) enum Number {
Small(Rational64), Small(Rational64),
Big(Box<BigRational>), Big(Box<BigRational>),
@ -321,6 +321,30 @@ impl PartialOrd for Number {
} }
} }
impl Ord for Number {
fn cmp(&self, other: &Self) -> Ordering {
match self {
Self::Small(val1) => match other {
Self::Small(val2) => val1.cmp(val2),
Self::Big(val2) => {
let tuple: (i64, i64) = (*val1).into();
BigRational::new_raw(BigInt::from(tuple.0), BigInt::from(tuple.1)).cmp(val2)
}
},
Self::Big(val1) => match other {
Self::Small(val2) => {
let tuple: (i64, i64) = (*val2).into();
(**val1).cmp(&BigRational::new_raw(
BigInt::from(tuple.0),
BigInt::from(tuple.1),
))
}
Self::Big(val2) => val1.cmp(val2),
},
}
}
}
impl Add for Number { impl Add for Number {
type Output = Self; type Output = Self;

40
tests/math-module.rs Normal file
View File

@ -0,0 +1,40 @@
#![cfg(test)]
#[macro_use]
mod macros;
test!(
clamp_in_the_middle,
"@use 'sass:math';\na {\n color: math.clamp(0, 1, 2);\n}\n",
"a {\n color: 1;\n}\n"
);
test!(
clamp_first_is_bigger,
"@use 'sass:math';\na {\n color: math.clamp(2, 1, 0);\n}\n",
"a {\n color: 2;\n}\n"
);
test!(
clamp_all_same_unit,
"@use 'sass:math';\na {\n color: math.clamp(0px, 1px, 2px);\n}\n",
"a {\n color: 1px;\n}\n"
);
test!(
clamp_all_different_but_compatible_unit,
"@use 'sass:math';\na {\n color: math.clamp(0mm, 1cm, 2in);\n}\n",
"a {\n color: 1cm;\n}\n"
);
error!(
clamp_only_min_has_no_unit,
"@use 'sass:math';\na {\n color: math.clamp(0, 1cm, 2in);\n}\n",
"Error: $min is unitless but $number has unit cm. Arguments must all have units or all be unitless."
);
error!(
clamp_only_number_has_no_unit,
"@use 'sass:math';\na {\n color: math.clamp(0mm, 1, 2in);\n}\n",
"Error: $min has unit mm but $number is unitless. Arguments must all have units or all be unitless."
);
error!(
clamp_only_max_has_no_unit,
"@use 'sass:math';\na {\n color: math.clamp(0mm, 1cm, 2);\n}\n",
"Error: $min has unit mm but $max is unitless. Arguments must all have units or all be unitless."
);

View File

@ -63,3 +63,8 @@ test!(
"a {\n color: 0 < 1;\n}\n", "a {\n color: 0 < 1;\n}\n",
"a {\n color: true;\n}\n" "a {\n color: true;\n}\n"
); );
test!(
ord_the_same_as_partial_ord,
"a {\n color: 2in > 1cm;\n}\n",
"a {\n color: true;\n}\n"
);