summaryrefslogtreecommitdiffstats
path: root/servo/components/style/values/specified/calc.rs
diff options
context:
space:
mode:
Diffstat (limited to 'servo/components/style/values/specified/calc.rs')
-rw-r--r--servo/components/style/values/specified/calc.rs644
1 files changed, 644 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..eb8b2fe421
--- /dev/null
+++ b/servo/components/style/values/specified/calc.rs
@@ -0,0 +1,644 @@
+/* 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::parser::ParserContext;
+use crate::values::generics::calc as generic;
+use crate::values::generics::calc::{MinMaxOp, SortKey};
+use crate::values::specified::length::ViewportPercentageLength;
+use crate::values::specified::length::{AbsoluteLength, FontRelativeLength, NoCalcLength};
+use crate::values::specified::{self, Angle, Time};
+use crate::values::{CSSFloat, CSSInteger};
+use cssparser::{AngleOrNumber, CowRcStr, NumberOrPercentage, 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)]
+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,
+}
+
+/// 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),
+ /// `<percentage>`
+ Percentage(CSSFloat),
+ /// `<number>`
+ Number(CSSFloat),
+}
+
+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(ref n) => n.to_css(dest),
+ Self::Percentage(p) => crate::values::serialize_percentage(p, dest),
+ Self::Angle(ref a) => a.to_css(dest),
+ Self::Time(ref t) => t.to_css(dest),
+ }
+ }
+}
+
+/// An expected unit we intend to parse within a `calc()` expression.
+///
+/// This is used as a hint for the parser to fast-reject invalid expressions.
+#[derive(Clone, Copy, PartialEq)]
+enum CalcUnit {
+ /// `<number>`
+ Number,
+ /// `<length>`
+ Length,
+ /// `<percentage>`
+ Percentage,
+ /// `<length> | <percentage>`
+ LengthPercentage,
+ /// `<angle>`
+ Angle,
+ /// `<time>`
+ Time,
+}
+
+/// 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 SpecifiedValueInfo for CalcLengthPercentage {}
+
+impl PartialOrd for Leaf {
+ fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+ use self::Leaf::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return None;
+ }
+
+ match (self, other) {
+ (&Length(ref one), &Length(ref other)) => one.partial_cmp(other),
+ (&Percentage(ref one), &Percentage(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()),
+ (&Number(ref one), &Number(ref other)) => one.partial_cmp(other),
+ _ => {
+ match *self {
+ Length(..) | Percentage(..) | Angle(..) | Time(..) | Number(..) => {},
+ }
+ unsafe {
+ debug_unreachable!("Forgot a branch?");
+ }
+ },
+ }
+ }
+}
+
+impl generic::CalcNodeLeaf for Leaf {
+ fn is_negative(&self) -> bool {
+ match *self {
+ Self::Length(ref l) => l.is_negative(),
+ Self::Percentage(n) | Self::Number(n) => n < 0.,
+ Self::Angle(ref a) => a.degrees() < 0.,
+ Self::Time(ref t) => t.seconds() < 0.,
+ }
+ }
+
+ fn mul_by(&mut self, scalar: f32) {
+ match *self {
+ Self::Length(ref mut l) => {
+ // FIXME: For consistency this should probably convert absolute
+ // lengths into pixels.
+ *l = *l * scalar;
+ },
+ Self::Number(ref mut n) => {
+ *n *= scalar;
+ },
+ Self::Angle(ref mut a) => {
+ *a = Angle::from_calc(a.degrees() * scalar);
+ },
+ Self::Time(ref mut t) => {
+ *t = Time::from_calc(t.seconds() * scalar);
+ },
+ Self::Percentage(ref mut p) => {
+ *p *= scalar;
+ },
+ }
+ }
+
+ fn sort_key(&self) -> SortKey {
+ match *self {
+ Self::Number(..) => SortKey::Number,
+ Self::Percentage(..) => SortKey::Percentage,
+ Self::Time(..) => SortKey::Sec,
+ 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::Rem(..) => SortKey::Rem,
+ },
+ NoCalcLength::ViewportPercentage(ref vp) => match *vp {
+ ViewportPercentageLength::Vh(..) => SortKey::Vh,
+ ViewportPercentageLength::Vw(..) => SortKey::Vw,
+ ViewportPercentageLength::Vmax(..) => SortKey::Vmax,
+ ViewportPercentageLength::Vmin(..) => SortKey::Vmin,
+ },
+ 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_calc(one.seconds() + other.seconds());
+ },
+ (&mut Length(ref mut one), &Length(ref other)) => {
+ *one = one.try_sum(other)?;
+ },
+ _ => {
+ match *other {
+ Number(..) | Percentage(..) | Angle(..) | Time(..) | Length(..) => {},
+ }
+ unsafe {
+ debug_unreachable!();
+ }
+ },
+ }
+
+ Ok(())
+ }
+}
+
+/// 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>`, according to
+ /// `expected_unit`.
+ ///
+ /// 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>,
+ expected_unit: CalcUnit,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ match (input.next()?, expected_unit) {
+ (&Token::Number { value, .. }, _) => Ok(CalcNode::Leaf(Leaf::Number(value))),
+ (
+ &Token::Dimension {
+ value, ref unit, ..
+ },
+ CalcUnit::Length,
+ ) |
+ (
+ &Token::Dimension {
+ value, ref unit, ..
+ },
+ CalcUnit::LengthPercentage,
+ ) => match NoCalcLength::parse_dimension(context, value, unit) {
+ Ok(l) => Ok(CalcNode::Leaf(Leaf::Length(l))),
+ Err(()) => Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ },
+ (
+ &Token::Dimension {
+ value, ref unit, ..
+ },
+ CalcUnit::Angle,
+ ) => {
+ match Angle::parse_dimension(value, unit, /* from_calc = */ true) {
+ Ok(a) => Ok(CalcNode::Leaf(Leaf::Angle(a))),
+ Err(()) => {
+ Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ },
+ }
+ },
+ (
+ &Token::Dimension {
+ value, ref unit, ..
+ },
+ CalcUnit::Time,
+ ) => {
+ match Time::parse_dimension(value, unit, /* from_calc = */ true) {
+ Ok(t) => Ok(CalcNode::Leaf(Leaf::Time(t))),
+ Err(()) => {
+ Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ },
+ }
+ },
+ (&Token::Percentage { unit_value, .. }, CalcUnit::LengthPercentage) |
+ (&Token::Percentage { unit_value, .. }, CalcUnit::Percentage) => {
+ Ok(CalcNode::Leaf(Leaf::Percentage(unit_value)))
+ },
+ (&Token::ParenthesisBlock, _) => input.parse_nested_block(|input| {
+ CalcNode::parse_argument(context, input, expected_unit)
+ }),
+ (&Token::Function(ref name), _) => {
+ let function = CalcNode::math_function(name, location)?;
+ CalcNode::parse(context, input, function, expected_unit)
+ },
+ (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,
+ expected_unit: CalcUnit,
+ ) -> Result<Self, ParseError<'i>> {
+ // TODO: Do something different based on the function name. In
+ // particular, for non-calc function we need to take a list of
+ // comma-separated arguments and such.
+ input.parse_nested_block(|input| {
+ match function {
+ MathFunction::Calc => Self::parse_argument(context, input, expected_unit),
+ MathFunction::Clamp => {
+ let min = Self::parse_argument(context, input, expected_unit)?;
+ input.expect_comma()?;
+ let center = Self::parse_argument(context, input, expected_unit)?;
+ input.expect_comma()?;
+ let max = Self::parse_argument(context, input, expected_unit)?;
+ Ok(Self::Clamp {
+ min: Box::new(min),
+ center: Box::new(center),
+ max: Box::new(max),
+ })
+ },
+ 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, expected_unit)
+ })?;
+
+ let op = match function {
+ MathFunction::Min => MinMaxOp::Min,
+ MathFunction::Max => MinMaxOp::Max,
+ _ => unreachable!(),
+ };
+
+ Ok(Self::MinMax(arguments.into(), op))
+ },
+ }
+ })
+ }
+
+ fn parse_argument<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ expected_unit: CalcUnit,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut sum = SmallVec::<[CalcNode; 1]>::new();
+ sum.push(Self::parse_product(context, input, expected_unit)?);
+
+ 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, expected_unit)?);
+ },
+ Token::Delim('-') => {
+ let mut rhs = Self::parse_product(context, input, expected_unit)?;
+ rhs.negate();
+ sum.push(rhs);
+ },
+ ref t => {
+ let t = t.clone();
+ return Err(input.new_unexpected_token_error(t));
+ },
+ }
+ },
+ _ => {
+ 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>,
+ expected_unit: CalcUnit,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut node = Self::parse_one(context, input, expected_unit)?;
+
+ loop {
+ let start = input.state();
+ match input.next() {
+ Ok(&Token::Delim('*')) => {
+ let rhs = Self::parse_one(context, input, expected_unit)?;
+ if let Ok(rhs) = rhs.to_number() {
+ node.mul_by(rhs);
+ } else if let Ok(number) = node.to_number() {
+ node = rhs;
+ node.mul_by(number);
+ } else {
+ // One of the two parts of the multiplication has to be
+ // a number, at least until we implement unit math.
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ },
+ Ok(&Token::Delim('/')) => {
+ let rhs = Self::parse_one(context, input, expected_unit)?;
+ // Dividing by units is not ok.
+ //
+ // TODO(emilio): Eventually it should be.
+ let number = match rhs.to_number() {
+ Ok(n) if n != 0. => n,
+ _ => {
+ return Err(
+ input.new_custom_error(StyleParseErrorKind::UnspecifiedError)
+ );
+ },
+ };
+ node.mul_by(1. / number);
+ },
+ _ => {
+ input.reset(&start);
+ break;
+ },
+ }
+ }
+
+ Ok(node)
+ }
+
+ /// Tries to simplify this expression into a `<length>` or `<percentage>`
+ /// value.
+ fn into_length_or_percentage(
+ mut self,
+ clamping_mode: AllowedNumericType,
+ ) -> Result<CalcLengthPercentage, ()> {
+ // Keep track of whether there's any invalid member of the calculation,
+ // so as to reject the calculation properly at parse-time.
+ let mut any_invalid = false;
+ self.visit_depth_first(|node| {
+ if let CalcNode::Leaf(ref l) = *node {
+ any_invalid |= !matches!(*l, Leaf::Percentage(..) | Leaf::Length(..));
+ }
+ node.simplify_and_sort_direct_children();
+ });
+
+ if any_invalid {
+ return Err(());
+ }
+
+ Ok(CalcLengthPercentage {
+ clamping_mode,
+ node: self,
+ })
+ }
+
+ /// Tries to simplify this expression into a `<time>` value.
+ fn to_time(&self) -> Result<Time, ()> {
+ let seconds = self.resolve(|leaf| match *leaf {
+ Leaf::Time(ref t) => Ok(t.seconds()),
+ _ => Err(()),
+ })?;
+ Ok(Time::from_calc(crate::values::normalize(seconds)))
+ }
+
+ /// Tries to simplify this expression into an `Angle` value.
+ fn to_angle(&self) -> Result<Angle, ()> {
+ let degrees = self.resolve(|leaf| match *leaf {
+ Leaf::Angle(ref angle) => Ok(angle.degrees()),
+ _ => Err(()),
+ })?;
+ Ok(Angle::from_calc(crate::values::normalize(degrees)))
+ }
+
+ /// Tries to simplify this expression into a `<number>` value.
+ fn to_number(&self) -> Result<CSSFloat, ()> {
+ self.resolve(|leaf| match *leaf {
+ Leaf::Number(n) => Ok(n),
+ _ => Err(()),
+ })
+ }
+
+ /// Tries to simplify this expression into a `<percentage>` value.
+ fn to_percentage(&self) -> Result<CSSFloat, ()> {
+ self.resolve(|leaf| match *leaf {
+ Leaf::Percentage(p) => Ok(p),
+ _ => 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>(
+ name: &CowRcStr<'i>,
+ location: cssparser::SourceLocation,
+ ) -> Result<MathFunction, ParseError<'i>> {
+ Ok(match_ignore_ascii_case! { &*name,
+ "calc" => MathFunction::Calc,
+ "min" => MathFunction::Min,
+ "max" => MathFunction::Max,
+ "clamp" => MathFunction::Clamp,
+ _ => return Err(location.new_unexpected_token_error(Token::Function(name.clone()))),
+ })
+ }
+
+ /// 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.round() 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, CalcUnit::LengthPercentage)?
+ .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, CalcUnit::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, CalcUnit::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, CalcUnit::Number)?
+ .to_number()
+ .map(crate::values::normalize)
+ .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, CalcUnit::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>,
+ function: MathFunction,
+ ) -> Result<Time, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnit::Time)?
+ .to_time()
+ .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, CalcUnit::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, CalcUnit::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)),
+ }
+ }
+}