//! Mathematical calculation functions and expressions. use crate::compat::Feature; use crate::error::{ParserError, PrinterError}; use crate::printer::Printer; use crate::traits::private::AddInternal; use crate::traits::{Parse, ToCss}; use cssparser::*; use super::number::CSSNumber; /// 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 = "serde", derive(serde::Serialize, serde::Deserialize), serde(tag = "type", content = "value", rename_all = "kebab-case") )] 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), } impl + std::ops::Mul + 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 let Some(targets) = dest.targets { if !Feature::Clamp.is_compatible(targets) { 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(')') } } } } /// 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 = "serde", derive(serde::Serialize, serde::Deserialize), serde(tag = "type", content = "value", rename_all = "kebab-case") )] pub enum Calc { /// A literal value. Value(Box), /// A literal number. Number(CSSNumber), /// A sum of two calc expressions. Sum(Box>, Box>), /// A product of a number and another calc expression. Product(CSSNumber, Box>), /// A math function, such as `calc()`, `min()`, or `max()`. Function(Box>), } impl< 'i, V: Parse<'i> + std::ops::Mul + AddInternal + std::cmp::PartialOrd + std::convert::Into> + std::convert::From> + std::fmt::Debug, > Parse<'i> for Calc { fn parse<'t>(input: &mut Parser<'i, 't>) -> Result>> { let location = input.current_source_location(); let f = input.expect_function()?; match_ignore_ascii_case! { &f, "calc" => { let calc = input.parse_nested_block(Calc::parse_sum)?; 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(Calc::parse_sum))?; 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(Calc::parse_sum))?; 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)?); input.expect_comma()?; let center: Calc = Calc::parse_sum(input)?; input.expect_comma()?; let max = Some(Calc::parse_sum(input)?); 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 => {} } 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)))) } }, _ => Err(location.new_unexpected_token_error(Token::Ident(f.clone()))), } } } impl< 'i, V: Parse<'i> + std::ops::Mul + AddInternal + std::cmp::PartialOrd + std::convert::Into> + std::convert::From> + std::fmt::Debug, > Calc { fn parse_sum<'t>(input: &mut Parser<'i, 't>) -> Result>> { let mut cur: Calc = Calc::parse_product(input)?; 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)?; cur = cur.add(next); } Token::Delim('-') => { let mut rhs = Calc::parse_product(input)?; rhs = rhs * -1.0; cur = cur.add(rhs); } ref t => { let t = t.clone(); return Err(input.new_unexpected_token_error(t)); } } } _ => { input.reset(&start); break; } } } Ok(cur) } fn parse_product<'t>(input: &mut Parser<'i, 't>) -> Result>> { let mut node = Calc::parse_value(input)?; 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)?; 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)?; 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>(input: &mut Parser<'i, 't>) -> 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(Calc::parse_sum); } if let Ok(num) = input.try_parse(|input| input.expect_number()) { return Ok(Calc::Number(num)); } if let Ok(value) = input.try_parse(V::parse) { return Ok(Calc::Value(Box::new(value))); } Err(input.new_error_for_next_token()) } 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 } } impl> std::ops::Mul for Calc { type Output = Self; fn mul(self, other: f32) -> Self { if other == 1.0 { return self; } match self { Calc::Value(v) => Calc::Value(Box::new(*v * other)), Calc::Number(n) => Calc::Number(n * other), Calc::Sum(a, b) => Calc::Sum(Box::new(*a * other), Box::new(*b * other)), Calc::Product(num, calc) => { let num = num * other; if num == 1.0 { return *calc; } Calc::Product(num, calc) } Calc::Function(f) => match *f { MathFunction::Calc(c) => Calc::Function(Box::new(MathFunction::Calc(c * other))), _ => Calc::Product(other, Box::new(Calc::Function(f))), }, } } } impl> + std::convert::From> + std::fmt::Debug> AddInternal for Calc { fn add(self, other: Calc) -> Calc { match (self, other) { (Calc::Value(a), Calc::Value(b)) => (a.add(*b)).into(), (Calc::Number(a), Calc::Number(b)) => Calc::Number(a + b), (Calc::Value(a), b) => (a.add(V::from(b))).into(), (a, Calc::Value(b)) => (V::from(a).add(*b)).into(), (Calc::Function(a), b) => Calc::Sum(Box::new(Calc::Function(a)), Box::new(b)), (a, Calc::Function(b)) => Calc::Sum(Box::new(a), Box::new(Calc::Function(b))), (a, b) => V::from(a).add(V::from(b)).into(), } } } impl> std::cmp::PartialEq for Calc { fn eq(&self, other: &f32) -> bool { match self { Calc::Value(a) => **a == *other, Calc::Number(a) => *a == *other, _ => false, } } } impl> std::cmp::PartialOrd for Calc { fn partial_cmp(&self, other: &f32) -> Option { match self { Calc::Value(a) => a.partial_cmp(other), Calc::Number(a) => a.partial_cmp(other), _ => None, } } } impl + std::ops::Mul + Clone + std::fmt::Debug> ToCss for Calc { fn to_css(&self, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { let was_in_calc = dest.in_calc; dest.in_calc = true; let res = match self { Calc::Value(v) => v.to_css(dest), Calc::Number(n) => n.to_css(dest), Calc::Sum(a, b) => { a.to_css(dest)?; // Whitespace is always required. let b = &**b; if *b < 0.0 { dest.write_str(" - ")?; let b = b.clone() * -1.0; b.to_css(dest) } else { dest.write_str(" + ")?; b.to_css(dest) } } Calc::Product(num, calc) => { if num.abs() < 1.0 { let div = 1.0 / num; calc.to_css(dest)?; dest.delim('/', true)?; div.to_css(dest) } else { num.to_css(dest)?; dest.delim('*', true)?; calc.to_css(dest) } } Calc::Function(f) => f.to_css(dest), }; dest.in_calc = was_in_calc; res } }