style: Centralize calc function parsing.

So that extending it to support other math functions like min / max / etc is
simpler.

There should be no behavior change with this patch, though I added a comment to
some places where we don't do calc() clamping correctly (though other browsers
don't either so...).

Differential Revision: https://phabricator.services.mozilla.com/D59939
This commit is contained in:
Emilio Cobos Álvarez 2020-01-15 00:46:01 +00:00
parent 9026720f04
commit d74f90e3a7
No known key found for this signature in database
GPG key ID: E1152D0994E4BF8A
7 changed files with 116 additions and 58 deletions

View file

@ -208,7 +208,9 @@ impl Angle {
input: &mut Parser<'i, 't>, input: &mut Parser<'i, 't>,
allow_unitless_zero: AllowUnitlessZeroAngle, allow_unitless_zero: AllowUnitlessZeroAngle,
) -> Result<Self, ParseError<'i>> { ) -> Result<Self, ParseError<'i>> {
let location = input.current_source_location();
let t = input.next()?; let t = input.next()?;
let allow_unitless_zero = matches!(allow_unitless_zero, AllowUnitlessZeroAngle::Yes);
match *t { match *t {
Token::Dimension { Token::Dimension {
value, ref unit, .. value, ref unit, ..
@ -221,15 +223,12 @@ impl Angle {
}, },
} }
}, },
Token::Number { value, .. } if value == 0. => match allow_unitless_zero { Token::Function(ref name) => {
AllowUnitlessZeroAngle::Yes => Ok(Angle::zero()), let function = CalcNode::math_function(name, location)?;
AllowUnitlessZeroAngle::No => { CalcNode::parse_angle(context, input, function)
let t = t.clone();
Err(input.new_unexpected_token_error(t))
},
}, },
Token::Function(ref name) if name.eq_ignore_ascii_case("calc") => { Token::Number { value, .. } if value == 0. && allow_unitless_zero => {
return input.parse_nested_block(|i| CalcNode::parse_angle(context, i)); Ok(Angle::zero())
}, },
ref t => { ref t => {
let t = t.clone(); let t = t.clone();

View file

@ -12,11 +12,19 @@ use crate::values::specified::length::ViewportPercentageLength;
use crate::values::specified::length::{AbsoluteLength, FontRelativeLength, NoCalcLength}; use crate::values::specified::length::{AbsoluteLength, FontRelativeLength, NoCalcLength};
use crate::values::specified::{Angle, Time}; use crate::values::specified::{Angle, Time};
use crate::values::{CSSFloat, CSSInteger}; use crate::values::{CSSFloat, CSSInteger};
use cssparser::{AngleOrNumber, NumberOrPercentage, Parser, Token}; use cssparser::{AngleOrNumber, CowRcStr, NumberOrPercentage, Parser, Token};
use std::fmt::{self, Write}; use std::fmt::{self, Write};
use style_traits::values::specified::AllowedNumericType; use style_traits::values::specified::AllowedNumericType;
use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss}; use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss};
/// The name of the mathematical function that we're parsing.
#[derive(Debug, Copy, Clone)]
pub enum MathFunction {
/// `calc()`
Calc,
// FIXME: min() / max() / clamp
}
/// A node inside a `Calc` expression's AST. /// A node inside a `Calc` expression's AST.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum CalcNode { pub enum CalcNode {
@ -204,10 +212,13 @@ impl CalcNode {
Ok(CalcNode::Percentage(unit_value)) Ok(CalcNode::Percentage(unit_value))
}, },
(&Token::ParenthesisBlock, _) => { (&Token::ParenthesisBlock, _) => {
input.parse_nested_block(|i| CalcNode::parse(context, i, expected_unit)) input.parse_nested_block(|input| {
CalcNode::parse_argument(context, input, expected_unit)
})
}, },
(&Token::Function(ref name), _) if name.eq_ignore_ascii_case("calc") => { (&Token::Function(ref name), _) => {
input.parse_nested_block(|i| CalcNode::parse(context, i, expected_unit)) let function = CalcNode::math_function(name, location)?;
CalcNode::parse(context, input, function, expected_unit)
}, },
(t, _) => Err(location.new_unexpected_token_error(t.clone())), (t, _) => Err(location.new_unexpected_token_error(t.clone())),
} }
@ -217,6 +228,20 @@ impl CalcNode {
/// ///
/// This is in charge of parsing, for example, `2 + 3 * 100%`. /// This is in charge of parsing, for example, `2 + 3 * 100%`.
fn parse<'i, 't>( 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| {
Self::parse_argument(context, input, expected_unit)
})
}
fn parse_argument<'i, 't>(
context: &ParserContext, context: &ParserContext,
input: &mut Parser<'i, 't>, input: &mut Parser<'i, 't>,
expected_unit: CalcUnit, expected_unit: CalcUnit,
@ -526,12 +551,26 @@ impl CalcNode {
}) })
} }
/// 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>> {
if !name.eq_ignore_ascii_case("calc") {
return Err(location.new_unexpected_token_error(Token::Function(name.clone())));
}
Ok(MathFunction::Calc)
}
/// Convenience parsing function for integers. /// Convenience parsing function for integers.
pub fn parse_integer<'i, 't>( pub fn parse_integer<'i, 't>(
context: &ParserContext, context: &ParserContext,
input: &mut Parser<'i, 't>, input: &mut Parser<'i, 't>,
function: MathFunction,
) -> Result<CSSInteger, ParseError<'i>> { ) -> Result<CSSInteger, ParseError<'i>> {
Self::parse_number(context, input).map(|n| n.round() as CSSInteger) Self::parse_number(context, input, function).map(|n| n.round() as CSSInteger)
} }
/// Convenience parsing function for `<length> | <percentage>`. /// Convenience parsing function for `<length> | <percentage>`.
@ -539,8 +578,9 @@ impl CalcNode {
context: &ParserContext, context: &ParserContext,
input: &mut Parser<'i, 't>, input: &mut Parser<'i, 't>,
clamping_mode: AllowedNumericType, clamping_mode: AllowedNumericType,
function: MathFunction,
) -> Result<CalcLengthPercentage, ParseError<'i>> { ) -> Result<CalcLengthPercentage, ParseError<'i>> {
Self::parse(context, input, CalcUnit::LengthPercentage)? Self::parse(context, input, function, CalcUnit::LengthPercentage)?
.to_length_or_percentage(clamping_mode) .to_length_or_percentage(clamping_mode)
.map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
} }
@ -549,8 +589,9 @@ impl CalcNode {
pub fn parse_percentage<'i, 't>( pub fn parse_percentage<'i, 't>(
context: &ParserContext, context: &ParserContext,
input: &mut Parser<'i, 't>, input: &mut Parser<'i, 't>,
function: MathFunction,
) -> Result<CSSFloat, ParseError<'i>> { ) -> Result<CSSFloat, ParseError<'i>> {
Self::parse(context, input, CalcUnit::Percentage)? Self::parse(context, input, function, CalcUnit::Percentage)?
.to_percentage() .to_percentage()
.map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
} }
@ -560,8 +601,9 @@ impl CalcNode {
context: &ParserContext, context: &ParserContext,
input: &mut Parser<'i, 't>, input: &mut Parser<'i, 't>,
clamping_mode: AllowedNumericType, clamping_mode: AllowedNumericType,
function: MathFunction,
) -> Result<CalcLengthPercentage, ParseError<'i>> { ) -> Result<CalcLengthPercentage, ParseError<'i>> {
Self::parse(context, input, CalcUnit::Length)? Self::parse(context, input, function, CalcUnit::Length)?
.to_length_or_percentage(clamping_mode) .to_length_or_percentage(clamping_mode)
.map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
} }
@ -570,8 +612,9 @@ impl CalcNode {
pub fn parse_number<'i, 't>( pub fn parse_number<'i, 't>(
context: &ParserContext, context: &ParserContext,
input: &mut Parser<'i, 't>, input: &mut Parser<'i, 't>,
function: MathFunction,
) -> Result<CSSFloat, ParseError<'i>> { ) -> Result<CSSFloat, ParseError<'i>> {
Self::parse(context, input, CalcUnit::Number)? Self::parse(context, input, function, CalcUnit::Number)?
.to_number() .to_number()
.map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
} }
@ -580,8 +623,9 @@ impl CalcNode {
pub fn parse_angle<'i, 't>( pub fn parse_angle<'i, 't>(
context: &ParserContext, context: &ParserContext,
input: &mut Parser<'i, 't>, input: &mut Parser<'i, 't>,
function: MathFunction,
) -> Result<Angle, ParseError<'i>> { ) -> Result<Angle, ParseError<'i>> {
Self::parse(context, input, CalcUnit::Angle)? Self::parse(context, input, function, CalcUnit::Angle)?
.to_angle() .to_angle()
.map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
} }
@ -590,8 +634,9 @@ impl CalcNode {
pub fn parse_time<'i, 't>( pub fn parse_time<'i, 't>(
context: &ParserContext, context: &ParserContext,
input: &mut Parser<'i, 't>, input: &mut Parser<'i, 't>,
function: MathFunction,
) -> Result<Time, ParseError<'i>> { ) -> Result<Time, ParseError<'i>> {
Self::parse(context, input, CalcUnit::Time)? Self::parse(context, input, function, CalcUnit::Time)?
.to_time() .to_time()
.map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
} }
@ -600,8 +645,9 @@ impl CalcNode {
pub fn parse_number_or_percentage<'i, 't>( pub fn parse_number_or_percentage<'i, 't>(
context: &ParserContext, context: &ParserContext,
input: &mut Parser<'i, 't>, input: &mut Parser<'i, 't>,
function: MathFunction,
) -> Result<NumberOrPercentage, ParseError<'i>> { ) -> Result<NumberOrPercentage, ParseError<'i>> {
let node = Self::parse(context, input, CalcUnit::Percentage)?; let node = Self::parse(context, input, function, CalcUnit::Percentage)?;
if let Ok(value) = node.to_number() { if let Ok(value) = node.to_number() {
return Ok(NumberOrPercentage::Number { value }); return Ok(NumberOrPercentage::Number { value });
@ -617,8 +663,9 @@ impl CalcNode {
pub fn parse_angle_or_number<'i, 't>( pub fn parse_angle_or_number<'i, 't>(
context: &ParserContext, context: &ParserContext,
input: &mut Parser<'i, 't>, input: &mut Parser<'i, 't>,
function: MathFunction,
) -> Result<AngleOrNumber, ParseError<'i>> { ) -> Result<AngleOrNumber, ParseError<'i>> {
let node = Self::parse(context, input, CalcUnit::Angle)?; let node = Self::parse(context, input, function, CalcUnit::Angle)?;
if let Ok(angle) = node.to_angle() { if let Ok(angle) = node.to_angle() {
let degrees = angle.degrees(); let degrees = angle.degrees();

View file

@ -298,8 +298,9 @@ impl<'a, 'b: 'a, 'i: 'a> ::cssparser::ColorComponentParser<'i> for ColorComponen
Ok(AngleOrNumber::Angle { degrees }) Ok(AngleOrNumber::Angle { degrees })
}, },
Token::Number { value, .. } => Ok(AngleOrNumber::Number { value }), Token::Number { value, .. } => Ok(AngleOrNumber::Number { value }),
Token::Function(ref name) if name.eq_ignore_ascii_case("calc") => { Token::Function(ref name) => {
input.parse_nested_block(|i| CalcNode::parse_angle_or_number(self.0, i)) let function = CalcNode::math_function(name, location)?;
CalcNode::parse_angle_or_number(self.0, input, function)
}, },
t => return Err(location.new_unexpected_token_error(t)), t => return Err(location.new_unexpected_token_error(t)),
} }
@ -323,15 +324,16 @@ impl<'a, 'b: 'a, 'i: 'a> ::cssparser::ColorComponentParser<'i> for ColorComponen
) -> Result<NumberOrPercentage, ParseError<'i>> { ) -> Result<NumberOrPercentage, ParseError<'i>> {
let location = input.current_source_location(); let location = input.current_source_location();
match input.next()?.clone() { match *input.next()? {
Token::Number { value, .. } => Ok(NumberOrPercentage::Number { value }), Token::Number { value, .. } => Ok(NumberOrPercentage::Number { value }),
Token::Percentage { unit_value, .. } => { Token::Percentage { unit_value, .. } => {
Ok(NumberOrPercentage::Percentage { unit_value }) Ok(NumberOrPercentage::Percentage { unit_value })
}, },
Token::Function(ref name) if name.eq_ignore_ascii_case("calc") => { Token::Function(ref name) => {
input.parse_nested_block(|i| CalcNode::parse_number_or_percentage(self.0, i)) let function = CalcNode::math_function(name, location)?;
CalcNode::parse_number_or_percentage(self.0, input, function)
}, },
t => return Err(location.new_unexpected_token_error(t)), ref t => return Err(location.new_unexpected_token_error(t.clone())),
} }
} }
} }

View file

@ -599,11 +599,11 @@ impl Length {
value, value,
)))) ))))
}, },
Token::Function(ref name) if name.eq_ignore_ascii_case("calc") => input Token::Function(ref name) => {
.parse_nested_block(|input| { let function = CalcNode::math_function(name, location)?;
CalcNode::parse_length(context, input, num_context) let calc = CalcNode::parse_length(context, input, num_context, function)?;
.map(|calc| Length::Calc(Box::new(calc))) Ok(Length::Calc(Box::new(calc)))
}), },
ref token => return Err(location.new_unexpected_token_error(token.clone())), ref token => return Err(location.new_unexpected_token_error(token.clone())),
} }
} }
@ -822,10 +822,9 @@ impl LengthPercentage {
return Ok(LengthPercentage::Length(NoCalcLength::from_px(value))); return Ok(LengthPercentage::Length(NoCalcLength::from_px(value)));
} }
}, },
Token::Function(ref name) if name.eq_ignore_ascii_case("calc") => { Token::Function(ref name) => {
let calc = input.parse_nested_block(|i| { let function = CalcNode::math_function(name, location)?;
CalcNode::parse_length_or_percentage(context, i, num_context) let calc = CalcNode::parse_length_or_percentage(context, input, num_context, function)?;
})?;
Ok(LengthPercentage::Calc(Box::new(calc))) Ok(LengthPercentage::Calc(Box::new(calc)))
}, },
_ => return Err(location.new_unexpected_token_error(token.clone())), _ => return Err(location.new_unexpected_token_error(token.clone())),

