//! Mathematical calculation functions and expressions. use crate::compat::Feature; use crate::error::{ParserError, PrinterError}; use crate::macros::enum_property; use crate::printer::Printer; use crate::targets::{should_compile, Browsers}; use crate::traits::private::AddInternal; use crate::traits::{IsCompatible, Parse, Sign, ToCss, TryMap, TryOp, TrySign}; #[cfg(feature = "visitor")] use crate::visitor::Visit; use cssparser::*; use super::angle::Angle; use super::length::Length; use super::number::CSSNumber; use super::percentage::Percentage; use super::time::Time; /// A CSS [math function](https://www.w3.org/TR/css-values-4/#math-function). /// /// Math functions may be used in most properties and values that accept numeric /// values, including lengths, percentages, angles, times, etc. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(tag = "type", content = "value", rename_all = "kebab-case") )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub enum MathFunction { /// The [`calc()`](https://www.w3.org/TR/css-values-4/#calc-func) function. Calc(Calc), /// The [`min()`](https://www.w3.org/TR/css-values-4/#funcdef-min) function. Min(Vec>), /// The [`max()`](https://www.w3.org/TR/css-values-4/#funcdef-max) function. Max(Vec>), /// The [`clamp()`](https://www.w3.org/TR/css-values-4/#funcdef-clamp) function. Clamp(Calc, Calc, Calc), /// The [`round()`](https://www.w3.org/TR/css-values-4/#funcdef-round) function. Round(RoundingStrategy, Calc, Calc), /// The [`rem()`](https://www.w3.org/TR/css-values-4/#funcdef-rem) function. Rem(Calc, Calc), /// The [`mod()`](https://www.w3.org/TR/css-values-4/#funcdef-mod) function. Mod(Calc, Calc), /// The [`abs()`](https://drafts.csswg.org/css-values-4/#funcdef-abs) function. Abs(Calc), /// The [`sign()`](https://drafts.csswg.org/css-values-4/#funcdef-sign) function. Sign(Calc), /// The [`hypot()`](https://drafts.csswg.org/css-values-4/#funcdef-hypot) function. Hypot(Vec>), } impl IsCompatible for MathFunction { fn is_compatible(&self, browsers: Browsers) -> bool { match self { MathFunction::Calc(v) => Feature::CalcFunction.is_compatible(browsers) && v.is_compatible(browsers), MathFunction::Min(v) => { Feature::MinFunction.is_compatible(browsers) && v.iter().all(|v| v.is_compatible(browsers)) } MathFunction::Max(v) => { Feature::MaxFunction.is_compatible(browsers) && v.iter().all(|v| v.is_compatible(browsers)) } MathFunction::Clamp(a, b, c) => { Feature::ClampFunction.is_compatible(browsers) && a.is_compatible(browsers) && b.is_compatible(browsers) && c.is_compatible(browsers) } MathFunction::Round(_, a, b) => { Feature::RoundFunction.is_compatible(browsers) && a.is_compatible(browsers) && b.is_compatible(browsers) } MathFunction::Rem(a, b) => { Feature::RemFunction.is_compatible(browsers) && a.is_compatible(browsers) && b.is_compatible(browsers) } MathFunction::Mod(a, b) => { Feature::ModFunction.is_compatible(browsers) && a.is_compatible(browsers) && b.is_compatible(browsers) } MathFunction::Abs(v) => Feature::AbsFunction.is_compatible(browsers) && v.is_compatible(browsers), MathFunction::Sign(v) => Feature::SignFunction.is_compatible(browsers) && v.is_compatible(browsers), MathFunction::Hypot(v) => { Feature::HypotFunction.is_compatible(browsers) && v.iter().all(|v| v.is_compatible(browsers)) } } } } enum_property! { /// A [rounding strategy](https://www.w3.org/TR/css-values-4/#typedef-rounding-strategy), /// as used in the `round()` function. pub enum RoundingStrategy { /// Round to the nearest integer. Nearest, /// Round up (ceil). Up, /// Round down (floor). Down, /// Round toward zero (truncate). ToZero, } } impl Default for RoundingStrategy { fn default() -> Self { RoundingStrategy::Nearest } } fn round(value: f32, to: f32, strategy: RoundingStrategy) -> f32 { let v = value / to; match strategy { RoundingStrategy::Down => v.floor() * to, RoundingStrategy::Up => v.ceil() * to, RoundingStrategy::Nearest => v.round() * to, RoundingStrategy::ToZero => v.trunc() * to, } } fn modulo(a: f32, b: f32) -> f32 { ((a % b) + b) % b } impl + TrySign + Clone + std::fmt::Debug> ToCss for MathFunction { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { match self { MathFunction::Calc(calc) => { dest.write_str("calc(")?; calc.to_css(dest)?; dest.write_char(')') } MathFunction::Min(args) => { dest.write_str("min(")?; let mut first = true; for arg in args { if first { first = false; } else { dest.delim(',', false)?; } arg.to_css(dest)?; } dest.write_char(')') } MathFunction::Max(args) => { dest.write_str("max(")?; let mut first = true; for arg in args { if first { first = false; } else { dest.delim(',', false)?; } arg.to_css(dest)?; } dest.write_char(')') } MathFunction::Clamp(a, b, c) => { // If clamp() is unsupported by targets, output min()/max() if should_compile!(dest.targets.current, ClampFunction) { dest.write_str("max(")?; a.to_css(dest)?; dest.delim(',', false)?; dest.write_str("min(")?; b.to_css(dest)?; dest.delim(',', false)?; c.to_css(dest)?; dest.write_str("))")?; return Ok(()); } dest.write_str("clamp(")?; a.to_css(dest)?; dest.delim(',', false)?; b.to_css(dest)?; dest.delim(',', false)?; c.to_css(dest)?; dest.write_char(')') } MathFunction::Round(strategy, a, b) => { dest.write_str("round(")?; if *strategy != RoundingStrategy::default() { strategy.to_css(dest)?; dest.delim(',', false)?; } a.to_css(dest)?; dest.delim(',', false)?; b.to_css(dest)?; dest.write_char(')') } MathFunction::Rem(a, b) => { dest.write_str("rem(")?; a.to_css(dest)?; dest.delim(',', false)?; b.to_css(dest)?; dest.write_char(')') } MathFunction::Mod(a, b) => { dest.write_str("mod(")?; a.to_css(dest)?; dest.delim(',', false)?; b.to_css(dest)?; dest.write_char(')') } MathFunction::Abs(v) => { dest.write_str("abs(")?; v.to_css(dest)?; dest.write_char(')') } MathFunction::Sign(v) => { dest.write_str("sign(")?; v.to_css(dest)?; dest.write_char(')') } MathFunction::Hypot(args) => { dest.write_str("hypot(")?; let mut first = true; for arg in args { if first { first = false; } else { dest.delim(',', false)?; } arg.to_css(dest)?; } dest.write_char(')') } } } } /// A mathematical expression used within the [`calc()`](https://www.w3.org/TR/css-values-4/#calc-func) function. /// /// This type supports generic value types. Values such as [Length](super::length::Length), [Percentage](super::percentage::Percentage), /// [Time](super::time::Time), and [Angle](super::angle::Angle) support `calc()` expressions. #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(tag = "type", content = "value", rename_all = "kebab-case") )] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub enum Calc { /// A literal value. Value(Box), /// A literal number. Number(CSSNumber), /// A sum of two calc expressions. #[cfg_attr(feature = "visitor", skip_type)] Sum(Box>, Box>), /// A product of a number and another calc expression. #[cfg_attr(feature = "visitor", skip_type)] Product(CSSNumber, Box>), /// A math function, such as `calc()`, `min()`, or `max()`. #[cfg_attr(feature = "visitor", skip_type)] Function(Box>), } impl IsCompatible for Calc { fn is_compatible(&self, browsers: Browsers) -> bool { match self { Calc::Sum(a, b) => a.is_compatible(browsers) && b.is_compatible(browsers), Calc::Product(_, v) => v.is_compatible(browsers), Calc::Function(f) => f.is_compatible(browsers), Calc::Value(v) => v.is_compatible(browsers), Calc::Number(..) => true, } } } enum_property! { /// A mathematical constant. pub enum Constant { /// The base of the natural logarithm "e": E, /// The ratio of a circle’s circumference to its diameter "pi": Pi, /// infinity "infinity": Infinity, /// -infinity "-infinity": NegativeInfinity, /// Not a number. "nan": Nan, } } impl Into for Constant { fn into(self) -> f32 { use std::f32::consts; use Constant::*; match self { E => consts::E, Pi => consts::PI, Infinity => f32::INFINITY, NegativeInfinity => -f32::INFINITY, Nan => f32::NAN, } } } impl< 'i, V: Parse<'i> + std::ops::Mul + AddInternal + TryOp + TryMap + TrySign + std::cmp::PartialOrd + Into> + TryFrom> + TryFrom + TryInto + Clone + std::fmt::Debug, > Parse<'i> for Calc { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { Self::parse_with(input, |_| None) } } impl< 'i, V: Parse<'i> + std::ops::Mul + AddInternal + TryOp + TryMap + TrySign + std::cmp::PartialOrd + Into> + TryFrom> + TryFrom + TryInto + Clone + std::fmt::Debug, > Calc { pub(crate) fn parse_with<'t, Parse: Copy + Fn(&str) -> Option>>( input: &mut Parser<'i, 't>, parse_ident: Parse, ) -> Result>> { let location = input.current_source_location(); let f = input.expect_function()?; match_ignore_ascii_case! { &f, "calc" => { let calc = input.parse_nested_block(|input| Calc::parse_sum(input, parse_ident))?; match calc { Calc::Value(_) | Calc::Number(_) => Ok(calc), _ => Ok(Calc::Function(Box::new(MathFunction::Calc(calc)))) } }, "min" => { let mut args = input.parse_nested_block(|input| input.parse_comma_separated(|input| Calc::parse_sum(input, parse_ident)))?; let mut reduced = Calc::reduce_args(&mut args, std::cmp::Ordering::Less); if reduced.len() == 1 { return Ok(reduced.remove(0)) } Ok(Calc::Function(Box::new(MathFunction::Min(reduced)))) }, "max" => { let mut args = input.parse_nested_block(|input| input.parse_comma_separated(|input| Calc::parse_sum(input, parse_ident)))?; let mut reduced = Calc::reduce_args(&mut args, std::cmp::Ordering::Greater); if reduced.len() == 1 { return Ok(reduced.remove(0)) } Ok(Calc::Function(Box::new(MathFunction::Max(reduced)))) }, "clamp" => { let (mut min, mut center, mut max) = input.parse_nested_block(|input| { let min = Some(Calc::parse_sum(input, parse_ident)?); input.expect_comma()?; let center: Calc = Calc::parse_sum(input, parse_ident)?; input.expect_comma()?; let max = Some(Calc::parse_sum(input, parse_ident)?); Ok((min, center, max)) })?; // According to the spec, the minimum should "win" over the maximum if they are in the wrong order. let cmp = if let (Some(Calc::Value(max_val)), Calc::Value(center_val)) = (&max, ¢er) { center_val.partial_cmp(&max_val) } else { None }; // If center is known to be greater than the maximum, replace it with maximum and remove the max argument. // Otherwise, if center is known to be less than the maximum, remove the max argument. match cmp { Some(std::cmp::Ordering::Greater) => { center = std::mem::take(&mut max).unwrap(); } Some(_) => { max = None; } None => {} } if cmp.is_some() { let cmp = if let (Some(Calc::Value(min_val)), Calc::Value(center_val)) = (&min, ¢er) { center_val.partial_cmp(&min_val) } else { None }; // If center is known to be less than the minimum, replace it with minimum and remove the min argument. // Otherwise, if center is known to be greater than the minimum, remove the min argument. match cmp { Some(std::cmp::Ordering::Less) => { center = std::mem::take(&mut min).unwrap(); } Some(_) => { min = None; } None => {} } } // Generate clamp(), min(), max(), or value depending on which arguments are left. match (min, max) { (None, None) => Ok(center), (Some(min), None) => Ok(Calc::Function(Box::new(MathFunction::Max(vec![min, center])))), (None, Some(max)) => Ok(Calc::Function(Box::new(MathFunction::Min(vec![center, max])))), (Some(min), Some(max)) => Ok(Calc::Function(Box::new(MathFunction::Clamp(min, center, max)))) } }, "round" => { input.parse_nested_block(|input| { let strategy = if let Ok(s) = input.try_parse(RoundingStrategy::parse) { input.expect_comma()?; s } else { RoundingStrategy::default() }; Self::parse_math_fn( input, |a, b| round(a, b, strategy), |a, b| MathFunction::Round(strategy, a, b), parse_ident ) }) }, "rem" => { input.parse_nested_block(|input| { Self::parse_math_fn(input, std::ops::Rem::rem, MathFunction::Rem, parse_ident) }) }, "mod" => { input.parse_nested_block(|input| { Self::parse_math_fn(input, modulo, MathFunction::Mod, parse_ident) }) }, "sin" => Self::parse_trig(input, f32::sin, false, parse_ident), "cos" => Self::parse_trig(input, f32::cos, false, parse_ident), "tan" => Self::parse_trig(input, f32::tan, false, parse_ident), "asin" => Self::parse_trig(input, f32::asin, true, parse_ident), "acos" => Self::parse_trig(input, f32::acos, true, parse_ident), "atan" => Self::parse_trig(input, f32::atan, true, parse_ident), "atan2" => { input.parse_nested_block(|input| { let res = Self::parse_atan2(input, parse_ident)?; if let Ok(v) = V::try_from(res) { return Ok(Calc::Value(Box::new(v))) } Err(input.new_custom_error(ParserError::InvalidValue)) }) }, "pow" => { input.parse_nested_block(|input| { let a = Self::parse_numeric(input, parse_ident)?; input.expect_comma()?; let b = Self::parse_numeric(input, parse_ident)?; Ok(Calc::Number(a.powf(b))) }) }, "log" => { input.parse_nested_block(|input| { let value = Self::parse_numeric(input, parse_ident)?; if input.try_parse(|input| input.expect_comma()).is_ok() { let base = Self::parse_numeric(input, parse_ident)?; Ok(Calc::Number(value.log(base))) } else { Ok(Calc::Number(value.ln())) } }) }, "sqrt" => Self::parse_numeric_fn(input, f32::sqrt, parse_ident), "exp" => Self::parse_numeric_fn(input, f32::exp, parse_ident), "hypot" => { input.parse_nested_block(|input| { let args: Vec = input.parse_comma_separated(|input| Calc::parse_sum(input, parse_ident))?; Self::parse_hypot(&args)? .map_or_else( || Ok(Calc::Function(Box::new(MathFunction::Hypot(args)))), |v| Ok(v) ) }) }, "abs" => { input.parse_nested_block(|input| { let v: Calc = Self::parse_sum(input, parse_ident)?; Self::apply_map(&v, f32::abs) .map_or_else( || Ok(Calc::Function(Box::new(MathFunction::Abs(v)))), |v| Ok(v) ) }) }, "sign" => { input.parse_nested_block(|input| { let v: Calc = Self::parse_sum(input, parse_ident)?; match &v { Calc::Number(n) => return Ok(Calc::Number(n.sign())), Calc::Value(v) => { // First map so we ignore percentages, which must be resolved to their // computed value in order to determine the sign. if let Some(v) = v.try_map(|s| s.sign()) { // sign() always resolves to a number. return Ok(Calc::Number(v.try_sign().unwrap())); } } _ => {} } Ok(Calc::Function(Box::new(MathFunction::Sign(v)))) }) }, _ => Err(location.new_unexpected_token_error(Token::Ident(f.clone()))), } } fn parse_sum<'t, Parse: Copy + Fn(&str) -> Option>>( input: &mut Parser<'i, 't>, parse_ident: Parse, ) -> Result>> { let mut cur: Calc = Calc::parse_product(input, parse_ident)?; loop { let start = input.state(); match input.next_including_whitespace() { Ok(&Token::WhiteSpace(_)) => { if input.is_exhausted() { break; // allow trailing whitespace } match *input.next()? { Token::Delim('+') => { let next = Calc::parse_product(input, parse_ident)?; cur = cur.add(next).map_err(|_| input.new_custom_error(ParserError::InvalidValue))?; } Token::Delim('-') => { let mut rhs = Calc::parse_product(input, parse_ident)?; rhs = rhs * -1.0; cur = cur.add(rhs).map_err(|_| input.new_custom_error(ParserError::InvalidValue))?; } ref t => { let t = t.clone(); return Err(input.new_unexpected_token_error(t)); } } } _ => { input.reset(&start); break; } } } Ok(cur) } fn parse_product<'t, Parse: Copy + Fn(&str) -> Option>>( input: &mut Parser<'i, 't>, parse_ident: Parse, ) -> Result>> { let mut node = Calc::parse_value(input, parse_ident)?; loop { let start = input.state(); match input.next() { Ok(&Token::Delim('*')) => { // At least one of the operands must be a number. let rhs = Self::parse_value(input, parse_ident)?; if let Calc::Number(val) = rhs { node = node * val; } else if let Calc::Number(val) = node { node = rhs; node = node * val; } else { return Err(input.new_unexpected_token_error(Token::Delim('*'))); } } Ok(&Token::Delim('/')) => { let rhs = Self::parse_value(input, parse_ident)?; if let Calc::Number(val) = rhs { if val != 0.0 { node = node * (1.0 / val); continue; } } return Err(input.new_custom_error(ParserError::InvalidValue)); } _ => { input.reset(&start); break; } } } Ok(node) } fn parse_value<'t, Parse: Copy + Fn(&str) -> Option>>( input: &mut Parser<'i, 't>, parse_ident: Parse, ) -> Result>> { // Parse nested calc() and other math functions. if let Ok(calc) = input.try_parse(Self::parse) { match calc { Calc::Function(f) => { return Ok(match *f { MathFunction::Calc(c) => c, _ => Calc::Function(f), }) } c => return Ok(c), } } if input.try_parse(|input| input.expect_parenthesis_block()).is_ok() { return input.parse_nested_block(|input| Calc::parse_sum(input, parse_ident)); } if let Ok(num) = input.try_parse(|input| input.expect_number()) { return Ok(Calc::Number(num)); } if let Ok(constant) = input.try_parse(Constant::parse) { return Ok(Calc::Number(constant.into())); } let location = input.current_source_location(); if let Ok(ident) = input.try_parse(|input| input.expect_ident_cloned()) { if let Some(v) = parse_ident(ident.as_ref()) { return Ok(v); } return Err(location.new_unexpected_token_error(Token::Ident(ident.clone()))); } let value = input.try_parse(V::parse)?; Ok(Calc::Value(Box::new(value))) } fn reduce_args(args: &mut Vec>, cmp: std::cmp::Ordering) -> Vec> { // Reduces the arguments of a min() or max() expression, combining compatible values. // e.g. min(1px, 1em, 2px, 3in) => min(1px, 1em) let mut reduced: Vec> = vec![]; for arg in args.drain(..) { let mut found = None; match &arg { Calc::Value(val) => { for b in reduced.iter_mut() { if let Calc::Value(v) = b { match val.partial_cmp(v) { Some(ord) if ord == cmp => { found = Some(Some(b)); break; } Some(_) => { found = Some(None); break; } None => {} } } } } _ => {} } if let Some(r) = found { if let Some(r) = r { *r = arg } } else { reduced.push(arg) } } reduced } fn parse_math_fn< 't, O: FnOnce(f32, f32) -> f32, F: FnOnce(Calc, Calc) -> MathFunction, Parse: Copy + Fn(&str) -> Option>, >( input: &mut Parser<'i, 't>, op: O, fallback: F, parse_ident: Parse, ) -> Result>> { let a: Calc = Calc::parse_sum(input, parse_ident)?; input.expect_comma()?; let b: Calc = Calc::parse_sum(input, parse_ident)?; Ok(Self::apply_op(&a, &b, op).unwrap_or_else(|| Calc::Function(Box::new(fallback(a, b))))) } fn apply_op<'t, O: FnOnce(f32, f32) -> f32>(a: &Calc, b: &Calc, op: O) -> Option { match (a, b) { (Calc::Value(a), Calc::Value(b)) => { if let Some(v) = a.try_op(&**b, op) { return Some(Calc::Value(Box::new(v))); } } (Calc::Number(a), Calc::Number(b)) => return Some(Calc::Number(op(*a, *b))), _ => {} } None } fn apply_map<'t, O: FnOnce(f32) -> f32>(v: &Calc, op: O) -> Option { match v { Calc::Number(n) => return Some(Calc::Number(op(*n))), Calc::Value(v) => { if let Some(v) = v.try_map(op) { return Some(Calc::Value(Box::new(v))); } } _ => {} } None } fn parse_trig<'t, F: FnOnce(f32) -> f32, Parse: Copy + Fn(&str) -> Option>>( input: &mut Parser<'i, 't>, f: F, to_angle: bool, parse_ident: Parse, ) -> Result>> { input.parse_nested_block(|input| { let v: Calc = Calc::parse_sum(input, |v| { parse_ident(v).and_then(|v| -> Option> { match v { Calc::Number(v) => Some(Calc::Number(v)), Calc::Value(v) => (*v).try_into().ok().map(|v| Calc::Value(Box::new(v))), _ => None, } }) })?; let rad = match v { Calc::Value(angle) if !to_angle => f(angle.to_radians()), Calc::Number(v) => f(v), _ => return Err(input.new_custom_error(ParserError::InvalidValue)), }; if to_angle && !rad.is_nan() { if let Ok(v) = V::try_from(Angle::Rad(rad)) { return Ok(Calc::Value(Box::new(v))); } else { return Err(input.new_custom_error(ParserError::InvalidValue)); } } else { Ok(Calc::Number(rad)) } }) } fn parse_numeric<'t, Parse: Copy + Fn(&str) -> Option>>( input: &mut Parser<'i, 't>, parse_ident: Parse, ) -> Result>> { let v: Calc = Calc::parse_sum(input, |v| { parse_ident(v).and_then(|v| match v { Calc::Number(v) => Some(Calc::Number(v)), _ => None, }) })?; match v { Calc::Number(n) => Ok(n), Calc::Value(v) => Ok(*v), _ => Err(input.new_custom_error(ParserError::InvalidValue)), } } fn parse_numeric_fn<'t, F: FnOnce(f32) -> f32, Parse: Copy + Fn(&str) -> Option>>( input: &mut Parser<'i, 't>, f: F, parse_ident: Parse, ) -> Result>> { input.parse_nested_block(|input| { let v = Self::parse_numeric(input, parse_ident)?; Ok(Calc::Number(f(v))) }) } fn parse_atan2<'t, Parse: Copy + Fn(&str) -> Option>>( input: &mut Parser<'i, 't>, parse_ident: Parse, ) -> Result>> { // atan2 supports arguments of any , , or , even ones that wouldn't // normally be supported by V. The only requirement is that the arguments be of the same type. // Try parsing with each type, and return the first one that parses successfully. if let Ok(v) = input.try_parse(|input| Calc::::parse_atan2_args(input, |_| None)) { return Ok(v); } if let Ok(v) = input.try_parse(|input| Calc::::parse_atan2_args(input, |_| None)) { return Ok(v); } if let Ok(v) = input.try_parse(|input| Calc::::parse_atan2_args(input, |_| None)) { return Ok(v); } if let Ok(v) = input.try_parse(|input| Calc::