servo/components/style/values/specified/calc.rs
Emilio Cobos Álvarez 908c952ab0 style: Don't simplify percentages that resolve to lengths in min/max/clamp
Those can't be ordered at specified / computed value time, since the
percentage basis could be negative.

Needs tests of course, running through try atm.

Differential Revision: https://phabricator.services.mozilla.com/D115591
2023-05-24 18:32:31 +02:00

649 lines
24 KiB
Rust

/* 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) {
// NOTE: Percentages can't be compared reasonably here because the
// percentage basis might be negative, see bug 1709018.
// Conveniently, we only use this for <length-percentage> (for raw
// percentages, we go through resolve()).
(&Percentage(..), &Percentage(..)) => None,
(&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()),
(&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::Cap(..) => SortKey::Cap,
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);
},
_ => {
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>,
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)),
}
}
}