summaryrefslogtreecommitdiffstats
path: root/servo/components/style/values/specified/calc.rs
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /servo/components/style/values/specified/calc.rs
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'servo/components/style/values/specified/calc.rs')
-rw-r--r--servo/components/style/values/specified/calc.rs1086
1 files changed, 1086 insertions, 0 deletions
diff --git a/servo/components/style/values/specified/calc.rs b/servo/components/style/values/specified/calc.rs
new file mode 100644
index 0000000000..2660864319
--- /dev/null
+++ b/servo/components/style/values/specified/calc.rs
@@ -0,0 +1,1086 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! [Calc expressions][calc].
+//!
+//! [calc]: https://drafts.csswg.org/css-values/#calc-notation
+
+use crate::color::parsing::{AngleOrNumber, NumberOrPercentage};
+use crate::parser::ParserContext;
+use crate::values::generics::calc::{
+ self as generic, CalcNodeLeaf, CalcUnits, MinMaxOp, ModRemOp, PositivePercentageBasis,
+ RoundingStrategy, SortKey,
+};
+use crate::values::specified::length::{AbsoluteLength, FontRelativeLength, NoCalcLength};
+use crate::values::specified::length::{ContainerRelativeLength, ViewportPercentageLength};
+use crate::values::specified::{self, Angle, Resolution, Time};
+use crate::values::{serialize_number, serialize_percentage, CSSFloat, CSSInteger};
+use cssparser::{CowRcStr, Parser, Token};
+use smallvec::SmallVec;
+use std::cmp;
+use std::fmt::{self, Write};
+use style_traits::values::specified::AllowedNumericType;
+use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss};
+
+/// The name of the mathematical function that we're parsing.
+#[derive(Clone, Copy, Debug, Parse)]
+pub enum MathFunction {
+ /// `calc()`: https://drafts.csswg.org/css-values-4/#funcdef-calc
+ Calc,
+ /// `min()`: https://drafts.csswg.org/css-values-4/#funcdef-min
+ Min,
+ /// `max()`: https://drafts.csswg.org/css-values-4/#funcdef-max
+ Max,
+ /// `clamp()`: https://drafts.csswg.org/css-values-4/#funcdef-clamp
+ Clamp,
+ /// `round()`: https://drafts.csswg.org/css-values-4/#funcdef-round
+ Round,
+ /// `mod()`: https://drafts.csswg.org/css-values-4/#funcdef-mod
+ Mod,
+ /// `rem()`: https://drafts.csswg.org/css-values-4/#funcdef-rem
+ Rem,
+ /// `sin()`: https://drafts.csswg.org/css-values-4/#funcdef-sin
+ Sin,
+ /// `cos()`: https://drafts.csswg.org/css-values-4/#funcdef-cos
+ Cos,
+ /// `tan()`: https://drafts.csswg.org/css-values-4/#funcdef-tan
+ Tan,
+ /// `asin()`: https://drafts.csswg.org/css-values-4/#funcdef-asin
+ Asin,
+ /// `acos()`: https://drafts.csswg.org/css-values-4/#funcdef-acos
+ Acos,
+ /// `atan()`: https://drafts.csswg.org/css-values-4/#funcdef-atan
+ Atan,
+ /// `atan2()`: https://drafts.csswg.org/css-values-4/#funcdef-atan2
+ Atan2,
+ /// `pow()`: https://drafts.csswg.org/css-values-4/#funcdef-pow
+ Pow,
+ /// `sqrt()`: https://drafts.csswg.org/css-values-4/#funcdef-sqrt
+ Sqrt,
+ /// `hypot()`: https://drafts.csswg.org/css-values-4/#funcdef-hypot
+ Hypot,
+ /// `log()`: https://drafts.csswg.org/css-values-4/#funcdef-log
+ Log,
+ /// `exp()`: https://drafts.csswg.org/css-values-4/#funcdef-exp
+ Exp,
+ /// `abs()`: https://drafts.csswg.org/css-values-4/#funcdef-abs
+ Abs,
+ /// `sign()`: https://drafts.csswg.org/css-values-4/#funcdef-sign
+ Sign,
+}
+
+/// A leaf node inside a `Calc` expression's AST.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub enum Leaf {
+ /// `<length>`
+ Length(NoCalcLength),
+ /// `<angle>`
+ Angle(Angle),
+ /// `<time>`
+ Time(Time),
+ /// `<resolution>`
+ Resolution(Resolution),
+ /// `<percentage>`
+ Percentage(CSSFloat),
+ /// `<number>`
+ Number(CSSFloat),
+}
+
+impl Leaf {
+ fn as_length(&self) -> Option<&NoCalcLength> {
+ match *self {
+ Self::Length(ref l) => Some(l),
+ _ => None,
+ }
+ }
+}
+
+impl ToCss for Leaf {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ Self::Length(ref l) => l.to_css(dest),
+ Self::Number(n) => serialize_number(n, /* was_calc = */ false, dest),
+ Self::Resolution(ref r) => r.to_css(dest),
+ Self::Percentage(p) => serialize_percentage(p, dest),
+ Self::Angle(ref a) => a.to_css(dest),
+ Self::Time(ref t) => t.to_css(dest),
+ }
+ }
+}
+
+/// A struct to hold a simplified `<length>` or `<percentage>` expression.
+///
+/// In some cases, e.g. DOMMatrix, we support calc(), but reject all the
+/// relative lengths, and to_computed_pixel_length_without_context() handles
+/// this case. Therefore, if you want to add a new field, please make sure this
+/// function work properly.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)]
+#[allow(missing_docs)]
+pub struct CalcLengthPercentage {
+ #[css(skip)]
+ pub clamping_mode: AllowedNumericType,
+ pub node: CalcNode,
+}
+
+impl CalcLengthPercentage {
+ fn same_unit_length_as(a: &Self, b: &Self) -> Option<(CSSFloat, CSSFloat)> {
+ debug_assert_eq!(a.clamping_mode, b.clamping_mode);
+ debug_assert_eq!(a.clamping_mode, AllowedNumericType::All);
+
+ let a = a.node.as_leaf()?;
+ let b = b.node.as_leaf()?;
+
+ if a.sort_key() != b.sort_key() {
+ return None;
+ }
+
+ let a = a.as_length()?.unitless_value();
+ let b = b.as_length()?.unitless_value();
+ return Some((a, b));
+ }
+}
+
+impl SpecifiedValueInfo for CalcLengthPercentage {}
+
+impl generic::CalcNodeLeaf for Leaf {
+ fn unit(&self) -> CalcUnits {
+ match self {
+ Leaf::Length(_) => CalcUnits::LENGTH,
+ Leaf::Angle(_) => CalcUnits::ANGLE,
+ Leaf::Time(_) => CalcUnits::TIME,
+ Leaf::Resolution(_) => CalcUnits::RESOLUTION,
+ Leaf::Percentage(_) => CalcUnits::PERCENTAGE,
+ Leaf::Number(_) => CalcUnits::empty(),
+ }
+ }
+
+ fn unitless_value(&self) -> f32 {
+ match *self {
+ Self::Length(ref l) => l.unitless_value(),
+ Self::Percentage(n) | Self::Number(n) => n,
+ Self::Resolution(ref r) => r.dppx(),
+ Self::Angle(ref a) => a.degrees(),
+ Self::Time(ref t) => t.seconds(),
+ }
+ }
+
+ fn new_number(value: f32) -> Self {
+ Self::Number(value)
+ }
+
+ fn compare(&self, other: &Self, basis: PositivePercentageBasis) -> Option<cmp::Ordering> {
+ use self::Leaf::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return None;
+ }
+
+ if matches!(self, Percentage(..)) && matches!(basis, PositivePercentageBasis::Unknown) {
+ return None;
+ }
+
+ let self_negative = self.is_negative();
+ if self_negative != other.is_negative() {
+ return Some(if self_negative { cmp::Ordering::Less } else { cmp::Ordering::Greater });
+ }
+
+ match (self, other) {
+ (&Percentage(ref one), &Percentage(ref other)) => one.partial_cmp(other),
+ (&Length(ref one), &Length(ref other)) => one.partial_cmp(other),
+ (&Angle(ref one), &Angle(ref other)) => one.degrees().partial_cmp(&other.degrees()),
+ (&Time(ref one), &Time(ref other)) => one.seconds().partial_cmp(&other.seconds()),
+ (&Resolution(ref one), &Resolution(ref other)) => one.dppx().partial_cmp(&other.dppx()),
+ (&Number(ref one), &Number(ref other)) => one.partial_cmp(other),
+ _ => {
+ match *self {
+ Length(..) | Percentage(..) | Angle(..) | Time(..) | Number(..) |
+ Resolution(..) => {},
+ }
+ unsafe {
+ debug_unreachable!("Forgot a branch?");
+ }
+ },
+ }
+ }
+
+ fn as_number(&self) -> Option<f32> {
+ match *self {
+ Leaf::Length(_) |
+ Leaf::Angle(_) |
+ Leaf::Time(_) |
+ Leaf::Resolution(_) |
+ Leaf::Percentage(_) => None,
+ Leaf::Number(value) => Some(value),
+ }
+ }
+
+ fn sort_key(&self) -> SortKey {
+ match *self {
+ Self::Number(..) => SortKey::Number,
+ Self::Percentage(..) => SortKey::Percentage,
+ Self::Time(..) => SortKey::Sec,
+ Self::Resolution(..) => SortKey::Dppx,
+ Self::Angle(..) => SortKey::Deg,
+ Self::Length(ref l) => match *l {
+ NoCalcLength::Absolute(..) => SortKey::Px,
+ NoCalcLength::FontRelative(ref relative) => match *relative {
+ FontRelativeLength::Ch(..) => SortKey::Ch,
+ FontRelativeLength::Em(..) => SortKey::Em,
+ FontRelativeLength::Ex(..) => SortKey::Ex,
+ FontRelativeLength::Cap(..) => SortKey::Cap,
+ FontRelativeLength::Ic(..) => SortKey::Ic,
+ FontRelativeLength::Rem(..) => SortKey::Rem,
+ FontRelativeLength::Lh(..) => SortKey::Lh,
+ FontRelativeLength::Rlh(..) => SortKey::Rlh,
+ },
+ NoCalcLength::ViewportPercentage(ref vp) => match *vp {
+ ViewportPercentageLength::Vh(..) => SortKey::Vh,
+ ViewportPercentageLength::Svh(..) => SortKey::Svh,
+ ViewportPercentageLength::Lvh(..) => SortKey::Lvh,
+ ViewportPercentageLength::Dvh(..) => SortKey::Dvh,
+ ViewportPercentageLength::Vw(..) => SortKey::Vw,
+ ViewportPercentageLength::Svw(..) => SortKey::Svw,
+ ViewportPercentageLength::Lvw(..) => SortKey::Lvw,
+ ViewportPercentageLength::Dvw(..) => SortKey::Dvw,
+ ViewportPercentageLength::Vmax(..) => SortKey::Vmax,
+ ViewportPercentageLength::Svmax(..) => SortKey::Svmax,
+ ViewportPercentageLength::Lvmax(..) => SortKey::Lvmax,
+ ViewportPercentageLength::Dvmax(..) => SortKey::Dvmax,
+ ViewportPercentageLength::Vmin(..) => SortKey::Vmin,
+ ViewportPercentageLength::Svmin(..) => SortKey::Svmin,
+ ViewportPercentageLength::Lvmin(..) => SortKey::Lvmin,
+ ViewportPercentageLength::Dvmin(..) => SortKey::Dvmin,
+ ViewportPercentageLength::Vb(..) => SortKey::Vb,
+ ViewportPercentageLength::Svb(..) => SortKey::Svb,
+ ViewportPercentageLength::Lvb(..) => SortKey::Lvb,
+ ViewportPercentageLength::Dvb(..) => SortKey::Dvb,
+ ViewportPercentageLength::Vi(..) => SortKey::Vi,
+ ViewportPercentageLength::Svi(..) => SortKey::Svi,
+ ViewportPercentageLength::Lvi(..) => SortKey::Lvi,
+ ViewportPercentageLength::Dvi(..) => SortKey::Dvi,
+ },
+ NoCalcLength::ContainerRelative(ref cq) => match *cq {
+ ContainerRelativeLength::Cqw(..) => SortKey::Cqw,
+ ContainerRelativeLength::Cqh(..) => SortKey::Cqh,
+ ContainerRelativeLength::Cqi(..) => SortKey::Cqi,
+ ContainerRelativeLength::Cqb(..) => SortKey::Cqb,
+ ContainerRelativeLength::Cqmin(..) => SortKey::Cqmin,
+ ContainerRelativeLength::Cqmax(..) => SortKey::Cqmax,
+ },
+ NoCalcLength::ServoCharacterWidth(..) => unreachable!(),
+ },
+ }
+ }
+
+ fn simplify(&mut self) {
+ if let Self::Length(NoCalcLength::Absolute(ref mut abs)) = *self {
+ *abs = AbsoluteLength::Px(abs.to_px());
+ }
+ }
+
+ /// Tries to merge one sum to another, that is, perform `x` + `y`.
+ ///
+ /// Only handles leaf nodes, it's the caller's responsibility to simplify
+ /// them before calling this if needed.
+ fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()> {
+ use self::Leaf::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+
+ match (self, other) {
+ (&mut Number(ref mut one), &Number(ref other)) |
+ (&mut Percentage(ref mut one), &Percentage(ref other)) => {
+ *one += *other;
+ },
+ (&mut Angle(ref mut one), &Angle(ref other)) => {
+ *one = specified::Angle::from_calc(one.degrees() + other.degrees());
+ },
+ (&mut Time(ref mut one), &Time(ref other)) => {
+ *one = specified::Time::from_seconds(one.seconds() + other.seconds());
+ },
+ (&mut Resolution(ref mut one), &Resolution(ref other)) => {
+ *one = specified::Resolution::from_dppx(one.dppx() + other.dppx());
+ },
+ (&mut Length(ref mut one), &Length(ref other)) => {
+ *one = one.try_op(other, std::ops::Add::add)?;
+ },
+ _ => {
+ match *other {
+ Number(..) | Percentage(..) | Angle(..) | Time(..) | Resolution(..) |
+ Length(..) => {},
+ }
+ unsafe {
+ debug_unreachable!();
+ }
+ },
+ }
+
+ Ok(())
+ }
+
+ fn try_product_in_place(&mut self, other: &mut Self) -> bool {
+ if let Self::Number(ref mut left) = *self {
+ if let Self::Number(ref right) = *other {
+ // Both sides are numbers, so we can just modify the left side.
+ *left *= *right;
+ true
+ } else {
+ // The right side is not a number, so the result should be in the units of the right
+ // side.
+ other.map(|v| v * *left);
+ std::mem::swap(self, other);
+ true
+ }
+ } else if let Self::Number(ref right) = *other {
+ // The left side is not a number, but the right side is, so the result is the left
+ // side unit.
+ self.map(|v| v * *right);
+ true
+ } else {
+ // Neither side is a number, so a product is not possible.
+ false
+ }
+ }
+
+ fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()>
+ where
+ O: Fn(f32, f32) -> f32,
+ {
+ use self::Leaf::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+
+ match (self, other) {
+ (&Number(one), &Number(other)) => {
+ return Ok(Leaf::Number(op(one, other)));
+ },
+ (&Percentage(one), &Percentage(other)) => {
+ return Ok(Leaf::Percentage(op(one, other)));
+ },
+ (&Angle(ref one), &Angle(ref other)) => {
+ return Ok(Leaf::Angle(specified::Angle::from_calc(op(
+ one.degrees(),
+ other.degrees(),
+ ))));
+ },
+ (&Resolution(ref one), &Resolution(ref other)) => {
+ return Ok(Leaf::Resolution(specified::Resolution::from_dppx(op(
+ one.dppx(),
+ other.dppx(),
+ ))));
+ },
+ (&Time(ref one), &Time(ref other)) => {
+ return Ok(Leaf::Time(specified::Time::from_seconds(op(
+ one.seconds(),
+ other.seconds(),
+ ))));
+ },
+ (&Length(ref one), &Length(ref other)) => {
+ return Ok(Leaf::Length(one.try_op(other, op)?));
+ },
+ _ => {
+ match *other {
+ Number(..) | Percentage(..) | Angle(..) | Time(..) | Length(..) |
+ Resolution(..) => {},
+ }
+ unsafe {
+ debug_unreachable!();
+ }
+ },
+ }
+ }
+
+ fn map(&mut self, mut op: impl FnMut(f32) -> f32) {
+ match self {
+ Leaf::Length(one) => *one = one.map(op),
+ Leaf::Angle(one) => *one = specified::Angle::from_calc(op(one.degrees())),
+ Leaf::Time(one) => *one = specified::Time::from_seconds(op(one.seconds())),
+ Leaf::Resolution(one) => *one = specified::Resolution::from_dppx(op(one.dppx())),
+ Leaf::Percentage(one) => *one = op(*one),
+ Leaf::Number(one) => *one = op(*one),
+ }
+ }
+}
+
+/// A calc node representation for specified values.
+pub type CalcNode = generic::GenericCalcNode<Leaf>;
+
+impl CalcNode {
+ /// Tries to parse a single element in the expression, that is, a
+ /// `<length>`, `<angle>`, `<time>`, `<percentage>`, `<resolution>`, etc.
+ ///
+ /// May return a "complex" `CalcNode`, in the presence of a parenthesized
+ /// expression, for example.
+ fn parse_one<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allowed_units: CalcUnits,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ match input.next()? {
+ &Token::Number { value, .. } => Ok(CalcNode::Leaf(Leaf::Number(value))),
+ &Token::Dimension {
+ value, ref unit, ..
+ } => {
+ if allowed_units.intersects(CalcUnits::LENGTH) {
+ if let Ok(l) = NoCalcLength::parse_dimension(context, value, unit) {
+ return Ok(CalcNode::Leaf(Leaf::Length(l)));
+ }
+ }
+ if allowed_units.intersects(CalcUnits::ANGLE) {
+ if let Ok(a) = Angle::parse_dimension(value, unit, /* from_calc = */ true) {
+ return Ok(CalcNode::Leaf(Leaf::Angle(a)));
+ }
+ }
+ if allowed_units.intersects(CalcUnits::TIME) {
+ if let Ok(t) = Time::parse_dimension(value, unit) {
+ return Ok(CalcNode::Leaf(Leaf::Time(t)));
+ }
+ }
+ if allowed_units.intersects(CalcUnits::RESOLUTION) {
+ if let Ok(t) = Resolution::parse_dimension(value, unit) {
+ return Ok(CalcNode::Leaf(Leaf::Resolution(t)));
+ }
+ }
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ },
+ &Token::Percentage { unit_value, .. }
+ if allowed_units.intersects(CalcUnits::PERCENTAGE) =>
+ {
+ Ok(CalcNode::Leaf(Leaf::Percentage(unit_value)))
+ },
+ &Token::ParenthesisBlock => input.parse_nested_block(|input| {
+ CalcNode::parse_argument(context, input, allowed_units)
+ }),
+ &Token::Function(ref name) => {
+ let function = CalcNode::math_function(context, name, location)?;
+ CalcNode::parse(context, input, function, allowed_units)
+ },
+ &Token::Ident(ref ident) => {
+ let number = match_ignore_ascii_case! { &**ident,
+ "e" => std::f32::consts::E,
+ "pi" => std::f32::consts::PI,
+ "infinity" => f32::INFINITY,
+ "-infinity" => f32::NEG_INFINITY,
+ "nan" => f32::NAN,
+ _ => return Err(location.new_unexpected_token_error(Token::Ident(ident.clone()))),
+ };
+ Ok(CalcNode::Leaf(Leaf::Number(number)))
+ },
+ t => Err(location.new_unexpected_token_error(t.clone())),
+ }
+ }
+
+ /// Parse a top-level `calc` expression, with all nested sub-expressions.
+ ///
+ /// This is in charge of parsing, for example, `2 + 3 * 100%`.
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ allowed_units: CalcUnits,
+ ) -> Result<Self, ParseError<'i>> {
+ input.parse_nested_block(|input| {
+ match function {
+ MathFunction::Calc => Self::parse_argument(context, input, allowed_units),
+ MathFunction::Clamp => {
+ let min = Self::parse_argument(context, input, allowed_units)?;
+ input.expect_comma()?;
+ let center = Self::parse_argument(context, input, allowed_units)?;
+ input.expect_comma()?;
+ let max = Self::parse_argument(context, input, allowed_units)?;
+ Ok(Self::Clamp {
+ min: Box::new(min),
+ center: Box::new(center),
+ max: Box::new(max),
+ })
+ },
+ MathFunction::Round => {
+ let strategy = input.try_parse(parse_rounding_strategy);
+
+ // <rounding-strategy> = nearest | up | down | to-zero
+ // https://drafts.csswg.org/css-values-4/#calc-syntax
+ fn parse_rounding_strategy<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ ) -> Result<RoundingStrategy, ParseError<'i>> {
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ "nearest" => RoundingStrategy::Nearest,
+ "up" => RoundingStrategy::Up,
+ "down" => RoundingStrategy::Down,
+ "to-zero" => RoundingStrategy::ToZero,
+ })
+ }
+
+ if strategy.is_ok() {
+ input.expect_comma()?;
+ }
+
+ let value = Self::parse_argument(context, input, allowed_units)?;
+ input.expect_comma()?;
+ let step = Self::parse_argument(context, input, allowed_units)?;
+
+ Ok(Self::Round {
+ strategy: strategy.unwrap_or(RoundingStrategy::Nearest),
+ value: Box::new(value),
+ step: Box::new(step),
+ })
+ },
+ MathFunction::Mod | MathFunction::Rem => {
+ let dividend = Self::parse_argument(context, input, allowed_units)?;
+ input.expect_comma()?;
+ let divisor = Self::parse_argument(context, input, allowed_units)?;
+
+ let op = match function {
+ MathFunction::Mod => ModRemOp::Mod,
+ MathFunction::Rem => ModRemOp::Rem,
+ _ => unreachable!(),
+ };
+ Ok(Self::ModRem {
+ dividend: Box::new(dividend),
+ divisor: Box::new(divisor),
+ op,
+ })
+ },
+ MathFunction::Min | MathFunction::Max => {
+ // TODO(emilio): The common case for parse_comma_separated
+ // is just one element, but for min / max is two, really...
+ //
+ // Consider adding an API to cssparser to specify the
+ // initial vector capacity?
+ let arguments = input.parse_comma_separated(|input| {
+ Self::parse_argument(context, input, allowed_units)
+ })?;
+
+ let op = match function {
+ MathFunction::Min => MinMaxOp::Min,
+ MathFunction::Max => MinMaxOp::Max,
+ _ => unreachable!(),
+ };
+
+ Ok(Self::MinMax(arguments.into(), op))
+ },
+ MathFunction::Sin | MathFunction::Cos | MathFunction::Tan => {
+ let a = Self::parse_angle_argument(context, input)?;
+
+ let number = match function {
+ MathFunction::Sin => a.sin(),
+ MathFunction::Cos => a.cos(),
+ MathFunction::Tan => a.tan(),
+ _ => unsafe {
+ debug_unreachable!("We just checked!");
+ },
+ };
+
+ Ok(Self::Leaf(Leaf::Number(number)))
+ },
+ MathFunction::Asin | MathFunction::Acos | MathFunction::Atan => {
+ let a = Self::parse_number_argument(context, input)?;
+
+ let radians = match function {
+ MathFunction::Asin => a.asin(),
+ MathFunction::Acos => a.acos(),
+ MathFunction::Atan => a.atan(),
+ _ => unsafe {
+ debug_unreachable!("We just checked!");
+ },
+ };
+
+ Ok(Self::Leaf(Leaf::Angle(Angle::from_radians(radians))))
+ },
+ MathFunction::Atan2 => {
+ let a = Self::parse_argument(context, input, CalcUnits::ALL)?;
+ input.expect_comma()?;
+ let b = Self::parse_argument(context, input, CalcUnits::ALL)?;
+
+ let radians = Self::try_resolve(input, || {
+ if let Ok(a) = a.to_number() {
+ let b = b.to_number()?;
+ return Ok(a.atan2(b));
+ }
+
+ if let Ok(a) = a.to_percentage() {
+ let b = b.to_percentage()?;
+ return Ok(a.atan2(b));
+ }
+
+ if let Ok(a) = a.to_time(None) {
+ let b = b.to_time(None)?;
+ return Ok(a.seconds().atan2(b.seconds()));
+ }
+
+ if let Ok(a) = a.to_angle() {
+ let b = b.to_angle()?;
+ return Ok(a.radians().atan2(b.radians()));
+ }
+
+ if let Ok(a) = a.to_resolution() {
+ let b = b.to_resolution()?;
+ return Ok(a.dppx().atan2(b.dppx()));
+ }
+
+ let a = a.into_length_or_percentage(AllowedNumericType::All)?;
+ let b = b.into_length_or_percentage(AllowedNumericType::All)?;
+ let (a, b) = CalcLengthPercentage::same_unit_length_as(&a, &b).ok_or(())?;
+
+ Ok(a.atan2(b))
+ })?;
+
+ Ok(Self::Leaf(Leaf::Angle(Angle::from_radians(radians))))
+ },
+ MathFunction::Pow => {
+ let a = Self::parse_number_argument(context, input)?;
+ input.expect_comma()?;
+ let b = Self::parse_number_argument(context, input)?;
+
+ let number = a.powf(b);
+
+ Ok(Self::Leaf(Leaf::Number(number)))
+ },
+ MathFunction::Sqrt => {
+ let a = Self::parse_number_argument(context, input)?;
+
+ let number = a.sqrt();
+
+ Ok(Self::Leaf(Leaf::Number(number)))
+ },
+ MathFunction::Hypot => {
+ let arguments = input.parse_comma_separated(|input| {
+ Self::parse_argument(context, input, allowed_units)
+ })?;
+
+ Ok(Self::Hypot(arguments.into()))
+ },
+ MathFunction::Log => {
+ let a = Self::parse_number_argument(context, input)?;
+ let b = input
+ .try_parse(|input| {
+ input.expect_comma()?;
+ Self::parse_number_argument(context, input)
+ })
+ .ok();
+
+ let number = match b {
+ Some(b) => a.log(b),
+ None => a.ln(),
+ };
+
+ Ok(Self::Leaf(Leaf::Number(number)))
+ },
+ MathFunction::Exp => {
+ let a = Self::parse_number_argument(context, input)?;
+ let number = a.exp();
+ Ok(Self::Leaf(Leaf::Number(number)))
+ },
+ MathFunction::Abs => {
+ let node = Self::parse_argument(context, input, allowed_units)?;
+ Ok(Self::Abs(Box::new(node)))
+ },
+ MathFunction::Sign => {
+ // The sign of a percentage is dependent on the percentage basis, so if
+ // percentages aren't allowed (so there's no basis) we shouldn't allow them in
+ // sign(). The rest of the units are safe tho.
+ let sign_units = allowed_units | (CalcUnits::ALL - CalcUnits::PERCENTAGE);
+ let node = Self::parse_argument(context, input, sign_units)?;
+ Ok(Self::Sign(Box::new(node)))
+ },
+ }
+ })
+ }
+
+ fn parse_angle_argument<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<CSSFloat, ParseError<'i>> {
+ let argument = Self::parse_argument(context, input, CalcUnits::ANGLE)?;
+ argument
+ .to_number()
+ .or_else(|()| Ok(argument.to_angle()?.radians()))
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ fn parse_number_argument<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<CSSFloat, ParseError<'i>> {
+ Self::parse_argument(context, input, CalcUnits::empty())?
+ .to_number()
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ fn parse_argument<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allowed_units: CalcUnits,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut sum = SmallVec::<[CalcNode; 1]>::new();
+ sum.push(Self::parse_product(context, input, allowed_units)?);
+
+ 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('+') => {
+ sum.push(Self::parse_product(context, input, allowed_units)?);
+ },
+ Token::Delim('-') => {
+ let mut rhs = Self::parse_product(context, input, allowed_units)?;
+ rhs.negate();
+ sum.push(rhs);
+ },
+ _ => {
+ input.reset(&start);
+ break;
+ },
+ }
+ },
+ _ => {
+ input.reset(&start);
+ break;
+ },
+ }
+ }
+
+ Ok(if sum.len() == 1 {
+ sum.drain(..).next().unwrap()
+ } else {
+ Self::Sum(sum.into_boxed_slice().into())
+ })
+ }
+
+ /// Parse a top-level `calc` expression, and all the products that may
+ /// follow, and stop as soon as a non-product expression is found.
+ ///
+ /// This should parse correctly:
+ ///
+ /// * `2`
+ /// * `2 * 2`
+ /// * `2 * 2 + 2` (but will leave the `+ 2` unparsed).
+ ///
+ fn parse_product<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allowed_units: CalcUnits,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut product = SmallVec::<[CalcNode; 1]>::new();
+ product.push(Self::parse_one(context, input, allowed_units)?);
+
+ loop {
+ let start = input.state();
+ match input.next() {
+ Ok(&Token::Delim('*')) => {
+ let mut rhs = Self::parse_one(context, input, allowed_units)?;
+
+ // We can unwrap here, becuase we start the function by adding a node to
+ // the list.
+ if !product.last_mut().unwrap().try_product_in_place(&mut rhs) {
+ product.push(rhs);
+ }
+ },
+ Ok(&Token::Delim('/')) => {
+ let rhs = Self::parse_one(context, input, allowed_units)?;
+
+ enum InPlaceDivisionResult {
+ /// The right was merged into the left.
+ Merged,
+ /// The right is not a number or could not be resolved, so the left is
+ /// unchanged.
+ Unchanged,
+ /// The right was resolved, but was not a number, so the calculation is
+ /// invalid.
+ Invalid,
+ }
+
+ fn try_division_in_place(
+ left: &mut CalcNode,
+ right: &CalcNode,
+ ) -> InPlaceDivisionResult {
+ if let Ok(resolved) = right.resolve() {
+ if let Some(number) = resolved.as_number() {
+ if number != 1.0 && left.is_product_distributive() {
+ left.map(|l| l / number);
+ return InPlaceDivisionResult::Merged;
+ }
+ } else {
+ return InPlaceDivisionResult::Invalid;
+ }
+ }
+ InPlaceDivisionResult::Unchanged
+ }
+
+ // The right hand side of a division *must* be a number, so if we can
+ // already resolve it, then merge it with the last node on the product list.
+ // We can unwrap here, becuase we start the function by adding a node to
+ // the list.
+ match try_division_in_place(product.last_mut().unwrap(), &rhs) {
+ InPlaceDivisionResult::Merged => {},
+ InPlaceDivisionResult::Unchanged => {
+ product.push(Self::Invert(Box::new(rhs)))
+ },
+ InPlaceDivisionResult::Invalid => {
+ return Err(
+ input.new_custom_error(StyleParseErrorKind::UnspecifiedError)
+ )
+ },
+ }
+ },
+ _ => {
+ input.reset(&start);
+ break;
+ },
+ }
+ }
+
+ Ok(if product.len() == 1 {
+ product.drain(..).next().unwrap()
+ } else {
+ Self::Product(product.into_boxed_slice().into())
+ })
+ }
+
+ fn try_resolve<'i, 't, F>(
+ input: &Parser<'i, 't>,
+ closure: F,
+ ) -> Result<CSSFloat, ParseError<'i>>
+ where
+ F: FnOnce() -> Result<CSSFloat, ()>,
+ {
+ closure().map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Tries to simplify this expression into a `<length>` or `<percentage>`
+ /// value.
+ pub fn into_length_or_percentage(
+ mut self,
+ clamping_mode: AllowedNumericType,
+ ) -> Result<CalcLengthPercentage, ()> {
+ self.simplify_and_sort();
+
+ // Although we allow numbers inside CalcLengthPercentage, calculations that resolve to a
+ // number result is still not allowed.
+ let unit = self.unit()?;
+ if !CalcUnits::LENGTH_PERCENTAGE.intersects(unit) {
+ Err(())
+ } else {
+ Ok(CalcLengthPercentage {
+ clamping_mode,
+ node: self,
+ })
+ }
+ }
+
+ /// Tries to simplify this expression into a `<time>` value.
+ fn to_time(&self, clamping_mode: Option<AllowedNumericType>) -> Result<Time, ()> {
+ let seconds = if let Leaf::Time(time) = self.resolve()? {
+ time.seconds()
+ } else {
+ return Err(());
+ };
+
+ Ok(Time::from_seconds_with_calc_clamping_mode(
+ seconds,
+ clamping_mode,
+ ))
+ }
+
+ /// Tries to simplify the expression into a `<resolution>` value.
+ fn to_resolution(&self) -> Result<Resolution, ()> {
+ let dppx = if let Leaf::Resolution(resolution) = self.resolve()? {
+ resolution.dppx()
+ } else {
+ return Err(());
+ };
+
+ Ok(Resolution::from_dppx_calc(dppx))
+ }
+
+ /// Tries to simplify this expression into an `Angle` value.
+ fn to_angle(&self) -> Result<Angle, ()> {
+ let degrees = if let Leaf::Angle(angle) = self.resolve()? {
+ angle.degrees()
+ } else {
+ return Err(());
+ };
+
+ let result = Angle::from_calc(degrees);
+ Ok(result)
+ }
+
+ /// Tries to simplify this expression into a `<number>` value.
+ fn to_number(&self) -> Result<CSSFloat, ()> {
+ let number = if let Leaf::Number(number) = self.resolve()? {
+ number
+ } else {
+ return Err(());
+ };
+
+ let result = number;
+
+ Ok(result)
+ }
+
+ /// Tries to simplify this expression into a `<percentage>` value.
+ fn to_percentage(&self) -> Result<CSSFloat, ()> {
+ if let Leaf::Percentage(percentage) = self.resolve()? {
+ Ok(percentage)
+ } else {
+ Err(())
+ }
+ }
+
+ /// Given a function name, and the location from where the token came from,
+ /// return a mathematical function corresponding to that name or an error.
+ #[inline]
+ pub fn math_function<'i>(
+ _: &ParserContext,
+ name: &CowRcStr<'i>,
+ location: cssparser::SourceLocation,
+ ) -> Result<MathFunction, ParseError<'i>> {
+ let function = match MathFunction::from_ident(&*name) {
+ Ok(f) => f,
+ Err(()) => {
+ return Err(location.new_unexpected_token_error(Token::Function(name.clone())))
+ },
+ };
+
+ Ok(function)
+ }
+
+ /// Convenience parsing function for integers.
+ pub fn parse_integer<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<CSSInteger, ParseError<'i>> {
+ Self::parse_number(context, input, function).map(|n| (n + 0.5).floor() as CSSInteger)
+ }
+
+ /// Convenience parsing function for `<length> | <percentage>`.
+ pub fn parse_length_or_percentage<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ clamping_mode: AllowedNumericType,
+ function: MathFunction,
+ ) -> Result<CalcLengthPercentage, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::LENGTH_PERCENTAGE)?
+ .into_length_or_percentage(clamping_mode)
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for percentages.
+ pub fn parse_percentage<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<CSSFloat, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::PERCENTAGE)?
+ .to_percentage()
+ .map(crate::values::normalize)
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for `<length>`.
+ pub fn parse_length<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ clamping_mode: AllowedNumericType,
+ function: MathFunction,
+ ) -> Result<CalcLengthPercentage, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::LENGTH)?
+ .into_length_or_percentage(clamping_mode)
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for `<number>`.
+ pub fn parse_number<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<CSSFloat, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::empty())?
+ .to_number()
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for `<angle>`.
+ pub fn parse_angle<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<Angle, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::ANGLE)?
+ .to_angle()
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for `<time>`.
+ pub fn parse_time<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ clamping_mode: AllowedNumericType,
+ function: MathFunction,
+ ) -> Result<Time, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::TIME)?
+ .to_time(Some(clamping_mode))
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for `<resolution>`.
+ pub fn parse_resolution<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<Resolution, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::RESOLUTION)?
+ .to_resolution()
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for `<number>` or `<percentage>`.
+ pub fn parse_number_or_percentage<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<NumberOrPercentage, ParseError<'i>> {
+ let node = Self::parse(context, input, function, CalcUnits::PERCENTAGE)?;
+
+ if let Ok(value) = node.to_number() {
+ return Ok(NumberOrPercentage::Number { value });
+ }
+
+ match node.to_percentage() {
+ Ok(unit_value) => Ok(NumberOrPercentage::Percentage { unit_value }),
+ Err(()) => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ }
+ }
+
+ /// Convenience parsing function for `<number>` or `<angle>`.
+ pub fn parse_angle_or_number<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<AngleOrNumber, ParseError<'i>> {
+ let node = Self::parse(context, input, function, CalcUnits::ANGLE)?;
+
+ if let Ok(angle) = node.to_angle() {
+ let degrees = angle.degrees();
+ return Ok(AngleOrNumber::Angle { degrees });
+ }
+
+ match node.to_number() {
+ Ok(value) => Ok(AngleOrNumber::Number { value }),
+ Err(()) => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ }
+ }
+}