View file

@ -144,8 +144,9 @@ fn parse_number_with_clamping_mode<'i, 't>(
calc_clamping_mode: None, calc_clamping_mode: None,
}) })
}, },
Token::Function(ref name) if name.eq_ignore_ascii_case("calc") => { Token::Function(ref name) => {
let result = input.parse_nested_block(|i| CalcNode::parse_number(context, i))?; let function = CalcNode::math_function(name, location)?;
let result = CalcNode::parse_number(context, input, function)?;
Ok(Number { Ok(Number {
value: result.min(f32::MAX).max(f32::MIN), value: result.min(f32::MAX).max(f32::MIN),
calc_clamping_mode: Some(clamping_mode), calc_clamping_mode: Some(clamping_mode),
@ -543,8 +544,9 @@ impl Parse for Integer {
Token::Number { Token::Number {
int_value: Some(v), .. int_value: Some(v), ..
} => Ok(Integer::new(v)), } => Ok(Integer::new(v)),
Token::Function(ref name) if name.eq_ignore_ascii_case("calc") => { Token::Function(ref name) => {
let result = input.parse_nested_block(|i| CalcNode::parse_integer(context, i))?; let function = CalcNode::math_function(name, location)?;
let result = CalcNode::parse_integer(context, input, function)?;
Ok(Integer::from_calc(result)) Ok(Integer::from_calc(result))
}, },
ref t => Err(location.new_unexpected_token_error(t.clone())), ref t => Err(location.new_unexpected_token_error(t.clone())),
@ -559,16 +561,16 @@ impl Integer {
input: &mut Parser<'i, 't>, input: &mut Parser<'i, 't>,
min: i32, min: i32,
) -> Result<Integer, ParseError<'i>> { ) -> Result<Integer, ParseError<'i>> {
match Integer::parse(context, input) { let value = Integer::parse(context, input)?;
// FIXME(emilio): The spec asks us to avoid rejecting it at parse // FIXME(emilio): The spec asks us to avoid rejecting it at parse
// time except until computed value time. // time except until computed value time.
// //
// It's not totally clear it's worth it though, and no other browser // It's not totally clear it's worth it though, and no other browser
// does this. // does this.
Ok(value) if value.value() >= min => Ok(value), if value.value() < min {
Ok(_value) => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
Err(e) => Err(e),
} }
Ok(value)
} }
/// Parse a non-negative integer. /// Parse a non-negative integer.

View file

@ -117,14 +117,14 @@ impl Percentage {
{ {
Ok(Percentage::new(unit_value)) Ok(Percentage::new(unit_value))
}, },
Token::Function(ref name) if name.eq_ignore_ascii_case("calc") => { Token::Function(ref name) => {
let result = let function = CalcNode::math_function(name, location)?;
input.parse_nested_block(|i| CalcNode::parse_percentage(context, i))?; let value = CalcNode::parse_percentage(context, input, function)?;
// TODO(emilio): -moz-image-rect is the only thing that uses // TODO(emilio): -moz-image-rect is the only thing that uses
// the clamping mode... I guess we could disallow it... // the clamping mode... I guess we could disallow it...
Ok(Percentage { Ok(Percentage {
value: result, value,
calc_clamping_mode: Some(num_context), calc_clamping_mode: Some(num_context),
}) })
}, },

View file

@ -69,7 +69,7 @@ impl Time {
/// Returns a `Time` value from a CSS `calc()` expression. /// Returns a `Time` value from a CSS `calc()` expression.
pub fn from_calc(seconds: CSSFloat) -> Self { pub fn from_calc(seconds: CSSFloat) -> Self {
Time { Time {
seconds: seconds, seconds,
unit: TimeUnit::Second, unit: TimeUnit::Second,
was_calc: true, was_calc: true,
} }
@ -95,11 +95,20 @@ impl Time {
Time::parse_dimension(value, unit, /* from_calc = */ false) Time::parse_dimension(value, unit, /* from_calc = */ false)
.map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) .map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError))
}, },
Token::Function(ref name) if name.eq_ignore_ascii_case("calc") => { Token::Function(ref name) => {
match input.parse_nested_block(|i| CalcNode::parse_time(context, i)) { let function = CalcNode::math_function(name, location)?;
Ok(time) if clamping_mode.is_ok(ParsingMode::DEFAULT, time.seconds) => Ok(time), let time = CalcNode::parse_time(context, input, function)?;
_ => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
// FIXME(emilio): Rejecting calc() at parse time is wrong,
// was_calc should probably be replaced by calc_clamping_mode or
// something like we do for numbers, or we should do the
// clamping here instead (simpler, but technically incorrect,
// though still more correct than this!).
if !clamping_mode.is_ok(ParsingMode::DEFAULT, time.seconds) {
return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
} }
Ok(time)
}, },
ref t => return Err(location.new_unexpected_token_error(t.clone())), ref t => return Err(location.new_unexpected_token_error(t.clone())),
} }