diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
commit | 9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /third_party/rust/cssparser/src | |
parent | Initial commit. (diff) | |
download | thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.tar.xz thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/cssparser/src')
-rw-r--r-- | third_party/rust/cssparser/src/color.rs | 1592 | ||||
-rw-r--r-- | third_party/rust/cssparser/src/cow_rc_str.rs | 185 | ||||
-rw-r--r-- | third_party/rust/cssparser/src/from_bytes.rs | 64 | ||||
-rw-r--r-- | third_party/rust/cssparser/src/lib.rs | 113 | ||||
-rw-r--r-- | third_party/rust/cssparser/src/macros.rs | 185 | ||||
-rw-r--r-- | third_party/rust/cssparser/src/nth.rs | 145 | ||||
-rw-r--r-- | third_party/rust/cssparser/src/parser.rs | 1142 | ||||
-rw-r--r-- | third_party/rust/cssparser/src/rules_and_declarations.rs | 496 | ||||
-rw-r--r-- | third_party/rust/cssparser/src/serializer.rs | 526 | ||||
-rw-r--r-- | third_party/rust/cssparser/src/size_of_tests.rs | 52 | ||||
-rw-r--r-- | third_party/rust/cssparser/src/tests.rs | 1670 | ||||
-rw-r--r-- | third_party/rust/cssparser/src/tokenizer.rs | 1405 | ||||
-rw-r--r-- | third_party/rust/cssparser/src/unicode_range.rs | 181 |
13 files changed, 7756 insertions, 0 deletions
diff --git a/third_party/rust/cssparser/src/color.rs b/third_party/rust/cssparser/src/color.rs new file mode 100644 index 0000000000..f4484180ef --- /dev/null +++ b/third_party/rust/cssparser/src/color.rs @@ -0,0 +1,1592 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +// Allow text like <color> in docs. +#![allow(rustdoc::invalid_html_tags)] + +use std::f32::consts::PI; +use std::fmt; +use std::str::FromStr; + +use super::{ParseError, Parser, ToCss, Token}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +const OPAQUE: f32 = 1.0; + +fn serialize_none_or<T>(dest: &mut impl fmt::Write, value: &Option<T>) -> fmt::Result +where + T: ToCss, +{ + match value { + Some(v) => v.to_css(dest), + None => dest.write_str("none"), + } +} + +/// <https://drafts.csswg.org/css-color-4/#serializing-alpha-values> +#[inline] +fn serialize_alpha( + dest: &mut impl fmt::Write, + alpha: Option<f32>, + legacy_syntax: bool, +) -> fmt::Result { + let alpha = match alpha { + None => return dest.write_str(" / none"), + Some(a) => a, + }; + + // If the alpha component is full opaque, don't emit the alpha value in CSS. + if alpha == OPAQUE { + return Ok(()); + } + + dest.write_str(if legacy_syntax { ", " } else { " / " })?; + + // Try first with two decimal places, then with three. + let mut rounded_alpha = (alpha * 100.).round() / 100.; + if clamp_unit_f32(rounded_alpha) != clamp_unit_f32(alpha) { + rounded_alpha = (alpha * 1000.).round() / 1000.; + } + + rounded_alpha.to_css(dest) +} + +// Guaratees hue in [0..360) +fn normalize_hue(hue: f32) -> f32 { + // <https://drafts.csswg.org/css-values/#angles> + // Subtract an integer before rounding, to avoid some rounding errors: + hue - 360.0 * (hue / 360.0).floor() +} + +/// A color with red, green, blue, and alpha components, in a byte each. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct RGBA { + /// The red component. + pub red: Option<u8>, + /// The green component. + pub green: Option<u8>, + /// The blue component. + pub blue: Option<u8>, + /// The alpha component. + pub alpha: Option<f32>, +} + +impl RGBA { + /// Constructs a new RGBA value from float components. It expects the red, + /// green, blue and alpha channels in that order, and all values will be + /// clamped to the 0.0 ... 1.0 range. + #[inline] + pub fn from_floats( + red: Option<f32>, + green: Option<f32>, + blue: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Self::new( + red.map(clamp_unit_f32), + green.map(clamp_unit_f32), + blue.map(clamp_unit_f32), + alpha.map(|a| a.clamp(0.0, OPAQUE)), + ) + } + + /// Same thing, but with `u8` values instead of floats in the 0 to 1 range. + #[inline] + pub const fn new( + red: Option<u8>, + green: Option<u8>, + blue: Option<u8>, + alpha: Option<f32>, + ) -> Self { + Self { + red, + green, + blue, + alpha, + } + } +} + +#[cfg(feature = "serde")] +impl Serialize for RGBA { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + (self.red, self.green, self.blue, self.alpha).serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for RGBA { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let (r, g, b, a) = Deserialize::deserialize(deserializer)?; + Ok(RGBA::new(r, g, b, a)) + } +} + +impl ToCss for RGBA { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + let has_alpha = self.alpha.unwrap_or(0.0) != OPAQUE; + + dest.write_str(if has_alpha { "rgba(" } else { "rgb(" })?; + self.red.unwrap_or(0).to_css(dest)?; + dest.write_str(", ")?; + self.green.unwrap_or(0).to_css(dest)?; + dest.write_str(", ")?; + self.blue.unwrap_or(0).to_css(dest)?; + + // Legacy syntax does not allow none components. + serialize_alpha(dest, Some(self.alpha.unwrap_or(0.0)), true)?; + + dest.write_char(')') + } +} + +/// Color specified by hue, saturation and lightness components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Hsl { + /// The hue component. + pub hue: Option<f32>, + /// The saturation component. + pub saturation: Option<f32>, + /// The lightness component. + pub lightness: Option<f32>, + /// The alpha component. + pub alpha: Option<f32>, +} + +impl Hsl { + /// Construct a new HSL color from it's components. + pub fn new( + hue: Option<f32>, + saturation: Option<f32>, + lightness: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Self { + hue, + saturation, + lightness, + alpha, + } + } +} + +impl ToCss for Hsl { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + // HSL serializes to RGB, so we have to convert it. + let (red, green, blue) = hsl_to_rgb( + self.hue.unwrap_or(0.0) / 360.0, + self.saturation.unwrap_or(0.0), + self.lightness.unwrap_or(0.0), + ); + + RGBA::from_floats(Some(red), Some(green), Some(blue), self.alpha).to_css(dest) + } +} + +#[cfg(feature = "serde")] +impl Serialize for Hsl { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + (self.hue, self.saturation, self.lightness, self.alpha).serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Hsl { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let (lightness, a, b, alpha) = Deserialize::deserialize(deserializer)?; + Ok(Self::new(lightness, a, b, alpha)) + } +} + +/// Color specified by hue, whiteness and blackness components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Hwb { + /// The hue component. + pub hue: Option<f32>, + /// The whiteness component. + pub whiteness: Option<f32>, + /// The blackness component. + pub blackness: Option<f32>, + /// The alpha component. + pub alpha: Option<f32>, +} + +impl Hwb { + /// Construct a new HWB color from it's components. + pub fn new( + hue: Option<f32>, + whiteness: Option<f32>, + blackness: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Self { + hue, + whiteness, + blackness, + alpha, + } + } +} + +impl ToCss for Hwb { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + // HWB serializes to RGB, so we have to convert it. + let (red, green, blue) = hwb_to_rgb( + self.hue.unwrap_or(0.0) / 360.0, + self.whiteness.unwrap_or(0.0), + self.blackness.unwrap_or(0.0), + ); + + RGBA::from_floats(Some(red), Some(green), Some(blue), self.alpha).to_css(dest) + } +} + +#[cfg(feature = "serde")] +impl Serialize for Hwb { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + (self.hue, self.whiteness, self.blackness, self.alpha).serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Hwb { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let (lightness, whiteness, blackness, alpha) = Deserialize::deserialize(deserializer)?; + Ok(Self::new(lightness, whiteness, blackness, alpha)) + } +} + +// NOTE: LAB and OKLAB is not declared inside the [impl_lab_like] macro, +// because it causes cbindgen to ignore them. + +/// Color specified by lightness, a- and b-axis components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Lab { + /// The lightness component. + pub lightness: Option<f32>, + /// The a-axis component. + pub a: Option<f32>, + /// The b-axis component. + pub b: Option<f32>, + /// The alpha component. + pub alpha: Option<f32>, +} + +/// Color specified by lightness, a- and b-axis components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Oklab { + /// The lightness component. + pub lightness: Option<f32>, + /// The a-axis component. + pub a: Option<f32>, + /// The b-axis component. + pub b: Option<f32>, + /// The alpha component. + pub alpha: Option<f32>, +} + +macro_rules! impl_lab_like { + ($cls:ident, $fname:literal) => { + impl $cls { + /// Construct a new Lab color format with lightness, a, b and alpha components. + pub fn new( + lightness: Option<f32>, + a: Option<f32>, + b: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Self { + lightness, + a, + b, + alpha, + } + } + } + + #[cfg(feature = "serde")] + impl Serialize for $cls { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + (self.lightness, self.a, self.b, self.alpha).serialize(serializer) + } + } + + #[cfg(feature = "serde")] + impl<'de> Deserialize<'de> for $cls { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let (lightness, a, b, alpha) = Deserialize::deserialize(deserializer)?; + Ok(Self::new(lightness, a, b, alpha)) + } + } + + impl ToCss for $cls { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + dest.write_str($fname)?; + dest.write_str("(")?; + serialize_none_or(dest, &self.lightness)?; + dest.write_char(' ')?; + serialize_none_or(dest, &self.a)?; + dest.write_char(' ')?; + serialize_none_or(dest, &self.b)?; + serialize_alpha(dest, self.alpha, false)?; + dest.write_char(')') + } + } + }; +} + +impl_lab_like!(Lab, "lab"); +impl_lab_like!(Oklab, "oklab"); + +// NOTE: LCH and OKLCH is not declared inside the [impl_lch_like] macro, +// because it causes cbindgen to ignore them. + +/// Color specified by lightness, chroma and hue components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Lch { + /// The lightness component. + pub lightness: Option<f32>, + /// The chroma component. + pub chroma: Option<f32>, + /// The hue component. + pub hue: Option<f32>, + /// The alpha component. + pub alpha: Option<f32>, +} + +/// Color specified by lightness, chroma and hue components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Oklch { + /// The lightness component. + pub lightness: Option<f32>, + /// The chroma component. + pub chroma: Option<f32>, + /// The hue component. + pub hue: Option<f32>, + /// The alpha component. + pub alpha: Option<f32>, +} + +macro_rules! impl_lch_like { + ($cls:ident, $fname:literal) => { + impl $cls { + /// Construct a new color with lightness, chroma and hue components. + pub fn new( + lightness: Option<f32>, + chroma: Option<f32>, + hue: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Self { + lightness, + chroma, + hue, + alpha, + } + } + } + + #[cfg(feature = "serde")] + impl Serialize for $cls { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + (self.lightness, self.chroma, self.hue, self.alpha).serialize(serializer) + } + } + + #[cfg(feature = "serde")] + impl<'de> Deserialize<'de> for $cls { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let (lightness, chroma, hue, alpha) = Deserialize::deserialize(deserializer)?; + Ok(Self::new(lightness, chroma, hue, alpha)) + } + } + + impl ToCss for $cls { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + dest.write_str($fname)?; + dest.write_str("(")?; + serialize_none_or(dest, &self.lightness)?; + dest.write_char(' ')?; + serialize_none_or(dest, &self.chroma)?; + dest.write_char(' ')?; + serialize_none_or(dest, &self.hue)?; + serialize_alpha(dest, self.alpha, false)?; + dest.write_char(')') + } + } + }; +} + +impl_lch_like!(Lch, "lch"); +impl_lch_like!(Oklch, "oklch"); + +/// A Predefined color space specified in: +/// <https://drafts.csswg.org/css-color-4/#predefined> +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum PredefinedColorSpace { + /// <https://drafts.csswg.org/css-color-4/#predefined-sRGB> + Srgb, + /// <https://drafts.csswg.org/css-color-4/#predefined-sRGB-linear> + SrgbLinear, + /// <https://drafts.csswg.org/css-color-4/#predefined-display-p3> + DisplayP3, + /// <https://drafts.csswg.org/css-color-4/#predefined-a98-rgb> + A98Rgb, + /// <https://drafts.csswg.org/css-color-4/#predefined-prophoto-rgb> + ProphotoRgb, + /// <https://drafts.csswg.org/css-color-4/#predefined-rec2020> + Rec2020, + /// <https://drafts.csswg.org/css-color-4/#predefined-xyz> + XyzD50, + /// <https://drafts.csswg.org/css-color-4/#predefined-xyz> + XyzD65, +} + +impl PredefinedColorSpace { + /// Returns the string value of the predefined color space. + pub fn as_str(&self) -> &str { + match self { + PredefinedColorSpace::Srgb => "srgb", + PredefinedColorSpace::SrgbLinear => "srgb-linear", + PredefinedColorSpace::DisplayP3 => "display-p3", + PredefinedColorSpace::A98Rgb => "a98-rgb", + PredefinedColorSpace::ProphotoRgb => "prophoto-rgb", + PredefinedColorSpace::Rec2020 => "rec2020", + PredefinedColorSpace::XyzD50 => "xyz-d50", + PredefinedColorSpace::XyzD65 => "xyz-d65", + } + } +} + +impl FromStr for PredefinedColorSpace { + type Err = (); + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Ok(match_ignore_ascii_case! { s, + "srgb" => PredefinedColorSpace::Srgb, + "srgb-linear" => PredefinedColorSpace::SrgbLinear, + "display-p3" => PredefinedColorSpace::DisplayP3, + "a98-rgb" => PredefinedColorSpace::A98Rgb, + "prophoto-rgb" => PredefinedColorSpace::ProphotoRgb, + "rec2020" => PredefinedColorSpace::Rec2020, + "xyz-d50" => PredefinedColorSpace::XyzD50, + "xyz" | "xyz-d65" => PredefinedColorSpace::XyzD65, + + _ => return Err(()), + }) + } +} + +impl ToCss for PredefinedColorSpace { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + dest.write_str(self.as_str()) + } +} + +/// A color specified by the color() function. +/// <https://drafts.csswg.org/css-color-4/#color-function> +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct ColorFunction { + /// The color space for this color. + pub color_space: PredefinedColorSpace, + /// The first component of the color. Either red or x. + pub c1: Option<f32>, + /// The second component of the color. Either green or y. + pub c2: Option<f32>, + /// The third component of the color. Either blue or z. + pub c3: Option<f32>, + /// The alpha component of the color. + pub alpha: Option<f32>, +} + +impl ColorFunction { + /// Construct a new color function definition with the given color space and + /// color components. + pub fn new( + color_space: PredefinedColorSpace, + c1: Option<f32>, + c2: Option<f32>, + c3: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Self { + color_space, + c1, + c2, + c3, + alpha, + } + } +} + +impl ToCss for ColorFunction { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + dest.write_str("color(")?; + self.color_space.to_css(dest)?; + dest.write_char(' ')?; + serialize_none_or(dest, &self.c1)?; + dest.write_char(' ')?; + serialize_none_or(dest, &self.c2)?; + dest.write_char(' ')?; + serialize_none_or(dest, &self.c3)?; + + serialize_alpha(dest, self.alpha, false)?; + + dest.write_char(')') + } +} + +/// Describes one of the value <color> values according to the CSS +/// specification. +/// +/// Most components are `Option<_>`, so when the value is `None`, that component +/// serializes to the "none" keyword. +/// +/// <https://drafts.csswg.org/css-color-4/#color-type> +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum Color { + /// The 'currentcolor' keyword. + CurrentColor, + /// Specify sRGB colors directly by their red/green/blue/alpha chanels. + Rgba(RGBA), + /// Specifies a color in sRGB using hue, saturation and lightness components. + Hsl(Hsl), + /// Specifies a color in sRGB using hue, whiteness and blackness components. + Hwb(Hwb), + /// Specifies a CIELAB color by CIE Lightness and its a- and b-axis hue + /// coordinates (red/green-ness, and yellow/blue-ness) using the CIE LAB + /// rectangular coordinate model. + Lab(Lab), + /// Specifies a CIELAB color by CIE Lightness, Chroma, and hue using the + /// CIE LCH cylindrical coordinate model. + Lch(Lch), + /// Specifies an Oklab color by Oklab Lightness and its a- and b-axis hue + /// coordinates (red/green-ness, and yellow/blue-ness) using the Oklab + /// rectangular coordinate model. + Oklab(Oklab), + /// Specifies an Oklab color by Oklab Lightness, Chroma, and hue using + /// the OKLCH cylindrical coordinate model. + Oklch(Oklch), + /// Specifies a color in a predefined color space. + ColorFunction(ColorFunction), +} + +impl ToCss for Color { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + match *self { + Color::CurrentColor => dest.write_str("currentcolor"), + Color::Rgba(rgba) => rgba.to_css(dest), + Color::Hsl(hsl) => hsl.to_css(dest), + Color::Hwb(hwb) => hwb.to_css(dest), + Color::Lab(lab) => lab.to_css(dest), + Color::Lch(lch) => lch.to_css(dest), + Color::Oklab(lab) => lab.to_css(dest), + Color::Oklch(lch) => lch.to_css(dest), + Color::ColorFunction(color_function) => color_function.to_css(dest), + } + } +} + +/// Either a number or a percentage. +pub enum NumberOrPercentage { + /// `<number>`. + Number { + /// The numeric value parsed, as a float. + value: f32, + }, + /// `<percentage>` + Percentage { + /// The value as a float, divided by 100 so that the nominal range is + /// 0.0 to 1.0. + unit_value: f32, + }, +} + +impl NumberOrPercentage { + fn unit_value(&self) -> f32 { + match *self { + NumberOrPercentage::Number { value } => value, + NumberOrPercentage::Percentage { unit_value } => unit_value, + } + } + + fn value(&self, percentage_basis: f32) -> f32 { + match *self { + Self::Number { value } => value, + Self::Percentage { unit_value } => unit_value * percentage_basis, + } + } +} + +/// Either an angle or a number. +pub enum AngleOrNumber { + /// `<number>`. + Number { + /// The numeric value parsed, as a float. + value: f32, + }, + /// `<angle>` + Angle { + /// The value as a number of degrees. + degrees: f32, + }, +} + +impl AngleOrNumber { + fn degrees(&self) -> f32 { + match *self { + AngleOrNumber::Number { value } => value, + AngleOrNumber::Angle { degrees } => degrees, + } + } +} + +/// A trait that can be used to hook into how `cssparser` parses color +/// components, with the intention of implementing more complicated behavior. +/// +/// For example, this is used by Servo to support calc() in color. +pub trait ColorParser<'i> { + /// The type that the parser will construct on a successful parse. + type Output: FromParsedColor; + + /// A custom error type that can be returned from the parsing functions. + type Error: 'i; + + /// Parse an `<angle>` or `<number>`. + /// + /// Returns the result in degrees. + fn parse_angle_or_number<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result<AngleOrNumber, ParseError<'i, Self::Error>> { + let location = input.current_source_location(); + Ok(match *input.next()? { + Token::Number { value, .. } => AngleOrNumber::Number { value }, + Token::Dimension { + value: v, ref unit, .. + } => { + let degrees = match_ignore_ascii_case! { unit, + "deg" => v, + "grad" => v * 360. / 400., + "rad" => v * 360. / (2. * PI), + "turn" => v * 360., + _ => { + return Err(location.new_unexpected_token_error(Token::Ident(unit.clone()))) + } + }; + + AngleOrNumber::Angle { degrees } + } + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }) + } + + /// Parse a `<percentage>` value. + /// + /// Returns the result in a number from 0.0 to 1.0. + fn parse_percentage<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result<f32, ParseError<'i, Self::Error>> { + input.expect_percentage().map_err(From::from) + } + + /// Parse a `<number>` value. + fn parse_number<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result<f32, ParseError<'i, Self::Error>> { + input.expect_number().map_err(From::from) + } + + /// Parse a `<number>` value or a `<percentage>` value. + fn parse_number_or_percentage<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result<NumberOrPercentage, ParseError<'i, Self::Error>> { + let location = input.current_source_location(); + Ok(match *input.next()? { + Token::Number { value, .. } => NumberOrPercentage::Number { value }, + Token::Percentage { unit_value, .. } => NumberOrPercentage::Percentage { unit_value }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }) + } +} + +/// Default implementation of a [`ColorParser`] +pub struct DefaultColorParser; + +impl<'i> ColorParser<'i> for DefaultColorParser { + type Output = Color; + type Error = (); +} + +impl Color { + /// Parse a <color> value, per CSS Color Module Level 3. + /// + /// FIXME(#2) Deprecated CSS2 System Colors are not supported yet. + pub fn parse<'i>(input: &mut Parser<'i, '_>) -> Result<Color, ParseError<'i, ()>> { + parse_color_with(&DefaultColorParser, input) + } +} + +/// This trait is used by the [`ColorParser`] to construct colors of any type. +pub trait FromParsedColor { + /// Construct a new color from the CSS `currentcolor` keyword. + fn from_current_color() -> Self; + + /// Construct a new color from red, green, blue and alpha components. + fn from_rgba(red: Option<u8>, green: Option<u8>, blue: Option<u8>, alpha: Option<f32>) -> Self; + + /// Construct a new color from hue, saturation, lightness and alpha components. + fn from_hsl( + hue: Option<f32>, + saturation: Option<f32>, + lightness: Option<f32>, + alpha: Option<f32>, + ) -> Self; + + /// Construct a new color from hue, blackness, whiteness and alpha components. + fn from_hwb( + hue: Option<f32>, + whiteness: Option<f32>, + blackness: Option<f32>, + alpha: Option<f32>, + ) -> Self; + + /// Construct a new color from the `lab` notation. + fn from_lab(lightness: Option<f32>, a: Option<f32>, b: Option<f32>, alpha: Option<f32>) + -> Self; + + /// Construct a new color from the `lch` notation. + fn from_lch( + lightness: Option<f32>, + chroma: Option<f32>, + hue: Option<f32>, + alpha: Option<f32>, + ) -> Self; + + /// Construct a new color from the `oklab` notation. + fn from_oklab( + lightness: Option<f32>, + a: Option<f32>, + b: Option<f32>, + alpha: Option<f32>, + ) -> Self; + + /// Construct a new color from the `oklch` notation. + fn from_oklch( + lightness: Option<f32>, + chroma: Option<f32>, + hue: Option<f32>, + alpha: Option<f32>, + ) -> Self; + + /// Construct a new color with a predefined color space. + fn from_color_function( + color_space: PredefinedColorSpace, + c1: Option<f32>, + c2: Option<f32>, + c3: Option<f32>, + alpha: Option<f32>, + ) -> Self; +} + +/// Parse a color hash, without the leading '#' character. +#[inline] +pub fn parse_hash_color<'i, O>(value: &[u8]) -> Result<O, ()> +where + O: FromParsedColor, +{ + Ok(match value.len() { + 8 => O::from_rgba( + Some(from_hex(value[0])? * 16 + from_hex(value[1])?), + Some(from_hex(value[2])? * 16 + from_hex(value[3])?), + Some(from_hex(value[4])? * 16 + from_hex(value[5])?), + Some((from_hex(value[6])? * 16 + from_hex(value[7])?) as f32 / 255.0), + ), + 6 => O::from_rgba( + Some(from_hex(value[0])? * 16 + from_hex(value[1])?), + Some(from_hex(value[2])? * 16 + from_hex(value[3])?), + Some(from_hex(value[4])? * 16 + from_hex(value[5])?), + Some(OPAQUE), + ), + 4 => O::from_rgba( + Some(from_hex(value[0])? * 17), + Some(from_hex(value[1])? * 17), + Some(from_hex(value[2])? * 17), + Some((from_hex(value[3])? * 17) as f32 / 255.0), + ), + 3 => O::from_rgba( + Some(from_hex(value[0])? * 17), + Some(from_hex(value[1])? * 17), + Some(from_hex(value[2])? * 17), + Some(OPAQUE), + ), + _ => return Err(()), + }) +} + +/// Parse a CSS color using the specified [`ColorParser`] and return a new color +/// value on success. +pub fn parse_color_with<'i, 't, P>( + color_parser: &P, + input: &mut Parser<'i, 't>, +) -> Result<P::Output, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + let location = input.current_source_location(); + let token = input.next()?; + match *token { + Token::Hash(ref value) | Token::IDHash(ref value) => parse_hash_color(value.as_bytes()), + Token::Ident(ref value) => parse_color_keyword(value), + Token::Function(ref name) => { + let name = name.clone(); + return input.parse_nested_block(|arguments| { + parse_color_function(color_parser, &name, arguments) + }); + } + _ => Err(()), + } + .map_err(|()| location.new_unexpected_token_error(token.clone())) +} + +impl FromParsedColor for Color { + #[inline] + fn from_current_color() -> Self { + Color::CurrentColor + } + + #[inline] + fn from_rgba(red: Option<u8>, green: Option<u8>, blue: Option<u8>, alpha: Option<f32>) -> Self { + Color::Rgba(RGBA::new(red, green, blue, alpha)) + } + + fn from_hsl( + hue: Option<f32>, + saturation: Option<f32>, + lightness: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Color::Hsl(Hsl::new(hue, saturation, lightness, alpha)) + } + + fn from_hwb( + hue: Option<f32>, + blackness: Option<f32>, + whiteness: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Color::Hwb(Hwb::new(hue, blackness, whiteness, alpha)) + } + + #[inline] + fn from_lab( + lightness: Option<f32>, + a: Option<f32>, + b: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Color::Lab(Lab::new(lightness, a, b, alpha)) + } + + #[inline] + fn from_lch( + lightness: Option<f32>, + chroma: Option<f32>, + hue: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Color::Lch(Lch::new(lightness, chroma, hue, alpha)) + } + + #[inline] + fn from_oklab( + lightness: Option<f32>, + a: Option<f32>, + b: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Color::Oklab(Oklab::new(lightness, a, b, alpha)) + } + + #[inline] + fn from_oklch( + lightness: Option<f32>, + chroma: Option<f32>, + hue: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Color::Oklch(Oklch::new(lightness, chroma, hue, alpha)) + } + + #[inline] + fn from_color_function( + color_space: PredefinedColorSpace, + c1: Option<f32>, + c2: Option<f32>, + c3: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Color::ColorFunction(ColorFunction::new(color_space, c1, c2, c3, alpha)) + } +} + +/// Return the named color with the given name. +/// +/// Matching is case-insensitive in the ASCII range. +/// CSS escaping (if relevant) should be resolved before calling this function. +/// (For example, the value of an `Ident` token is fine.) +#[inline] +pub fn parse_color_keyword<Output>(ident: &str) -> Result<Output, ()> +where + Output: FromParsedColor, +{ + ascii_case_insensitive_phf_map! { + keyword -> (u8, u8, u8) = { + "black" => (0, 0, 0), + "silver" => (192, 192, 192), + "gray" => (128, 128, 128), + "white" => (255, 255, 255), + "maroon" => (128, 0, 0), + "red" => (255, 0, 0), + "purple" => (128, 0, 128), + "fuchsia" => (255, 0, 255), + "green" => (0, 128, 0), + "lime" => (0, 255, 0), + "olive" => (128, 128, 0), + "yellow" => (255, 255, 0), + "navy" => (0, 0, 128), + "blue" => (0, 0, 255), + "teal" => (0, 128, 128), + "aqua" => (0, 255, 255), + + "aliceblue" => (240, 248, 255), + "antiquewhite" => (250, 235, 215), + "aquamarine" => (127, 255, 212), + "azure" => (240, 255, 255), + "beige" => (245, 245, 220), + "bisque" => (255, 228, 196), + "blanchedalmond" => (255, 235, 205), + "blueviolet" => (138, 43, 226), + "brown" => (165, 42, 42), + "burlywood" => (222, 184, 135), + "cadetblue" => (95, 158, 160), + "chartreuse" => (127, 255, 0), + "chocolate" => (210, 105, 30), + "coral" => (255, 127, 80), + "cornflowerblue" => (100, 149, 237), + "cornsilk" => (255, 248, 220), + "crimson" => (220, 20, 60), + "cyan" => (0, 255, 255), + "darkblue" => (0, 0, 139), + "darkcyan" => (0, 139, 139), + "darkgoldenrod" => (184, 134, 11), + "darkgray" => (169, 169, 169), + "darkgreen" => (0, 100, 0), + "darkgrey" => (169, 169, 169), + "darkkhaki" => (189, 183, 107), + "darkmagenta" => (139, 0, 139), + "darkolivegreen" => (85, 107, 47), + "darkorange" => (255, 140, 0), + "darkorchid" => (153, 50, 204), + "darkred" => (139, 0, 0), + "darksalmon" => (233, 150, 122), + "darkseagreen" => (143, 188, 143), + "darkslateblue" => (72, 61, 139), + "darkslategray" => (47, 79, 79), + "darkslategrey" => (47, 79, 79), + "darkturquoise" => (0, 206, 209), + "darkviolet" => (148, 0, 211), + "deeppink" => (255, 20, 147), + "deepskyblue" => (0, 191, 255), + "dimgray" => (105, 105, 105), + "dimgrey" => (105, 105, 105), + "dodgerblue" => (30, 144, 255), + "firebrick" => (178, 34, 34), + "floralwhite" => (255, 250, 240), + "forestgreen" => (34, 139, 34), + "gainsboro" => (220, 220, 220), + "ghostwhite" => (248, 248, 255), + "gold" => (255, 215, 0), + "goldenrod" => (218, 165, 32), + "greenyellow" => (173, 255, 47), + "grey" => (128, 128, 128), + "honeydew" => (240, 255, 240), + "hotpink" => (255, 105, 180), + "indianred" => (205, 92, 92), + "indigo" => (75, 0, 130), + "ivory" => (255, 255, 240), + "khaki" => (240, 230, 140), + "lavender" => (230, 230, 250), + "lavenderblush" => (255, 240, 245), + "lawngreen" => (124, 252, 0), + "lemonchiffon" => (255, 250, 205), + "lightblue" => (173, 216, 230), + "lightcoral" => (240, 128, 128), + "lightcyan" => (224, 255, 255), + "lightgoldenrodyellow" => (250, 250, 210), + "lightgray" => (211, 211, 211), + "lightgreen" => (144, 238, 144), + "lightgrey" => (211, 211, 211), + "lightpink" => (255, 182, 193), + "lightsalmon" => (255, 160, 122), + "lightseagreen" => (32, 178, 170), + "lightskyblue" => (135, 206, 250), + "lightslategray" => (119, 136, 153), + "lightslategrey" => (119, 136, 153), + "lightsteelblue" => (176, 196, 222), + "lightyellow" => (255, 255, 224), + "limegreen" => (50, 205, 50), + "linen" => (250, 240, 230), + "magenta" => (255, 0, 255), + "mediumaquamarine" => (102, 205, 170), + "mediumblue" => (0, 0, 205), + "mediumorchid" => (186, 85, 211), + "mediumpurple" => (147, 112, 219), + "mediumseagreen" => (60, 179, 113), + "mediumslateblue" => (123, 104, 238), + "mediumspringgreen" => (0, 250, 154), + "mediumturquoise" => (72, 209, 204), + "mediumvioletred" => (199, 21, 133), + "midnightblue" => (25, 25, 112), + "mintcream" => (245, 255, 250), + "mistyrose" => (255, 228, 225), + "moccasin" => (255, 228, 181), + "navajowhite" => (255, 222, 173), + "oldlace" => (253, 245, 230), + "olivedrab" => (107, 142, 35), + "orange" => (255, 165, 0), + "orangered" => (255, 69, 0), + "orchid" => (218, 112, 214), + "palegoldenrod" => (238, 232, 170), + "palegreen" => (152, 251, 152), + "paleturquoise" => (175, 238, 238), + "palevioletred" => (219, 112, 147), + "papayawhip" => (255, 239, 213), + "peachpuff" => (255, 218, 185), + "peru" => (205, 133, 63), + "pink" => (255, 192, 203), + "plum" => (221, 160, 221), + "powderblue" => (176, 224, 230), + "rebeccapurple" => (102, 51, 153), + "rosybrown" => (188, 143, 143), + "royalblue" => (65, 105, 225), + "saddlebrown" => (139, 69, 19), + "salmon" => (250, 128, 114), + "sandybrown" => (244, 164, 96), + "seagreen" => (46, 139, 87), + "seashell" => (255, 245, 238), + "sienna" => (160, 82, 45), + "skyblue" => (135, 206, 235), + "slateblue" => (106, 90, 205), + "slategray" => (112, 128, 144), + "slategrey" => (112, 128, 144), + "snow" => (255, 250, 250), + "springgreen" => (0, 255, 127), + "steelblue" => (70, 130, 180), + "tan" => (210, 180, 140), + "thistle" => (216, 191, 216), + "tomato" => (255, 99, 71), + "turquoise" => (64, 224, 208), + "violet" => (238, 130, 238), + "wheat" => (245, 222, 179), + "whitesmoke" => (245, 245, 245), + "yellowgreen" => (154, 205, 50), + } + } + + match_ignore_ascii_case! { ident , + "transparent" => Ok(Output::from_rgba(Some(0), Some(0), Some(0), Some(0.0))), + "currentcolor" => Ok(Output::from_current_color()), + _ => keyword(ident) + .map(|(r, g, b)| Output::from_rgba(Some(*r), Some(*g), Some(*b), Some(1.0))) + .ok_or(()), + } +} + +#[inline] +fn from_hex(c: u8) -> Result<u8, ()> { + match c { + b'0'..=b'9' => Ok(c - b'0'), + b'a'..=b'f' => Ok(c - b'a' + 10), + b'A'..=b'F' => Ok(c - b'A' + 10), + _ => Err(()), + } +} + +fn clamp_unit_f32(val: f32) -> u8 { + // Whilst scaling by 256 and flooring would provide + // an equal distribution of integers to percentage inputs, + // this is not what Gecko does so we instead multiply by 255 + // and round (adding 0.5 and flooring is equivalent to rounding) + // + // Chrome does something similar for the alpha value, but not + // the rgb values. + // + // See <https://bugzilla.mozilla.org/show_bug.cgi?id=1340484> + // + // Clamping to 256 and rounding after would let 1.0 map to 256, and + // `256.0_f32 as u8` is undefined behavior: + // + // <https://github.com/rust-lang/rust/issues/10184> + clamp_floor_256_f32(val * 255.) +} + +fn clamp_floor_256_f32(val: f32) -> u8 { + val.round().clamp(0., 255.) as u8 +} + +fn parse_none_or<'i, 't, F, T, E>(input: &mut Parser<'i, 't>, thing: F) -> Result<Option<T>, E> +where + F: FnOnce(&mut Parser<'i, 't>) -> Result<T, E>, +{ + match input.try_parse(|p| p.expect_ident_matching("none")) { + Ok(_) => Ok(None), + Err(_) => Ok(Some(thing(input)?)), + } +} + +/// Parse one of the color functions: rgba(), lab(), color(), etc. +#[inline] +fn parse_color_function<'i, 't, P>( + color_parser: &P, + name: &str, + arguments: &mut Parser<'i, 't>, +) -> Result<P::Output, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + let color = match_ignore_ascii_case! { name, + "rgb" | "rgba" => parse_rgb(color_parser, arguments), + + "hsl" | "hsla" => parse_hsl(color_parser, arguments), + + "hwb" => parse_hwb(color_parser, arguments), + + // for L: 0% = 0.0, 100% = 100.0 + // for a and b: -100% = -125, 100% = 125 + "lab" => parse_lab_like(color_parser, arguments, 100.0, 125.0, P::Output::from_lab), + + // for L: 0% = 0.0, 100% = 100.0 + // for C: 0% = 0, 100% = 150 + "lch" => parse_lch_like(color_parser, arguments, 100.0, 150.0, P::Output::from_lch), + + // for L: 0% = 0.0, 100% = 1.0 + // for a and b: -100% = -0.4, 100% = 0.4 + "oklab" => parse_lab_like(color_parser, arguments, 1.0, 0.4, P::Output::from_oklab), + + // for L: 0% = 0.0, 100% = 1.0 + // for C: 0% = 0.0 100% = 0.4 + "oklch" => parse_lch_like(color_parser, arguments, 1.0, 0.4, P::Output::from_oklch), + + "color" => parse_color_with_color_space(color_parser, arguments), + + _ => return Err(arguments.new_unexpected_token_error(Token::Ident(name.to_owned().into()))), + }?; + + arguments.expect_exhausted()?; + + Ok(color) +} + +/// Parse the alpha component by itself from either number or percentage, +/// clipping the result to [0.0..1.0]. +#[inline] +fn parse_alpha_component<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result<f32, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + Ok(color_parser + .parse_number_or_percentage(arguments)? + .unit_value() + .clamp(0.0, OPAQUE)) +} + +fn parse_legacy_alpha<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result<f32, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + Ok(if !arguments.is_exhausted() { + arguments.expect_comma()?; + parse_alpha_component(color_parser, arguments)? + } else { + OPAQUE + }) +} + +fn parse_modern_alpha<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result<Option<f32>, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + if !arguments.is_exhausted() { + arguments.expect_delim('/')?; + parse_none_or(arguments, |p| parse_alpha_component(color_parser, p)) + } else { + Ok(Some(OPAQUE)) + } +} + +#[inline] +fn parse_rgb<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result<P::Output, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + let maybe_red = parse_none_or(arguments, |p| color_parser.parse_number_or_percentage(p))?; + + // If the first component is not "none" and is followed by a comma, then we + // are parsing the legacy syntax. + let is_legacy_syntax = maybe_red.is_some() && arguments.try_parse(|p| p.expect_comma()).is_ok(); + + let red: Option<u8>; + let green: Option<u8>; + let blue: Option<u8>; + + let alpha = if is_legacy_syntax { + match maybe_red.unwrap() { + NumberOrPercentage::Number { value } => { + red = Some(clamp_floor_256_f32(value)); + green = Some(clamp_floor_256_f32(color_parser.parse_number(arguments)?)); + arguments.expect_comma()?; + blue = Some(clamp_floor_256_f32(color_parser.parse_number(arguments)?)); + } + NumberOrPercentage::Percentage { unit_value } => { + red = Some(clamp_unit_f32(unit_value)); + green = Some(clamp_unit_f32(color_parser.parse_percentage(arguments)?)); + arguments.expect_comma()?; + blue = Some(clamp_unit_f32(color_parser.parse_percentage(arguments)?)); + } + } + + Some(parse_legacy_alpha(color_parser, arguments)?) + } else { + #[inline] + fn get_component_value(c: Option<NumberOrPercentage>) -> Option<u8> { + c.map(|c| match c { + NumberOrPercentage::Number { value } => clamp_floor_256_f32(value), + NumberOrPercentage::Percentage { unit_value } => clamp_unit_f32(unit_value), + }) + } + + red = get_component_value(maybe_red); + + green = get_component_value(parse_none_or(arguments, |p| { + color_parser.parse_number_or_percentage(p) + })?); + + blue = get_component_value(parse_none_or(arguments, |p| { + color_parser.parse_number_or_percentage(p) + })?); + + parse_modern_alpha(color_parser, arguments)? + }; + + Ok(P::Output::from_rgba(red, green, blue, alpha)) +} + +/// Parses hsl syntax. +/// +/// <https://drafts.csswg.org/css-color/#the-hsl-notation> +#[inline] +fn parse_hsl<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result<P::Output, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + let maybe_hue = parse_none_or(arguments, |p| color_parser.parse_angle_or_number(p))?; + + // If the hue is not "none" and is followed by a comma, then we are parsing + // the legacy syntax. + let is_legacy_syntax = maybe_hue.is_some() && arguments.try_parse(|p| p.expect_comma()).is_ok(); + + let saturation: Option<f32>; + let lightness: Option<f32>; + + let alpha = if is_legacy_syntax { + saturation = Some(color_parser.parse_percentage(arguments)?); + arguments.expect_comma()?; + lightness = Some(color_parser.parse_percentage(arguments)?); + Some(parse_legacy_alpha(color_parser, arguments)?) + } else { + saturation = parse_none_or(arguments, |p| color_parser.parse_percentage(p))?; + lightness = parse_none_or(arguments, |p| color_parser.parse_percentage(p))?; + + parse_modern_alpha(color_parser, arguments)? + }; + + let hue = maybe_hue.map(|h| normalize_hue(h.degrees())); + let saturation = saturation.map(|s| s.clamp(0.0, 1.0)); + let lightness = lightness.map(|l| l.clamp(0.0, 1.0)); + + Ok(P::Output::from_hsl(hue, saturation, lightness, alpha)) +} + +/// Parses hwb syntax. +/// +/// <https://drafts.csswg.org/css-color/#the-hbw-notation> +#[inline] +fn parse_hwb<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result<P::Output, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + let (hue, whiteness, blackness, alpha) = parse_components( + color_parser, + arguments, + P::parse_angle_or_number, + P::parse_percentage, + P::parse_percentage, + )?; + + let hue = hue.map(|h| normalize_hue(h.degrees())); + let whiteness = whiteness.map(|w| w.clamp(0.0, 1.0)); + let blackness = blackness.map(|b| b.clamp(0.0, 1.0)); + + Ok(P::Output::from_hwb(hue, whiteness, blackness, alpha)) +} + +/// <https://drafts.csswg.org/css-color-4/#hwb-to-rgb> +#[inline] +pub fn hwb_to_rgb(h: f32, w: f32, b: f32) -> (f32, f32, f32) { + if w + b >= 1.0 { + let gray = w / (w + b); + return (gray, gray, gray); + } + + // hue is expected in the range [0..1]. + let (mut red, mut green, mut blue) = hsl_to_rgb(h, 1.0, 0.5); + let x = 1.0 - w - b; + red = red * x + w; + green = green * x + w; + blue = blue * x + w; + (red, green, blue) +} + +/// <https://drafts.csswg.org/css-color/#hsl-color> +/// except with h pre-multiplied by 3, to avoid some rounding errors. +#[inline] +pub fn hsl_to_rgb(hue: f32, saturation: f32, lightness: f32) -> (f32, f32, f32) { + debug_assert!((0.0..=1.0).contains(&hue)); + + fn hue_to_rgb(m1: f32, m2: f32, mut h3: f32) -> f32 { + if h3 < 0. { + h3 += 3. + } + if h3 > 3. { + h3 -= 3. + } + if h3 * 2. < 1. { + m1 + (m2 - m1) * h3 * 2. + } else if h3 * 2. < 3. { + m2 + } else if h3 < 2. { + m1 + (m2 - m1) * (2. - h3) * 2. + } else { + m1 + } + } + let m2 = if lightness <= 0.5 { + lightness * (saturation + 1.) + } else { + lightness + saturation - lightness * saturation + }; + let m1 = lightness * 2. - m2; + let hue_times_3 = hue * 3.; + let red = hue_to_rgb(m1, m2, hue_times_3 + 1.); + let green = hue_to_rgb(m1, m2, hue_times_3); + let blue = hue_to_rgb(m1, m2, hue_times_3 - 1.); + (red, green, blue) +} + +type IntoColorFn<Output> = + fn(l: Option<f32>, a: Option<f32>, b: Option<f32>, alpha: Option<f32>) -> Output; + +#[inline] +fn parse_lab_like<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, + lightness_range: f32, + a_b_range: f32, + into_color: IntoColorFn<P::Output>, +) -> Result<P::Output, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + let (lightness, a, b, alpha) = parse_components( + color_parser, + arguments, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + )?; + + let lightness = lightness.map(|l| l.value(lightness_range).max(0.0)); + let a = a.map(|a| a.value(a_b_range)); + let b = b.map(|b| b.value(a_b_range)); + + Ok(into_color(lightness, a, b, alpha)) +} + +#[inline] +fn parse_lch_like<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, + lightness_range: f32, + chroma_range: f32, + into_color: IntoColorFn<P::Output>, +) -> Result<P::Output, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + let (lightness, chroma, hue, alpha) = parse_components( + color_parser, + arguments, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + P::parse_angle_or_number, + )?; + + let lightness = lightness.map(|l| l.value(lightness_range).max(0.0)); + let chroma = chroma.map(|c| c.value(chroma_range).max(0.0)); + let hue = hue.map(|h| normalize_hue(h.degrees())); + + Ok(into_color(lightness, chroma, hue, alpha)) +} + +/// Parse the color() function. +#[inline] +fn parse_color_with_color_space<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result<P::Output, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + let color_space = { + let location = arguments.current_source_location(); + + let ident = arguments.expect_ident()?; + PredefinedColorSpace::from_str(ident) + .map_err(|_| location.new_unexpected_token_error(Token::Ident(ident.clone())))? + }; + + let (c1, c2, c3, alpha) = parse_components( + color_parser, + arguments, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + )?; + + let c1 = c1.map(|c| c.unit_value()); + let c2 = c2.map(|c| c.unit_value()); + let c3 = c3.map(|c| c.unit_value()); + + Ok(P::Output::from_color_function( + color_space, + c1, + c2, + c3, + alpha, + )) +} + +type ComponentParseResult<'i, R1, R2, R3, Error> = + Result<(Option<R1>, Option<R2>, Option<R3>, Option<f32>), ParseError<'i, Error>>; + +/// Parse the color components and alpha with the modern [color-4] syntax. +pub fn parse_components<'i, 't, P, F1, F2, F3, R1, R2, R3>( + color_parser: &P, + input: &mut Parser<'i, 't>, + f1: F1, + f2: F2, + f3: F3, +) -> ComponentParseResult<'i, R1, R2, R3, P::Error> +where + P: ColorParser<'i>, + F1: FnOnce(&P, &mut Parser<'i, 't>) -> Result<R1, ParseError<'i, P::Error>>, + F2: FnOnce(&P, &mut Parser<'i, 't>) -> Result<R2, ParseError<'i, P::Error>>, + F3: FnOnce(&P, &mut Parser<'i, 't>) -> Result<R3, ParseError<'i, P::Error>>, +{ + let r1 = parse_none_or(input, |p| f1(color_parser, p))?; + let r2 = parse_none_or(input, |p| f2(color_parser, p))?; + let r3 = parse_none_or(input, |p| f3(color_parser, p))?; + + let alpha = parse_modern_alpha(color_parser, input)?; + + Ok((r1, r2, r3, alpha)) +} diff --git a/third_party/rust/cssparser/src/cow_rc_str.rs b/third_party/rust/cssparser/src/cow_rc_str.rs new file mode 100644 index 0000000000..ecf14a0a75 --- /dev/null +++ b/third_party/rust/cssparser/src/cow_rc_str.rs @@ -0,0 +1,185 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +use std::borrow::{Borrow, Cow}; +use std::rc::Rc; +use std::{cmp, fmt, hash, marker, mem, ops, slice, str, ptr}; + +/// A string that is either shared (heap-allocated and reference-counted) or borrowed. +/// +/// Equivalent to `enum { Borrowed(&'a str), Shared(Rc<String>) }`, but stored more compactly. +/// +/// * If `borrowed_len_or_max == usize::MAX`, then `ptr` represents `NonZero<*const String>` +/// from `Rc::into_raw`. +/// The lifetime parameter `'a` is irrelevant in this case. +/// +/// * Otherwise, `ptr` represents the `NonZero<*const u8>` data component of `&'a str`, +/// and `borrowed_len_or_max` its length. +pub struct CowRcStr<'a> { + ptr: ptr::NonNull<()>, + borrowed_len_or_max: usize, + + phantom: marker::PhantomData<Result<&'a str, Rc<String>>>, +} + +fn _static_assert_same_size<'a>() { + // "Instantiate" the generic function without calling it. + let _ = mem::transmute::<CowRcStr<'a>, Option<CowRcStr<'a>>>; +} + +impl<'a> From<Cow<'a, str>> for CowRcStr<'a> { + #[inline] + fn from(s: Cow<'a, str>) -> Self { + match s { + Cow::Borrowed(s) => CowRcStr::from(s), + Cow::Owned(s) => CowRcStr::from(s), + } + } +} + +impl<'a> From<&'a str> for CowRcStr<'a> { + #[inline] + fn from(s: &'a str) -> Self { + let len = s.len(); + assert!(len < usize::MAX); + CowRcStr { + ptr: unsafe { ptr::NonNull::new_unchecked(s.as_ptr() as *mut ()) }, + borrowed_len_or_max: len, + phantom: marker::PhantomData, + } + } +} + +impl<'a> From<String> for CowRcStr<'a> { + #[inline] + fn from(s: String) -> Self { + CowRcStr::from_rc(Rc::new(s)) + } +} + +impl<'a> CowRcStr<'a> { + #[inline] + fn from_rc(s: Rc<String>) -> Self { + let ptr = unsafe { ptr::NonNull::new_unchecked(Rc::into_raw(s) as *mut ()) }; + CowRcStr { + ptr, + borrowed_len_or_max: usize::MAX, + phantom: marker::PhantomData, + } + } + + #[inline] + fn unpack(&self) -> Result<&'a str, *const String> { + if self.borrowed_len_or_max == usize::MAX { + Err(self.ptr.as_ptr() as *const String) + } else { + unsafe { + Ok(str::from_utf8_unchecked(slice::from_raw_parts( + self.ptr.as_ptr() as *const u8, + self.borrowed_len_or_max, + ))) + } + } + } +} + +impl<'a> Clone for CowRcStr<'a> { + #[inline] + fn clone(&self) -> Self { + match self.unpack() { + Err(ptr) => { + let rc = unsafe { Rc::from_raw(ptr) }; + let new_rc = rc.clone(); + mem::forget(rc); // Don’t actually take ownership of this strong reference + CowRcStr::from_rc(new_rc) + } + Ok(_) => CowRcStr { ..*self }, + } + } +} + +impl<'a> Drop for CowRcStr<'a> { + #[inline] + fn drop(&mut self) { + if let Err(ptr) = self.unpack() { + mem::drop(unsafe { Rc::from_raw(ptr) }) + } + } +} + +impl<'a> ops::Deref for CowRcStr<'a> { + type Target = str; + + #[inline] + fn deref(&self) -> &str { + self.unpack().unwrap_or_else(|ptr| unsafe { &**ptr }) + } +} + +// Boilerplate / trivial impls below. + +impl<'a> AsRef<str> for CowRcStr<'a> { + #[inline] + fn as_ref(&self) -> &str { + self + } +} + +impl<'a> Borrow<str> for CowRcStr<'a> { + #[inline] + fn borrow(&self) -> &str { + self + } +} + +impl<'a> Default for CowRcStr<'a> { + #[inline] + fn default() -> Self { + Self::from("") + } +} + +impl<'a> hash::Hash for CowRcStr<'a> { + #[inline] + fn hash<H: hash::Hasher>(&self, hasher: &mut H) { + str::hash(self, hasher) + } +} + +impl<'a, T: AsRef<str>> PartialEq<T> for CowRcStr<'a> { + #[inline] + fn eq(&self, other: &T) -> bool { + str::eq(self, other.as_ref()) + } +} + +impl<'a, T: AsRef<str>> PartialOrd<T> for CowRcStr<'a> { + #[inline] + fn partial_cmp(&self, other: &T) -> Option<cmp::Ordering> { + str::partial_cmp(self, other.as_ref()) + } +} + +impl<'a> Eq for CowRcStr<'a> {} + +impl<'a> Ord for CowRcStr<'a> { + #[inline] + fn cmp(&self, other: &Self) -> cmp::Ordering { + str::cmp(self, other) + } +} + +impl<'a> fmt::Display for CowRcStr<'a> { + #[inline] + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + str::fmt(self, formatter) + } +} + +impl<'a> fmt::Debug for CowRcStr<'a> { + #[inline] + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + str::fmt(self, formatter) + } +} diff --git a/third_party/rust/cssparser/src/from_bytes.rs b/third_party/rust/cssparser/src/from_bytes.rs new file mode 100644 index 0000000000..78a56d3e14 --- /dev/null +++ b/third_party/rust/cssparser/src/from_bytes.rs @@ -0,0 +1,64 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +/// Abstraction for avoiding a dependency from cssparser to an encoding library +pub trait EncodingSupport { + /// One character encoding + type Encoding; + + /// https://encoding.spec.whatwg.org/#concept-encoding-get + fn from_label(ascii_label: &[u8]) -> Option<Self::Encoding>; + + /// Return the UTF-8 encoding + fn utf8() -> Self::Encoding; + + /// Whether the given encoding is UTF-16BE or UTF-16LE + fn is_utf16_be_or_le(encoding: &Self::Encoding) -> bool; +} + +/// Determine the character encoding of a CSS stylesheet. +/// +/// This is based on the presence of a BOM (Byte Order Mark), an `@charset` rule, and +/// encoding meta-information. +/// +/// * `css_bytes`: A byte string. +/// * `protocol_encoding`: The encoding label, if any, defined by HTTP or equivalent protocol. +/// (e.g. via the `charset` parameter of the `Content-Type` header.) +/// * `environment_encoding`: An optional `Encoding` object for the [environment encoding] +/// (https://drafts.csswg.org/css-syntax/#environment-encoding), if any. +/// +/// Returns the encoding to use. +pub fn stylesheet_encoding<E>( + css: &[u8], + protocol_encoding_label: Option<&[u8]>, + environment_encoding: Option<E::Encoding>, +) -> E::Encoding +where + E: EncodingSupport, +{ + // https://drafts.csswg.org/css-syntax/#the-input-byte-stream + if let Some(label) = protocol_encoding_label { + if let Some(protocol_encoding) = E::from_label(label) { + return protocol_encoding; + }; + }; + + let prefix = b"@charset \""; + if css.starts_with(prefix) { + let rest = &css[prefix.len()..]; + if let Some(label_length) = rest.iter().position(|&b| b == b'"') { + if rest[label_length..].starts_with(b"\";") { + let label = &rest[..label_length]; + if let Some(charset_encoding) = E::from_label(label) { + if E::is_utf16_be_or_le(&charset_encoding) { + return E::utf8(); + } else { + return charset_encoding; + } + } + } + } + } + environment_encoding.unwrap_or_else(E::utf8) +} diff --git a/third_party/rust/cssparser/src/lib.rs b/third_party/rust/cssparser/src/lib.rs new file mode 100644 index 0000000000..1bbcb55547 --- /dev/null +++ b/third_party/rust/cssparser/src/lib.rs @@ -0,0 +1,113 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#![crate_name = "cssparser"] +#![crate_type = "rlib"] +#![cfg_attr(feature = "bench", feature(test))] +#![deny(missing_docs)] + +/*! + +Implementation of [CSS Syntax Module Level 3](https://drafts.csswg.org/css-syntax/) for Rust. + +# Input + +Everything is based on `Parser` objects, which borrow a `&str` input. +If you have bytes (from a file, the network, or something) +and want to support character encodings other than UTF-8, +see the `stylesheet_encoding` function, +which can be used together with rust-encoding or encoding-rs. + +# Conventions for parsing functions + +* Take (at least) a `input: &mut cssparser::Parser` parameter +* Return `Result<_, ()>` +* When returning `Ok(_)`, + the function must have consumed exactly the amount of input that represents the parsed value. +* When returning `Err(())`, any amount of input may have been consumed. + +As a consequence, when calling another parsing function, either: + +* Any `Err(())` return value must be propagated. + This happens by definition for tail calls, + and can otherwise be done with the `?` operator. +* Or the call must be wrapped in a `Parser::try` call. + `try` takes a closure that takes a `Parser` and returns a `Result`, + calls it once, + and returns itself that same result. + If the result is `Err`, + it restores the position inside the input to the one saved before calling the closure. + +Examples: + +```{rust,ignore} +// 'none' | <image> +fn parse_background_image(context: &ParserContext, input: &mut Parser) + -> Result<Option<Image>, ()> { + if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { + Ok(None) + } else { + Image::parse(context, input).map(Some) // tail call + } +} +``` + +```{rust,ignore} +// [ <length> | <percentage> ] [ <length> | <percentage> ]? +fn parse_border_spacing(_context: &ParserContext, input: &mut Parser) + -> Result<(LengthOrPercentage, LengthOrPercentage), ()> { + let first = LengthOrPercentage::parse?; + let second = input.try_parse(LengthOrPercentage::parse).unwrap_or(first); + (first, second) +} +``` + +*/ + +#![recursion_limit = "200"] // For color::parse_color_keyword + +pub use crate::color::{ + hsl_to_rgb, hwb_to_rgb, parse_color_keyword, parse_color_with, parse_hash_color, AngleOrNumber, + Color, ColorFunction, ColorParser, FromParsedColor, Hsl, Hwb, Lab, Lch, NumberOrPercentage, + Oklab, Oklch, PredefinedColorSpace, RGBA, +}; +pub use crate::cow_rc_str::CowRcStr; +pub use crate::from_bytes::{stylesheet_encoding, EncodingSupport}; +#[doc(hidden)] +pub use crate::macros::{ + _cssparser_internal_create_uninit_array, _cssparser_internal_to_lowercase, +}; +pub use crate::nth::parse_nth; +pub use crate::parser::{BasicParseError, BasicParseErrorKind, ParseError, ParseErrorKind}; +pub use crate::parser::{Delimiter, Delimiters, Parser, ParserInput, ParserState}; +pub use crate::rules_and_declarations::{parse_important, parse_one_declaration}; +pub use crate::rules_and_declarations::{parse_one_rule, StyleSheetParser}; +pub use crate::rules_and_declarations::{AtRuleParser, QualifiedRuleParser}; +pub use crate::rules_and_declarations::{RuleBodyParser, RuleBodyItemParser, DeclarationParser}; +pub use crate::serializer::{serialize_identifier, serialize_name, serialize_string}; +pub use crate::serializer::{CssStringWriter, ToCss, TokenSerializationType}; +pub use crate::tokenizer::{SourceLocation, SourcePosition, Token}; +pub use crate::unicode_range::UnicodeRange; +pub use cssparser_macros::*; +#[doc(hidden)] +pub use phf as _cssparser_internal_phf; + +#[macro_use] +mod macros; + +mod rules_and_declarations; +mod tokenizer; + +mod color; +mod cow_rc_str; +mod from_bytes; +mod nth; +mod parser; +mod serializer; +mod unicode_range; + +#[cfg(test)] +mod size_of_tests; +#[cfg(test)] +mod tests; diff --git a/third_party/rust/cssparser/src/macros.rs b/third_party/rust/cssparser/src/macros.rs new file mode 100644 index 0000000000..981af01005 --- /dev/null +++ b/third_party/rust/cssparser/src/macros.rs @@ -0,0 +1,185 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +use std::mem::MaybeUninit; + +/// Expands to a `match` expression with string patterns, +/// matching case-insensitively in the ASCII range. +/// +/// The patterns must not contain ASCII upper case letters. (They must be already be lower-cased.) +/// +/// # Example +/// +/// ```rust +/// # fn main() {} // Make doctest not wrap everything in its own main +/// # fn dummy(function_name: &String) { let _ = +/// cssparser::match_ignore_ascii_case! { &function_name, +/// "rgb" => parse_rgb(..), +/// # #[cfg(not(something))] +/// "rgba" => parse_rgba(..), +/// "hsl" => parse_hsl(..), +/// "hsla" => parse_hsla(..), +/// _ => Err(format!("unknown function: {}", function_name)) +/// } +/// # ;} +/// # use std::ops::RangeFull; +/// # fn parse_rgb(_: RangeFull) -> Result<(), String> { Ok(()) } +/// # fn parse_rgba(_: RangeFull) -> Result<(), String> { Ok(()) } +/// # fn parse_hsl(_: RangeFull) -> Result<(), String> { Ok(()) } +/// # fn parse_hsla(_: RangeFull) -> Result<(), String> { Ok(()) } +/// ``` +#[macro_export] +macro_rules! match_ignore_ascii_case { + ( $input:expr, + $( + $( #[$meta: meta] )* + $( $pattern: pat )|+ $( if $guard: expr )? => $then: expr + ),+ + $(,)? + ) => { + { + // This dummy module works around the feature gate + // `error[E0658]: procedural macros cannot be expanded to statements` + // by forcing the macro to be in an item context + // rather than expression/statement context, + // even though the macro only expands to items. + mod cssparser_internal { + $crate::_cssparser_internal_max_len! { + $( $( $pattern )+ )+ + } + } + $crate::_cssparser_internal_to_lowercase!($input, cssparser_internal::MAX_LENGTH => lowercase); + // "A" is a short string that we know is different for every string pattern, + // since we’ve verified that none of them include ASCII upper case letters. + match lowercase.unwrap_or("A") { + $( + $( #[$meta] )* + $( $pattern )|+ $( if $guard )? => $then, + )+ + } + } + }; +} + +/// Define a function `$name(&str) -> Option<&'static $ValueType>` +/// +/// The function finds a match for the input string +/// in a [`phf` map](https://github.com/sfackler/rust-phf) +/// and returns a reference to the corresponding value. +/// Matching is case-insensitive in the ASCII range. +/// +/// ## Example: +/// +/// ```rust +/// # fn main() {} // Make doctest not wrap everything in its own main +/// +/// fn color_rgb(input: &str) -> Option<(u8, u8, u8)> { +/// cssparser::ascii_case_insensitive_phf_map! { +/// keyword -> (u8, u8, u8) = { +/// "red" => (255, 0, 0), +/// "green" => (0, 255, 0), +/// "blue" => (0, 0, 255), +/// } +/// } +/// keyword(input).cloned() +/// } +#[macro_export] +macro_rules! ascii_case_insensitive_phf_map { + ($name: ident -> $ValueType: ty = { $( $key: tt => $value: expr ),+ }) => { + ascii_case_insensitive_phf_map!($name -> $ValueType = { $( $key => $value, )+ }) + }; + ($name: ident -> $ValueType: ty = { $( $key: tt => $value: expr, )+ }) => { + pub fn $name(input: &str) -> Option<&'static $ValueType> { + // This dummy module works around a feature gate, + // see comment on the similar module in `match_ignore_ascii_case!` above. + mod _cssparser_internal { + $crate::_cssparser_internal_max_len! { + $( $key )+ + } + } + use $crate::_cssparser_internal_phf as phf; + static MAP: phf::Map<&'static str, $ValueType> = phf::phf_map! { + $( + $key => $value, + )* + }; + $crate::_cssparser_internal_to_lowercase!(input, _cssparser_internal::MAX_LENGTH => lowercase); + lowercase.and_then(|s| MAP.get(s)) + } + } +} + +/// Create a new array of MaybeUninit<T> items, in an uninitialized state. +#[inline(always)] +pub fn _cssparser_internal_create_uninit_array<const N: usize>() -> [MaybeUninit<u8>; N] { + unsafe { + // SAFETY: An uninitialized `[MaybeUninit<_>; LEN]` is valid. + // See: https://doc.rust-lang.org/stable/core/mem/union.MaybeUninit.html#method.uninit_array + MaybeUninit::<[MaybeUninit<u8>; N]>::uninit().assume_init() + } +} + +/// Implementation detail of match_ignore_ascii_case! and ascii_case_insensitive_phf_map! macros. +/// +/// **This macro is not part of the public API. It can change or be removed between any versions.** +/// +/// Define a local variable named `$output` +/// and assign it the result of calling `_cssparser_internal_to_lowercase` +/// with a stack-allocated buffer of length `$BUFFER_SIZE`. +#[macro_export] +#[doc(hidden)] +macro_rules! _cssparser_internal_to_lowercase { + ($input: expr, $BUFFER_SIZE: expr => $output: ident) => { + let mut buffer = $crate::_cssparser_internal_create_uninit_array::<{ $BUFFER_SIZE }>(); + let input: &str = $input; + let $output = $crate::_cssparser_internal_to_lowercase(&mut buffer, input); + }; +} + +/// Implementation detail of match_ignore_ascii_case! and ascii_case_insensitive_phf_map! macros. +/// +/// **This function is not part of the public API. It can change or be removed between any versions.** +/// +/// If `input` is larger than buffer, return `None`. +/// Otherwise, return `input` ASCII-lowercased, using `buffer` as temporary space if necessary. +#[doc(hidden)] +#[allow(non_snake_case)] +#[inline] +pub fn _cssparser_internal_to_lowercase<'a>( + buffer: &'a mut [MaybeUninit<u8>], + input: &'a str, +) -> Option<&'a str> { + let buffer = buffer.get_mut(..input.len())?; + + #[cold] + fn make_ascii_lowercase<'a>( + buffer: &'a mut [MaybeUninit<u8>], + input: &'a str, + first_uppercase: usize, + ) -> &'a str { + // This cast doesn't change the pointer's validity + // since `u8` has the same layout as `MaybeUninit<u8>`: + let input_bytes = + unsafe { &*(input.as_bytes() as *const [u8] as *const [MaybeUninit<u8>]) }; + + buffer.copy_from_slice(&*input_bytes); + + // Same as above re layout, plus these bytes have been initialized: + let buffer = unsafe { &mut *(buffer as *mut [MaybeUninit<u8>] as *mut [u8]) }; + + buffer[first_uppercase..].make_ascii_lowercase(); + // `buffer` was initialized to a copy of `input` + // (which is `&str` so well-formed UTF-8) + // then ASCII-lowercased (which preserves UTF-8 well-formedness): + unsafe { ::std::str::from_utf8_unchecked(buffer) } + } + + Some( + match input.bytes().position(|byte| matches!(byte, b'A'..=b'Z')) { + Some(first_uppercase) => make_ascii_lowercase(buffer, input, first_uppercase), + // common case: input is already lower-case + None => input, + }, + ) +} diff --git a/third_party/rust/cssparser/src/nth.rs b/third_party/rust/cssparser/src/nth.rs new file mode 100644 index 0000000000..518de4d9b4 --- /dev/null +++ b/third_party/rust/cssparser/src/nth.rs @@ -0,0 +1,145 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +use super::{BasicParseError, Parser, ParserInput, Token}; + +/// Parse the *An+B* notation, as found in the `:nth-child()` selector. +/// The input is typically the arguments of a function, +/// in which case the caller needs to check if the arguments’ parser is exhausted. +/// Return `Ok((A, B))`, or `Err(())` for a syntax error. +pub fn parse_nth<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(i32, i32), BasicParseError<'i>> { + match *input.next()? { + Token::Number { + int_value: Some(b), .. + } => Ok((0, b)), + Token::Dimension { + int_value: Some(a), + ref unit, + .. + } => { + match_ignore_ascii_case! { + unit, + "n" => Ok(parse_b(input, a)?), + "n-" => Ok(parse_signless_b(input, a, -1)?), + _ => match parse_n_dash_digits(&*unit) { + Ok(b) => Ok((a, b)), + Err(()) => { + let unit = unit.clone(); + Err(input.new_basic_unexpected_token_error(Token::Ident(unit))) + } + } + } + } + Token::Ident(ref value) => { + match_ignore_ascii_case! { value, + "even" => Ok((2, 0)), + "odd" => Ok((2, 1)), + "n" => Ok(parse_b(input, 1)?), + "-n" => Ok(parse_b(input, -1)?), + "n-" => Ok(parse_signless_b(input, 1, -1)?), + "-n-" => Ok(parse_signless_b(input, -1, -1)?), + _ => { + let (slice, a) = if value.starts_with("-") { + (&value[1..], -1) + } else { + (&**value, 1) + }; + match parse_n_dash_digits(slice) { + Ok(b) => Ok((a, b)), + Err(()) => { + let value = value.clone(); + Err(input.new_basic_unexpected_token_error(Token::Ident(value))) + } + } + } + } + } + Token::Delim('+') => match *input.next_including_whitespace()? { + Token::Ident(ref value) => { + match_ignore_ascii_case! { value, + "n" => parse_b(input, 1), + "n-" => parse_signless_b(input, 1, -1), + _ => match parse_n_dash_digits(value) { + Ok(b) => Ok((1, b)), + Err(()) => { + let value = value.clone(); + Err(input.new_basic_unexpected_token_error(Token::Ident(value))) + } + } + } + } + ref token => { + let token = token.clone(); + Err(input.new_basic_unexpected_token_error(token)) + } + }, + ref token => { + let token = token.clone(); + Err(input.new_basic_unexpected_token_error(token)) + } + } +} + +fn parse_b<'i, 't>(input: &mut Parser<'i, 't>, a: i32) -> Result<(i32, i32), BasicParseError<'i>> { + let start = input.state(); + match input.next() { + Ok(&Token::Delim('+')) => parse_signless_b(input, a, 1), + Ok(&Token::Delim('-')) => parse_signless_b(input, a, -1), + Ok(&Token::Number { + has_sign: true, + int_value: Some(b), + .. + }) => Ok((a, b)), + _ => { + input.reset(&start); + Ok((a, 0)) + } + } +} + +fn parse_signless_b<'i, 't>( + input: &mut Parser<'i, 't>, + a: i32, + b_sign: i32, +) -> Result<(i32, i32), BasicParseError<'i>> { + // FIXME: remove .clone() when lifetimes are non-lexical. + match input.next()?.clone() { + Token::Number { + has_sign: false, + int_value: Some(b), + .. + } => Ok((a, b_sign * b)), + token => Err(input.new_basic_unexpected_token_error(token)), + } +} + +fn parse_n_dash_digits(string: &str) -> Result<i32, ()> { + let bytes = string.as_bytes(); + if bytes.len() >= 3 + && bytes[..2].eq_ignore_ascii_case(b"n-") + && bytes[2..].iter().all(|&c| matches!(c, b'0'..=b'9')) + { + Ok(parse_number_saturate(&string[1..]).unwrap()) // Include the minus sign + } else { + Err(()) + } +} + +fn parse_number_saturate(string: &str) -> Result<i32, ()> { + let mut input = ParserInput::new(string); + let mut parser = Parser::new(&mut input); + let int = if let Ok(&Token::Number { + int_value: Some(int), + .. + }) = parser.next_including_whitespace_and_comments() + { + int + } else { + return Err(()); + }; + if !parser.is_exhausted() { + return Err(()); + } + Ok(int) +} diff --git a/third_party/rust/cssparser/src/parser.rs b/third_party/rust/cssparser/src/parser.rs new file mode 100644 index 0000000000..afc6281da6 --- /dev/null +++ b/third_party/rust/cssparser/src/parser.rs @@ -0,0 +1,1142 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +use crate::cow_rc_str::CowRcStr; +use crate::tokenizer::{SourceLocation, SourcePosition, Token, Tokenizer}; +use smallvec::SmallVec; +use std::fmt; +use std::ops::BitOr; +use std::ops::Range; + +/// A capture of the internal state of a `Parser` (including the position within the input), +/// obtained from the `Parser::position` method. +/// +/// Can be used with the `Parser::reset` method to restore that state. +/// Should only be used with the `Parser` instance it came from. +#[derive(Debug, Clone)] +pub struct ParserState { + pub(crate) position: usize, + pub(crate) current_line_start_position: usize, + pub(crate) current_line_number: u32, + pub(crate) at_start_of: Option<BlockType>, +} + +impl ParserState { + /// The position from the start of the input, counted in UTF-8 bytes. + #[inline] + pub fn position(&self) -> SourcePosition { + SourcePosition(self.position) + } + + /// The line number and column number + #[inline] + pub fn source_location(&self) -> SourceLocation { + SourceLocation { + line: self.current_line_number, + column: (self.position - self.current_line_start_position + 1) as u32, + } + } +} + +/// Details about a `BasicParseError` +#[derive(Clone, Debug, PartialEq)] +pub enum BasicParseErrorKind<'i> { + /// An unexpected token was encountered. + UnexpectedToken(Token<'i>), + /// The end of the input was encountered unexpectedly. + EndOfInput, + /// An `@` rule was encountered that was invalid. + AtRuleInvalid(CowRcStr<'i>), + /// The body of an '@' rule was invalid. + AtRuleBodyInvalid, + /// A qualified rule was encountered that was invalid. + QualifiedRuleInvalid, +} + +impl<'i> fmt::Display for BasicParseErrorKind<'i> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BasicParseErrorKind::UnexpectedToken(token) => { + write!(f, "unexpected token: {:?}", token) + } + BasicParseErrorKind::EndOfInput => write!(f, "unexpected end of input"), + BasicParseErrorKind::AtRuleInvalid(rule) => { + write!(f, "invalid @ rule encountered: '@{}'", rule) + } + BasicParseErrorKind::AtRuleBodyInvalid => write!(f, "invalid @ rule body encountered"), + BasicParseErrorKind::QualifiedRuleInvalid => { + write!(f, "invalid qualified rule encountered") + } + } + } +} + +/// The fundamental parsing errors that can be triggered by built-in parsing routines. +#[derive(Clone, Debug, PartialEq)] +pub struct BasicParseError<'i> { + /// Details of this error + pub kind: BasicParseErrorKind<'i>, + /// Location where this error occurred + pub location: SourceLocation, +} + +impl<'i, T> From<BasicParseError<'i>> for ParseError<'i, T> { + #[inline] + fn from(this: BasicParseError<'i>) -> ParseError<'i, T> { + ParseError { + kind: ParseErrorKind::Basic(this.kind), + location: this.location, + } + } +} + +impl SourceLocation { + /// Create a new BasicParseError at this location for an unexpected token + #[inline] + pub fn new_basic_unexpected_token_error<'i>(self, token: Token<'i>) -> BasicParseError<'i> { + BasicParseError { + kind: BasicParseErrorKind::UnexpectedToken(token), + location: self, + } + } + + /// Create a new ParseError at this location for an unexpected token + #[inline] + pub fn new_unexpected_token_error<'i, E>(self, token: Token<'i>) -> ParseError<'i, E> { + ParseError { + kind: ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(token)), + location: self, + } + } + + /// Create a new custom ParseError at this location + #[inline] + pub fn new_custom_error<'i, E1: Into<E2>, E2>(self, error: E1) -> ParseError<'i, E2> { + ParseError { + kind: ParseErrorKind::Custom(error.into()), + location: self, + } + } +} + +/// Details of a `ParseError` +#[derive(Clone, Debug, PartialEq)] +pub enum ParseErrorKind<'i, T: 'i> { + /// A fundamental parse error from a built-in parsing routine. + Basic(BasicParseErrorKind<'i>), + /// A parse error reported by downstream consumer code. + Custom(T), +} + +impl<'i, T> ParseErrorKind<'i, T> { + /// Like `std::convert::Into::into` + pub fn into<U>(self) -> ParseErrorKind<'i, U> + where + T: Into<U>, + { + match self { + ParseErrorKind::Basic(basic) => ParseErrorKind::Basic(basic), + ParseErrorKind::Custom(custom) => ParseErrorKind::Custom(custom.into()), + } + } +} + +impl<'i, E: fmt::Display> fmt::Display for ParseErrorKind<'i, E> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ParseErrorKind::Basic(ref basic) => basic.fmt(f), + ParseErrorKind::Custom(ref custom) => custom.fmt(f), + } + } +} + +/// Extensible parse errors that can be encountered by client parsing implementations. +#[derive(Clone, Debug, PartialEq)] +pub struct ParseError<'i, E> { + /// Details of this error + pub kind: ParseErrorKind<'i, E>, + /// Location where this error occurred + pub location: SourceLocation, +} + +impl<'i, T> ParseError<'i, T> { + /// Extract the fundamental parse error from an extensible error. + pub fn basic(self) -> BasicParseError<'i> { + match self.kind { + ParseErrorKind::Basic(kind) => BasicParseError { + kind, + location: self.location, + }, + ParseErrorKind::Custom(_) => panic!("Not a basic parse error"), + } + } + + /// Like `std::convert::Into::into` + pub fn into<U>(self) -> ParseError<'i, U> + where + T: Into<U>, + { + ParseError { + kind: self.kind.into(), + location: self.location, + } + } +} + +impl<'i, E: fmt::Display> fmt::Display for ParseError<'i, E> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.kind.fmt(f) + } +} + +impl<'i, E: fmt::Display + fmt::Debug> std::error::Error for ParseError<'i, E> {} + +/// The owned input for a parser. +pub struct ParserInput<'i> { + tokenizer: Tokenizer<'i>, + cached_token: Option<CachedToken<'i>>, +} + +struct CachedToken<'i> { + token: Token<'i>, + start_position: SourcePosition, + end_state: ParserState, +} + +impl<'i> ParserInput<'i> { + /// Create a new input for a parser. + pub fn new(input: &'i str) -> ParserInput<'i> { + ParserInput { + tokenizer: Tokenizer::new(input), + cached_token: None, + } + } + + /// Create a new input for a parser. Line numbers in locations + /// are offset by the given value. + pub fn new_with_line_number_offset(input: &'i str, first_line_number: u32) -> ParserInput<'i> { + ParserInput { + tokenizer: Tokenizer::with_first_line_number(input, first_line_number), + cached_token: None, + } + } + + #[inline] + fn cached_token_ref(&self) -> &Token<'i> { + &self.cached_token.as_ref().unwrap().token + } +} + +/// A CSS parser that borrows its `&str` input, +/// yields `Token`s, +/// and keeps track of nested blocks and functions. +pub struct Parser<'i, 't> { + input: &'t mut ParserInput<'i>, + /// If `Some(_)`, .parse_nested_block() can be called. + at_start_of: Option<BlockType>, + /// For parsers from `parse_until` or `parse_nested_block` + stop_before: Delimiters, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub(crate) enum BlockType { + Parenthesis, + SquareBracket, + CurlyBracket, +} + +impl BlockType { + fn opening(token: &Token) -> Option<BlockType> { + match *token { + Token::Function(_) | Token::ParenthesisBlock => Some(BlockType::Parenthesis), + Token::SquareBracketBlock => Some(BlockType::SquareBracket), + Token::CurlyBracketBlock => Some(BlockType::CurlyBracket), + _ => None, + } + } + + fn closing(token: &Token) -> Option<BlockType> { + match *token { + Token::CloseParenthesis => Some(BlockType::Parenthesis), + Token::CloseSquareBracket => Some(BlockType::SquareBracket), + Token::CloseCurlyBracket => Some(BlockType::CurlyBracket), + _ => None, + } + } +} + +/// A set of characters, to be used with the `Parser::parse_until*` methods. +/// +/// The union of two sets can be obtained with the `|` operator. Example: +/// +/// ```{rust,ignore} +/// input.parse_until_before(Delimiter::CurlyBracketBlock | Delimiter::Semicolon) +/// ``` +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub struct Delimiters { + bits: u8, +} + +/// `Delimiters` constants. +#[allow(non_upper_case_globals, non_snake_case)] +pub mod Delimiter { + use super::Delimiters; + + /// The empty delimiter set + pub const None: Delimiters = Delimiters { bits: 0 }; + /// The delimiter set with only the `{` opening curly bracket + pub const CurlyBracketBlock: Delimiters = Delimiters { bits: 1 << 1 }; + /// The delimiter set with only the `;` semicolon + pub const Semicolon: Delimiters = Delimiters { bits: 1 << 2 }; + /// The delimiter set with only the `!` exclamation point + pub const Bang: Delimiters = Delimiters { bits: 1 << 3 }; + /// The delimiter set with only the `,` comma + pub const Comma: Delimiters = Delimiters { bits: 1 << 4 }; +} + +#[allow(non_upper_case_globals, non_snake_case)] +mod ClosingDelimiter { + use super::Delimiters; + + pub const CloseCurlyBracket: Delimiters = Delimiters { bits: 1 << 5 }; + pub const CloseSquareBracket: Delimiters = Delimiters { bits: 1 << 6 }; + pub const CloseParenthesis: Delimiters = Delimiters { bits: 1 << 7 }; +} + +impl BitOr<Delimiters> for Delimiters { + type Output = Delimiters; + + #[inline] + fn bitor(self, other: Delimiters) -> Delimiters { + Delimiters { + bits: self.bits | other.bits, + } + } +} + +impl Delimiters { + #[inline] + fn contains(self, other: Delimiters) -> bool { + (self.bits & other.bits) != 0 + } + + #[inline] + fn from_byte(byte: Option<u8>) -> Delimiters { + match byte { + Some(b';') => Delimiter::Semicolon, + Some(b'!') => Delimiter::Bang, + Some(b',') => Delimiter::Comma, + Some(b'{') => Delimiter::CurlyBracketBlock, + Some(b'}') => ClosingDelimiter::CloseCurlyBracket, + Some(b']') => ClosingDelimiter::CloseSquareBracket, + Some(b')') => ClosingDelimiter::CloseParenthesis, + _ => Delimiter::None, + } + } +} + +/// Used in some `fn expect_*` methods +macro_rules! expect { + ($parser: ident, $($branches: tt)+) => { + { + let start_location = $parser.current_source_location(); + match *$parser.next()? { + $($branches)+ + ref token => { + return Err(start_location.new_basic_unexpected_token_error(token.clone())) + } + } + } + } +} + +impl<'i: 't, 't> Parser<'i, 't> { + /// Create a new parser + #[inline] + pub fn new(input: &'t mut ParserInput<'i>) -> Parser<'i, 't> { + Parser { + input: input, + at_start_of: None, + stop_before: Delimiter::None, + } + } + + /// Return the current line that is being parsed. + pub fn current_line(&self) -> &'i str { + self.input.tokenizer.current_source_line() + } + + /// Check whether the input is exhausted. That is, if `.next()` would return a token. + /// + /// This ignores whitespace and comments. + #[inline] + pub fn is_exhausted(&mut self) -> bool { + self.expect_exhausted().is_ok() + } + + /// Check whether the input is exhausted. That is, if `.next()` would return a token. + /// Return a `Result` so that the `?` operator can be used: `input.expect_exhausted()?` + /// + /// This ignores whitespace and comments. + #[inline] + pub fn expect_exhausted(&mut self) -> Result<(), BasicParseError<'i>> { + let start = self.state(); + let result = match self.next() { + Err(BasicParseError { + kind: BasicParseErrorKind::EndOfInput, + .. + }) => Ok(()), + Err(e) => unreachable!("Unexpected error encountered: {:?}", e), + Ok(t) => Err(start + .source_location() + .new_basic_unexpected_token_error(t.clone())), + }; + self.reset(&start); + result + } + + /// Return the current position within the input. + /// + /// This can be used with the `Parser::slice` and `slice_from` methods. + #[inline] + pub fn position(&self) -> SourcePosition { + self.input.tokenizer.position() + } + + /// The current line number and column number. + #[inline] + pub fn current_source_location(&self) -> SourceLocation { + self.input.tokenizer.current_source_location() + } + + /// The source map URL, if known. + /// + /// The source map URL is extracted from a specially formatted + /// comment. The last such comment is used, so this value may + /// change as parsing proceeds. + pub fn current_source_map_url(&self) -> Option<&str> { + self.input.tokenizer.current_source_map_url() + } + + /// The source URL, if known. + /// + /// The source URL is extracted from a specially formatted + /// comment. The last such comment is used, so this value may + /// change as parsing proceeds. + pub fn current_source_url(&self) -> Option<&str> { + self.input.tokenizer.current_source_url() + } + + /// Create a new BasicParseError at the current location + #[inline] + pub fn new_basic_error(&self, kind: BasicParseErrorKind<'i>) -> BasicParseError<'i> { + BasicParseError { + kind, + location: self.current_source_location(), + } + } + + /// Create a new basic ParseError at the current location + #[inline] + pub fn new_error<E>(&self, kind: BasicParseErrorKind<'i>) -> ParseError<'i, E> { + ParseError { + kind: ParseErrorKind::Basic(kind), + location: self.current_source_location(), + } + } + + /// Create a new custom BasicParseError at the current location + #[inline] + pub fn new_custom_error<E1: Into<E2>, E2>(&self, error: E1) -> ParseError<'i, E2> { + self.current_source_location().new_custom_error(error) + } + + /// Create a new unexpected token BasicParseError at the current location + #[inline] + pub fn new_basic_unexpected_token_error(&self, token: Token<'i>) -> BasicParseError<'i> { + self.new_basic_error(BasicParseErrorKind::UnexpectedToken(token)) + } + + /// Create a new unexpected token ParseError at the current location + #[inline] + pub fn new_unexpected_token_error<E>(&self, token: Token<'i>) -> ParseError<'i, E> { + self.new_error(BasicParseErrorKind::UnexpectedToken(token)) + } + + /// Create a new unexpected token or EOF ParseError at the current location + #[inline] + pub fn new_error_for_next_token<E>(&mut self) -> ParseError<'i, E> { + let token = match self.next() { + Ok(token) => token.clone(), + Err(e) => return e.into(), + }; + self.new_error(BasicParseErrorKind::UnexpectedToken(token)) + } + + /// Return the current internal state of the parser (including position within the input). + /// + /// This state can later be restored with the `Parser::reset` method. + #[inline] + pub fn state(&self) -> ParserState { + ParserState { + at_start_of: self.at_start_of, + ..self.input.tokenizer.state() + } + } + + /// Advance the input until the next token that’s not whitespace or a comment. + #[inline] + pub fn skip_whitespace(&mut self) { + if let Some(block_type) = self.at_start_of.take() { + consume_until_end_of_block(block_type, &mut self.input.tokenizer); + } + + self.input.tokenizer.skip_whitespace() + } + + #[inline] + pub(crate) fn skip_cdc_and_cdo(&mut self) { + if let Some(block_type) = self.at_start_of.take() { + consume_until_end_of_block(block_type, &mut self.input.tokenizer); + } + + self.input.tokenizer.skip_cdc_and_cdo() + } + + #[inline] + pub(crate) fn next_byte(&self) -> Option<u8> { + let byte = self.input.tokenizer.next_byte(); + if self.stop_before.contains(Delimiters::from_byte(byte)) { + return None; + } + byte + } + + /// Restore the internal state of the parser (including position within the input) + /// to what was previously saved by the `Parser::position` method. + /// + /// Should only be used with `SourcePosition` values from the same `Parser` instance. + #[inline] + pub fn reset(&mut self, state: &ParserState) { + self.input.tokenizer.reset(state); + self.at_start_of = state.at_start_of; + } + + /// Start looking for `var()` / `env()` functions. (See the + /// `.seen_var_or_env_functions()` method.) + #[inline] + pub fn look_for_var_or_env_functions(&mut self) { + self.input.tokenizer.look_for_var_or_env_functions() + } + + /// Return whether a `var()` or `env()` function has been seen by the + /// tokenizer since either `look_for_var_or_env_functions` was called, and + /// stop looking. + #[inline] + pub fn seen_var_or_env_functions(&mut self) -> bool { + self.input.tokenizer.seen_var_or_env_functions() + } + + /// The old name of `try_parse`, which requires raw identifiers in the Rust 2018 edition. + #[inline] + pub fn r#try<F, T, E>(&mut self, thing: F) -> Result<T, E> + where + F: FnOnce(&mut Parser<'i, 't>) -> Result<T, E>, + { + self.try_parse(thing) + } + + /// Execute the given closure, passing it the parser. + /// If the result (returned unchanged) is `Err`, + /// the internal state of the parser (including position within the input) + /// is restored to what it was before the call. + #[inline] + pub fn try_parse<F, T, E>(&mut self, thing: F) -> Result<T, E> + where + F: FnOnce(&mut Parser<'i, 't>) -> Result<T, E>, + { + let start = self.state(); + let result = thing(self); + if result.is_err() { + self.reset(&start) + } + result + } + + /// Return a slice of the CSS input + #[inline] + pub fn slice(&self, range: Range<SourcePosition>) -> &'i str { + self.input.tokenizer.slice(range) + } + + /// Return a slice of the CSS input, from the given position to the current one. + #[inline] + pub fn slice_from(&self, start_position: SourcePosition) -> &'i str { + self.input.tokenizer.slice_from(start_position) + } + + /// Return the next token in the input that is neither whitespace or a comment, + /// and advance the position accordingly. + /// + /// After returning a `Function`, `ParenthesisBlock`, + /// `CurlyBracketBlock`, or `SquareBracketBlock` token, + /// the next call will skip until after the matching `CloseParenthesis`, + /// `CloseCurlyBracket`, or `CloseSquareBracket` token. + /// + /// See the `Parser::parse_nested_block` method to parse the content of functions or blocks. + /// + /// This only returns a closing token when it is unmatched (and therefore an error). + pub fn next(&mut self) -> Result<&Token<'i>, BasicParseError<'i>> { + self.skip_whitespace(); + self.next_including_whitespace_and_comments() + } + + /// Same as `Parser::next`, but does not skip whitespace tokens. + pub fn next_including_whitespace(&mut self) -> Result<&Token<'i>, BasicParseError<'i>> { + loop { + match self.next_including_whitespace_and_comments() { + Err(e) => return Err(e), + Ok(&Token::Comment(_)) => {} + _ => break, + } + } + Ok(self.input.cached_token_ref()) + } + + /// Same as `Parser::next`, but does not skip whitespace or comment tokens. + /// + /// **Note**: This should only be used in contexts like a CSS pre-processor + /// where comments are preserved. + /// When parsing higher-level values, per the CSS Syntax specification, + /// comments should always be ignored between tokens. + pub fn next_including_whitespace_and_comments( + &mut self, + ) -> Result<&Token<'i>, BasicParseError<'i>> { + if let Some(block_type) = self.at_start_of.take() { + consume_until_end_of_block(block_type, &mut self.input.tokenizer); + } + + let byte = self.input.tokenizer.next_byte(); + if self.stop_before.contains(Delimiters::from_byte(byte)) { + return Err(self.new_basic_error(BasicParseErrorKind::EndOfInput)); + } + + let token_start_position = self.input.tokenizer.position(); + let using_cached_token = self + .input + .cached_token + .as_ref() + .map_or(false, |cached_token| { + cached_token.start_position == token_start_position + }); + let token = if using_cached_token { + let cached_token = self.input.cached_token.as_ref().unwrap(); + self.input.tokenizer.reset(&cached_token.end_state); + match cached_token.token { + Token::Function(ref name) => self.input.tokenizer.see_function(name), + _ => {} + } + &cached_token.token + } else { + let new_token = self + .input + .tokenizer + .next() + .map_err(|()| self.new_basic_error(BasicParseErrorKind::EndOfInput))?; + self.input.cached_token = Some(CachedToken { + token: new_token, + start_position: token_start_position, + end_state: self.input.tokenizer.state(), + }); + self.input.cached_token_ref() + }; + + if let Some(block_type) = BlockType::opening(token) { + self.at_start_of = Some(block_type); + } + Ok(token) + } + + /// Have the given closure parse something, then check the the input is exhausted. + /// The result is overridden to `Err(())` if some input remains. + /// + /// This can help tell e.g. `color: green;` from `color: green 4px;` + #[inline] + pub fn parse_entirely<F, T, E>(&mut self, parse: F) -> Result<T, ParseError<'i, E>> + where + F: FnOnce(&mut Parser<'i, 't>) -> Result<T, ParseError<'i, E>>, + { + let result = parse(self)?; + self.expect_exhausted()?; + Ok(result) + } + + /// Parse a list of comma-separated values, all with the same syntax. + /// + /// The given closure is called repeatedly with a "delimited" parser + /// (see the `Parser::parse_until_before` method) so that it can over + /// consume the input past a comma at this block/function nesting level. + /// + /// Successful results are accumulated in a vector. + /// + /// This method returns `Err(())` the first time that a closure call does, + /// or if a closure call leaves some input before the next comma or the end + /// of the input. + #[inline] + pub fn parse_comma_separated<F, T, E>( + &mut self, + parse_one: F, + ) -> Result<Vec<T>, ParseError<'i, E>> + where + F: for<'tt> FnMut(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>, + { + self.parse_comma_separated_internal(parse_one, /* ignore_errors = */ false) + } + + /// Like `parse_comma_separated`, but ignores errors on unknown components, + /// rather than erroring out in the whole list. + /// + /// Caller must deal with the fact that the resulting list might be empty, + /// if there's no valid component on the list. + #[inline] + pub fn parse_comma_separated_ignoring_errors<F, T, E: 'i>(&mut self, parse_one: F) -> Vec<T> + where + F: for<'tt> FnMut(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>, + { + match self.parse_comma_separated_internal(parse_one, /* ignore_errors = */ true) { + Ok(values) => values, + Err(..) => unreachable!(), + } + } + + #[inline] + fn parse_comma_separated_internal<F, T, E>( + &mut self, + mut parse_one: F, + ignore_errors: bool, + ) -> Result<Vec<T>, ParseError<'i, E>> + where + F: for<'tt> FnMut(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>, + { + // Vec grows from 0 to 4 by default on first push(). So allocate with + // capacity 1, so in the somewhat common case of only one item we don't + // way overallocate. Note that we always push at least one item if + // parsing succeeds. + let mut values = Vec::with_capacity(1); + loop { + self.skip_whitespace(); // Unnecessary for correctness, but may help try() in parse_one rewind less. + match self.parse_until_before(Delimiter::Comma, &mut parse_one) { + Ok(v) => values.push(v), + Err(e) if !ignore_errors => return Err(e), + Err(_) => {}, + } + match self.next() { + Err(_) => return Ok(values), + Ok(&Token::Comma) => continue, + Ok(_) => unreachable!(), + } + } + } + + /// Parse the content of a block or function. + /// + /// This method panics if the last token yielded by this parser + /// (from one of the `next*` methods) + /// is not a on that marks the start of a block or function: + /// a `Function`, `ParenthesisBlock`, `CurlyBracketBlock`, or `SquareBracketBlock`. + /// + /// The given closure is called with a "delimited" parser + /// that stops at the end of the block or function (at the matching closing token). + /// + /// The result is overridden to `Err(())` if the closure leaves some input before that point. + #[inline] + pub fn parse_nested_block<F, T, E>(&mut self, parse: F) -> Result<T, ParseError<'i, E>> + where + F: for<'tt> FnOnce(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>, + { + parse_nested_block(self, parse) + } + + /// Limit parsing to until a given delimiter or the end of the input. (E.g. + /// a semicolon for a property value.) + /// + /// The given closure is called with a "delimited" parser + /// that stops before the first character at this block/function nesting level + /// that matches the given set of delimiters, or at the end of the input. + /// + /// The result is overridden to `Err(())` if the closure leaves some input before that point. + #[inline] + pub fn parse_until_before<F, T, E>( + &mut self, + delimiters: Delimiters, + parse: F, + ) -> Result<T, ParseError<'i, E>> + where + F: for<'tt> FnOnce(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>, + { + parse_until_before(self, delimiters, parse) + } + + /// Like `parse_until_before`, but also consume the delimiter token. + /// + /// This can be useful when you don’t need to know which delimiter it was + /// (e.g. if these is only one in the given set) + /// or if it was there at all (as opposed to reaching the end of the input). + #[inline] + pub fn parse_until_after<F, T, E>( + &mut self, + delimiters: Delimiters, + parse: F, + ) -> Result<T, ParseError<'i, E>> + where + F: for<'tt> FnOnce(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>, + { + parse_until_after(self, delimiters, parse) + } + + /// Parse a <whitespace-token> and return its value. + #[inline] + pub fn expect_whitespace(&mut self) -> Result<&'i str, BasicParseError<'i>> { + let start_location = self.current_source_location(); + match *self.next_including_whitespace()? { + Token::WhiteSpace(value) => Ok(value), + ref t => Err(start_location.new_basic_unexpected_token_error(t.clone())), + } + } + + /// Parse a <ident-token> and return the unescaped value. + #[inline] + pub fn expect_ident(&mut self) -> Result<&CowRcStr<'i>, BasicParseError<'i>> { + expect! {self, + Token::Ident(ref value) => Ok(value), + } + } + + /// expect_ident, but clone the CowRcStr + #[inline] + pub fn expect_ident_cloned(&mut self) -> Result<CowRcStr<'i>, BasicParseError<'i>> { + self.expect_ident().map(|s| s.clone()) + } + + /// Parse a <ident-token> whose unescaped value is an ASCII-insensitive match for the given value. + #[inline] + pub fn expect_ident_matching( + &mut self, + expected_value: &str, + ) -> Result<(), BasicParseError<'i>> { + expect! {self, + Token::Ident(ref value) if value.eq_ignore_ascii_case(expected_value) => Ok(()), + } + } + + /// Parse a <string-token> and return the unescaped value. + #[inline] + pub fn expect_string(&mut self) -> Result<&CowRcStr<'i>, BasicParseError<'i>> { + expect! {self, + Token::QuotedString(ref value) => Ok(value), + } + } + + /// expect_string, but clone the CowRcStr + #[inline] + pub fn expect_string_cloned(&mut self) -> Result<CowRcStr<'i>, BasicParseError<'i>> { + self.expect_string().map(|s| s.clone()) + } + + /// Parse either a <ident-token> or a <string-token>, and return the unescaped value. + #[inline] + pub fn expect_ident_or_string(&mut self) -> Result<&CowRcStr<'i>, BasicParseError<'i>> { + expect! {self, + Token::Ident(ref value) => Ok(value), + Token::QuotedString(ref value) => Ok(value), + } + } + + /// Parse a <url-token> and return the unescaped value. + #[inline] + pub fn expect_url(&mut self) -> Result<CowRcStr<'i>, BasicParseError<'i>> { + expect! {self, + Token::UnquotedUrl(ref value) => Ok(value.clone()), + Token::Function(ref name) if name.eq_ignore_ascii_case("url") => { + self.parse_nested_block(|input| { + input.expect_string().map_err(Into::into).map(|s| s.clone()) + }) + .map_err(ParseError::<()>::basic) + } + } + } + + /// Parse either a <url-token> or a <string-token>, and return the unescaped value. + #[inline] + pub fn expect_url_or_string(&mut self) -> Result<CowRcStr<'i>, BasicParseError<'i>> { + expect! {self, + Token::UnquotedUrl(ref value) => Ok(value.clone()), + Token::QuotedString(ref value) => Ok(value.clone()), + Token::Function(ref name) if name.eq_ignore_ascii_case("url") => { + self.parse_nested_block(|input| { + input.expect_string().map_err(Into::into).map(|s| s.clone()) + }) + .map_err(ParseError::<()>::basic) + } + } + } + + /// Parse a <number-token> and return the integer value. + #[inline] + pub fn expect_number(&mut self) -> Result<f32, BasicParseError<'i>> { + expect! {self, + Token::Number { value, .. } => Ok(value), + } + } + + /// Parse a <number-token> that does not have a fractional part, and return the integer value. + #[inline] + pub fn expect_integer(&mut self) -> Result<i32, BasicParseError<'i>> { + expect! {self, + Token::Number { int_value: Some(int_value), .. } => Ok(int_value), + } + } + + /// Parse a <percentage-token> and return the value. + /// `0%` and `100%` map to `0.0` and `1.0` (not `100.0`), respectively. + #[inline] + pub fn expect_percentage(&mut self) -> Result<f32, BasicParseError<'i>> { + expect! {self, + Token::Percentage { unit_value, .. } => Ok(unit_value), + } + } + + /// Parse a `:` <colon-token>. + #[inline] + pub fn expect_colon(&mut self) -> Result<(), BasicParseError<'i>> { + expect! {self, + Token::Colon => Ok(()), + } + } + + /// Parse a `;` <semicolon-token>. + #[inline] + pub fn expect_semicolon(&mut self) -> Result<(), BasicParseError<'i>> { + expect! {self, + Token::Semicolon => Ok(()), + } + } + + /// Parse a `,` <comma-token>. + #[inline] + pub fn expect_comma(&mut self) -> Result<(), BasicParseError<'i>> { + expect! {self, + Token::Comma => Ok(()), + } + } + + /// Parse a <delim-token> with the given value. + #[inline] + pub fn expect_delim(&mut self, expected_value: char) -> Result<(), BasicParseError<'i>> { + expect! {self, + Token::Delim(value) if value == expected_value => Ok(()), + } + } + + /// Parse a `{ /* ... */ }` curly brackets block. + /// + /// If the result is `Ok`, you can then call the `Parser::parse_nested_block` method. + #[inline] + pub fn expect_curly_bracket_block(&mut self) -> Result<(), BasicParseError<'i>> { + expect! {self, + Token::CurlyBracketBlock => Ok(()), + } + } + + /// Parse a `[ /* ... */ ]` square brackets block. + /// + /// If the result is `Ok`, you can then call the `Parser::parse_nested_block` method. + #[inline] + pub fn expect_square_bracket_block(&mut self) -> Result<(), BasicParseError<'i>> { + expect! {self, + Token::SquareBracketBlock => Ok(()), + } + } + + /// Parse a `( /* ... */ )` parenthesis block. + /// + /// If the result is `Ok`, you can then call the `Parser::parse_nested_block` method. + #[inline] + pub fn expect_parenthesis_block(&mut self) -> Result<(), BasicParseError<'i>> { + expect! {self, + Token::ParenthesisBlock => Ok(()), + } + } + + /// Parse a <function> token and return its name. + /// + /// If the result is `Ok`, you can then call the `Parser::parse_nested_block` method. + #[inline] + pub fn expect_function(&mut self) -> Result<&CowRcStr<'i>, BasicParseError<'i>> { + expect! {self, + Token::Function(ref name) => Ok(name), + } + } + + /// Parse a <function> token whose name is an ASCII-insensitive match for the given value. + /// + /// If the result is `Ok`, you can then call the `Parser::parse_nested_block` method. + #[inline] + pub fn expect_function_matching( + &mut self, + expected_name: &str, + ) -> Result<(), BasicParseError<'i>> { + expect! {self, + Token::Function(ref name) if name.eq_ignore_ascii_case(expected_name) => Ok(()), + } + } + + /// Parse the input until exhaustion and check that it contains no “error” token. + /// + /// See `Token::is_parse_error`. This also checks nested blocks and functions recursively. + #[inline] + pub fn expect_no_error_token(&mut self) -> Result<(), BasicParseError<'i>> { + loop { + match self.next_including_whitespace_and_comments() { + Ok(&Token::Function(_)) + | Ok(&Token::ParenthesisBlock) + | Ok(&Token::SquareBracketBlock) + | Ok(&Token::CurlyBracketBlock) => self + .parse_nested_block(|input| input.expect_no_error_token().map_err(Into::into)) + .map_err(ParseError::<()>::basic)?, + Ok(t) => { + // FIXME: maybe these should be separate variants of + // BasicParseError instead? + if t.is_parse_error() { + let token = t.clone(); + return Err(self.new_basic_unexpected_token_error(token)); + } + } + Err(_) => return Ok(()), + } + } + } +} + +pub fn parse_until_before<'i: 't, 't, F, T, E>( + parser: &mut Parser<'i, 't>, + delimiters: Delimiters, + parse: F, +) -> Result<T, ParseError<'i, E>> +where + F: for<'tt> FnOnce(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>, +{ + let delimiters = parser.stop_before | delimiters; + let result; + // Introduce a new scope to limit duration of nested_parser’s borrow + { + let mut delimited_parser = Parser { + input: parser.input, + at_start_of: parser.at_start_of.take(), + stop_before: delimiters, + }; + result = delimited_parser.parse_entirely(parse); + if let Some(block_type) = delimited_parser.at_start_of { + consume_until_end_of_block(block_type, &mut delimited_parser.input.tokenizer); + } + } + // FIXME: have a special-purpose tokenizer method for this that does less work. + loop { + if delimiters.contains(Delimiters::from_byte(parser.input.tokenizer.next_byte())) { + break; + } + if let Ok(token) = parser.input.tokenizer.next() { + if let Some(block_type) = BlockType::opening(&token) { + consume_until_end_of_block(block_type, &mut parser.input.tokenizer); + } + } else { + break; + } + } + result +} + +pub fn parse_until_after<'i: 't, 't, F, T, E>( + parser: &mut Parser<'i, 't>, + delimiters: Delimiters, + parse: F, +) -> Result<T, ParseError<'i, E>> +where + F: for<'tt> FnOnce(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>, +{ + let result = parser.parse_until_before(delimiters, parse); + let next_byte = parser.input.tokenizer.next_byte(); + if next_byte.is_some() + && !parser + .stop_before + .contains(Delimiters::from_byte(next_byte)) + { + debug_assert!(delimiters.contains(Delimiters::from_byte(next_byte))); + // We know this byte is ASCII. + parser.input.tokenizer.advance(1); + if next_byte == Some(b'{') { + consume_until_end_of_block(BlockType::CurlyBracket, &mut parser.input.tokenizer); + } + } + result +} + +pub fn parse_nested_block<'i: 't, 't, F, T, E>( + parser: &mut Parser<'i, 't>, + parse: F, +) -> Result<T, ParseError<'i, E>> +where + F: for<'tt> FnOnce(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>, +{ + let block_type = parser.at_start_of.take().expect( + "\ + A nested parser can only be created when a Function, \ + ParenthesisBlock, SquareBracketBlock, or CurlyBracketBlock \ + token was just consumed.\ + ", + ); + let closing_delimiter = match block_type { + BlockType::CurlyBracket => ClosingDelimiter::CloseCurlyBracket, + BlockType::SquareBracket => ClosingDelimiter::CloseSquareBracket, + BlockType::Parenthesis => ClosingDelimiter::CloseParenthesis, + }; + let result; + // Introduce a new scope to limit duration of nested_parser’s borrow + { + let mut nested_parser = Parser { + input: parser.input, + at_start_of: None, + stop_before: closing_delimiter, + }; + result = nested_parser.parse_entirely(parse); + if let Some(block_type) = nested_parser.at_start_of { + consume_until_end_of_block(block_type, &mut nested_parser.input.tokenizer); + } + } + consume_until_end_of_block(block_type, &mut parser.input.tokenizer); + result +} + +#[inline(never)] +#[cold] +fn consume_until_end_of_block(block_type: BlockType, tokenizer: &mut Tokenizer) { + let mut stack = SmallVec::<[BlockType; 16]>::new(); + stack.push(block_type); + + // FIXME: have a special-purpose tokenizer method for this that does less work. + while let Ok(ref token) = tokenizer.next() { + if let Some(b) = BlockType::closing(token) { + if *stack.last().unwrap() == b { + stack.pop(); + if stack.is_empty() { + return; + } + } + } + + if let Some(block_type) = BlockType::opening(token) { + stack.push(block_type); + } + } +} diff --git a/third_party/rust/cssparser/src/rules_and_declarations.rs b/third_party/rust/cssparser/src/rules_and_declarations.rs new file mode 100644 index 0000000000..34ba8e985d --- /dev/null +++ b/third_party/rust/cssparser/src/rules_and_declarations.rs @@ -0,0 +1,496 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +// https://drafts.csswg.org/css-syntax/#parsing + +use super::{ + BasicParseError, BasicParseErrorKind, Delimiter, Delimiters, ParseError, Parser, Token, +}; +use crate::cow_rc_str::CowRcStr; +use crate::parser::{parse_nested_block, parse_until_after, parse_until_before, ParserState}; + +/// Parse `!important`. +/// +/// Typical usage is `input.try_parse(parse_important).is_ok()` +/// at the end of a `DeclarationParser::parse_value` implementation. +pub fn parse_important<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), BasicParseError<'i>> { + input.expect_delim('!')?; + input.expect_ident_matching("important") +} + +/// A trait to provide various parsing of declaration values. +/// +/// For example, there could be different implementations for property declarations in style rules +/// and for descriptors in `@font-face` rules. +pub trait DeclarationParser<'i> { + /// The finished representation of a declaration. + type Declaration; + + /// The error type that is included in the ParseError value that can be returned. + type Error: 'i; + + /// Parse the value of a declaration with the given `name`. + /// + /// Return the finished representation for the declaration + /// as returned by `DeclarationListParser::next`, + /// or `Err(())` to ignore the entire declaration as invalid. + /// + /// Declaration name matching should be case-insensitive in the ASCII range. + /// This can be done with `std::ascii::Ascii::eq_ignore_ascii_case`, + /// or with the `match_ignore_ascii_case!` macro. + /// + /// The given `input` is a "delimited" parser + /// that ends wherever the declaration value should end. + /// (In declaration lists, before the next semicolon or end of the current block.) + /// + /// If `!important` can be used in a given context, + /// `input.try_parse(parse_important).is_ok()` should be used at the end + /// of the implementation of this method and the result should be part of the return value. + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<Self::Declaration, ParseError<'i, Self::Error>> { + Err(input.new_error(BasicParseErrorKind::UnexpectedToken(Token::Ident(name)))) + } +} + +/// A trait to provide various parsing of at-rules. +/// +/// For example, there could be different implementations for top-level at-rules +/// (`@media`, `@font-face`, …) +/// and for page-margin rules inside `@page`. +/// +/// Default implementations that reject all at-rules are provided, +/// so that `impl AtRuleParser<(), ()> for ... {}` can be used +/// for using `DeclarationListParser` to parse a declarations list with only qualified rules. +pub trait AtRuleParser<'i> { + /// The intermediate representation of prelude of an at-rule. + type Prelude; + + /// The finished representation of an at-rule. + type AtRule; + + /// The error type that is included in the ParseError value that can be returned. + type Error: 'i; + + /// Parse the prelude of an at-rule with the given `name`. + /// + /// Return the representation of the prelude and the type of at-rule, + /// or `Err(())` to ignore the entire at-rule as invalid. + /// + /// The prelude is the part after the at-keyword + /// and before the `;` semicolon or `{ /* ... */ }` block. + /// + /// At-rule name matching should be case-insensitive in the ASCII range. + /// This can be done with `std::ascii::Ascii::eq_ignore_ascii_case`, + /// or with the `match_ignore_ascii_case!` macro. + /// + /// The given `input` is a "delimited" parser + /// that ends wherever the prelude should end. + /// (Before the next semicolon, the next `{`, or the end of the current block.) + fn parse_prelude<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<Self::Prelude, ParseError<'i, Self::Error>> { + Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name))) + } + + /// End an at-rule which doesn't have block. Return the finished + /// representation of the at-rule. + /// + /// The location passed in is source location of the start of the prelude. + /// + /// This is only called when `parse_prelude` returned `WithoutBlock`, and + /// either the `;` semicolon indeed follows the prelude, or parser is at + /// the end of the input. + fn rule_without_block( + &mut self, + prelude: Self::Prelude, + start: &ParserState, + ) -> Result<Self::AtRule, ()> { + let _ = prelude; + let _ = start; + Err(()) + } + + /// Parse the content of a `{ /* ... */ }` block for the body of the at-rule. + /// + /// The location passed in is source location of the start of the prelude. + /// + /// Return the finished representation of the at-rule + /// as returned by `RuleListParser::next` or `DeclarationListParser::next`, + /// or `Err(())` to ignore the entire at-rule as invalid. + /// + /// This is only called when `parse_prelude` returned `WithBlock`, and a block + /// was indeed found following the prelude. + fn parse_block<'t>( + &mut self, + prelude: Self::Prelude, + start: &ParserState, + input: &mut Parser<'i, 't>, + ) -> Result<Self::AtRule, ParseError<'i, Self::Error>> { + let _ = prelude; + let _ = start; + Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)) + } +} + +/// A trait to provide various parsing of qualified rules. +/// +/// For example, there could be different implementations for top-level qualified rules (i.e. style +/// rules with Selectors as prelude) and for qualified rules inside `@keyframes` (keyframe rules +/// with keyframe selectors as prelude). +/// +/// Default implementations that reject all qualified rules are provided, so that +/// `impl QualifiedRuleParser<(), ()> for ... {}` can be used for example for using +/// `RuleListParser` to parse a rule list with only at-rules (such as inside +/// `@font-feature-values`). +pub trait QualifiedRuleParser<'i> { + /// The intermediate representation of a qualified rule prelude. + type Prelude; + + /// The finished representation of a qualified rule. + type QualifiedRule; + + /// The error type that is included in the ParseError value that can be returned. + type Error: 'i; + + /// Parse the prelude of a qualified rule. For style rules, this is as Selector list. + /// + /// Return the representation of the prelude, + /// or `Err(())` to ignore the entire at-rule as invalid. + /// + /// The prelude is the part before the `{ /* ... */ }` block. + /// + /// The given `input` is a "delimited" parser + /// that ends where the prelude should end (before the next `{`). + fn parse_prelude<'t>( + &mut self, + input: &mut Parser<'i, 't>, + ) -> Result<Self::Prelude, ParseError<'i, Self::Error>> { + Err(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid)) + } + + /// Parse the content of a `{ /* ... */ }` block for the body of the qualified rule. + /// + /// The location passed in is source location of the start of the prelude. + /// + /// Return the finished representation of the qualified rule + /// as returned by `RuleListParser::next`, + /// or `Err(())` to ignore the entire at-rule as invalid. + fn parse_block<'t>( + &mut self, + prelude: Self::Prelude, + start: &ParserState, + input: &mut Parser<'i, 't>, + ) -> Result<Self::QualifiedRule, ParseError<'i, Self::Error>> { + let _ = prelude; + let _ = start; + Err(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid)) + } +} + +/// Provides an iterator for rule bodies and declaration lists. +pub struct RuleBodyParser<'i, 't, 'a, P, I, E> { + /// The input given to the parser. + pub input: &'a mut Parser<'i, 't>, + /// The parser given to `DeclarationListParser::new` + pub parser: &'a mut P, + + _phantom: std::marker::PhantomData<(I, E)>, +} + +/// A parser for a rule body item. +pub trait RuleBodyItemParser<'i, DeclOrRule, Error: 'i>: + DeclarationParser<'i, Declaration = DeclOrRule, Error = Error> + + QualifiedRuleParser<'i, QualifiedRule = DeclOrRule, Error = Error> + + AtRuleParser<'i, AtRule = DeclOrRule, Error = Error> +{ + /// Whether we should attempt to parse declarations. If you know you won't, returning false + /// here is slightly faster. + fn parse_declarations(&self) -> bool; + /// Whether we should attempt to parse qualified rules. If you know you won't, returning false + /// would be slightly faster. + fn parse_qualified(&self) -> bool; +} + +impl<'i, 't, 'a, P, I, E> RuleBodyParser<'i, 't, 'a, P, I, E> { + /// Create a new `DeclarationListParser` for the given `input` and `parser`. + /// + /// Note that all CSS declaration lists can on principle contain at-rules. + /// Even if no such valid at-rule exists (yet), + /// this affects error handling: at-rules end at `{}` blocks, not just semicolons. + /// + /// The given `parser` therefore needs to implement + /// both `DeclarationParser` and `AtRuleParser` traits. + /// However, the latter can be an empty `impl` + /// since `AtRuleParser` provides default implementations of its methods. + /// + /// The return type for finished declarations and at-rules also needs to be the same, + /// since `<DeclarationListParser as Iterator>::next` can return either. + /// It could be a custom enum. + pub fn new(input: &'a mut Parser<'i, 't>, parser: &'a mut P) -> Self { + Self { + input, + parser, + _phantom: std::marker::PhantomData, + } + } +} + +/// https://drafts.csswg.org/css-syntax/#consume-a-blocks-contents +impl<'i, 't, 'a, I, P, E: 'i> Iterator for RuleBodyParser<'i, 't, 'a, P, I, E> +where + P: RuleBodyItemParser<'i, I, E>, +{ + type Item = Result<I, (ParseError<'i, E>, &'i str)>; + + fn next(&mut self) -> Option<Self::Item> { + loop { + self.input.skip_whitespace(); + let start = self.input.state(); + match self.input.next_including_whitespace_and_comments().ok()? { + Token::CloseCurlyBracket | + Token::WhiteSpace(..) | + Token::Semicolon | + Token::Comment(..) => continue, + Token::AtKeyword(ref name) => { + let name = name.clone(); + return Some(parse_at_rule(&start, name, self.input, &mut *self.parser)); + } + // https://drafts.csswg.org/css-syntax/#consume-a-declaration bails out just to + // keep parsing as a qualified rule if the token is not an ident, so we implement + // that in a slightly more straight-forward way + Token::Ident(ref name) if self.parser.parse_declarations() => { + let name = name.clone(); + let result = { + let parser = &mut self.parser; + parse_until_after(self.input, Delimiter::Semicolon, |input| { + input.expect_colon()?; + parser.parse_value(name, input) + }) + }; + if result.is_err() && self.parser.parse_qualified() { + self.input.reset(&start); + // We ignore the resulting error here. The property declaration parse error + // is likely to be more relevant. + if let Ok(qual) = parse_qualified_rule( + &start, + self.input, + &mut *self.parser, + Delimiter::Semicolon | Delimiter::CurlyBracketBlock, + ) { + return Some(Ok(qual)) + } + } + + return Some(result.map_err(|e| (e, self.input.slice_from(start.position())))); + } + token => { + let result = if self.parser.parse_qualified() { + self.input.reset(&start); + let delimiters = if self.parser.parse_declarations() { + Delimiter::Semicolon | Delimiter::CurlyBracketBlock + } else { + Delimiter::CurlyBracketBlock + }; + parse_qualified_rule(&start, self.input, &mut *self.parser, delimiters) + } else { + let token = token.clone(); + self.input.parse_until_after(Delimiter::Semicolon, |_| { + Err(start.source_location().new_unexpected_token_error(token)) + }) + }; + return Some(result.map_err(|e| (e, self.input.slice_from(start.position())))); + } + } + } + } +} + +/// Provides an iterator for rule list parsing at the top-level of a stylesheet. +pub struct StyleSheetParser<'i, 't, 'a, P> { + /// The input given. + pub input: &'a mut Parser<'i, 't>, + + /// The parser given. + pub parser: &'a mut P, + + any_rule_so_far: bool, +} + +impl<'i, 't, 'a, R, P, E: 'i> StyleSheetParser<'i, 't, 'a, P> +where + P: QualifiedRuleParser<'i, QualifiedRule = R, Error = E> + + AtRuleParser<'i, AtRule = R, Error = E>, +{ + /// The given `parser` needs to implement both `QualifiedRuleParser` and `AtRuleParser` traits. + /// However, either of them can be an empty `impl` since the traits provide default + /// implementations of their methods. + /// + /// The return type for finished qualified rules and at-rules also needs to be the same, + /// since `<RuleListParser as Iterator>::next` can return either. It could be a custom enum. + pub fn new(input: &'a mut Parser<'i, 't>, parser: &'a mut P) -> Self { + Self { + input, + parser, + any_rule_so_far: false, + } + } +} + +/// `RuleListParser` is an iterator that yields `Ok(_)` for a rule or `Err(())` for an invalid one. +impl<'i, 't, 'a, R, P, E: 'i> Iterator for StyleSheetParser<'i, 't, 'a, P> +where + P: QualifiedRuleParser<'i, QualifiedRule = R, Error = E> + + AtRuleParser<'i, AtRule = R, Error = E>, +{ + type Item = Result<R, (ParseError<'i, E>, &'i str)>; + + fn next(&mut self) -> Option<Self::Item> { + loop { + self.input.skip_cdc_and_cdo(); + let start = self.input.state(); + let at_keyword = match self.input.next_byte()? { + b'@' => match self.input.next_including_whitespace_and_comments() { + Ok(&Token::AtKeyword(ref name)) => Some(name.clone()), + _ => { + self.input.reset(&start); + None + } + }, + _ => None, + }; + + if let Some(name) = at_keyword { + let first_stylesheet_rule = !self.any_rule_so_far; + self.any_rule_so_far = true; + if first_stylesheet_rule && name.eq_ignore_ascii_case("charset") { + let delimiters = Delimiter::Semicolon | Delimiter::CurlyBracketBlock; + let _: Result<(), ParseError<()>> = + self.input.parse_until_after(delimiters, |_| Ok(())); + } else { + return Some(parse_at_rule( + &start, + name.clone(), + self.input, + &mut *self.parser, + )); + } + } else { + self.any_rule_so_far = true; + let result = parse_qualified_rule( + &start, + self.input, + &mut *self.parser, + Delimiter::CurlyBracketBlock, + ); + return Some(result.map_err(|e| (e, self.input.slice_from(start.position())))); + } + } + } +} + +/// Parse a single declaration, such as an `( /* ... */ )` parenthesis in an `@supports` prelude. +pub fn parse_one_declaration<'i, 't, P, E>( + input: &mut Parser<'i, 't>, + parser: &mut P, +) -> Result<<P as DeclarationParser<'i>>::Declaration, (ParseError<'i, E>, &'i str)> +where + P: DeclarationParser<'i, Error = E>, +{ + let start_position = input.position(); + input + .parse_entirely(|input| { + let name = input.expect_ident()?.clone(); + input.expect_colon()?; + parser.parse_value(name, input) + }) + .map_err(|e| (e, input.slice_from(start_position))) +} + +/// Parse a single rule, such as for CSSOM’s `CSSStyleSheet.insertRule`. +pub fn parse_one_rule<'i, 't, R, P, E>( + input: &mut Parser<'i, 't>, + parser: &mut P, +) -> Result<R, ParseError<'i, E>> +where + P: QualifiedRuleParser<'i, QualifiedRule = R, Error = E> + + AtRuleParser<'i, AtRule = R, Error = E>, +{ + input.parse_entirely(|input| { + input.skip_whitespace(); + let start = input.state(); + let at_keyword = if input.next_byte() == Some(b'@') { + match *input.next_including_whitespace_and_comments()? { + Token::AtKeyword(ref name) => Some(name.clone()), + _ => { + input.reset(&start); + None + } + } + } else { + None + }; + + if let Some(name) = at_keyword { + parse_at_rule(&start, name, input, parser).map_err(|e| e.0) + } else { + parse_qualified_rule(&start, input, parser, Delimiter::CurlyBracketBlock) + } + }) +} + +fn parse_at_rule<'i, 't, P, E>( + start: &ParserState, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + parser: &mut P, +) -> Result<<P as AtRuleParser<'i>>::AtRule, (ParseError<'i, E>, &'i str)> +where + P: AtRuleParser<'i, Error = E>, +{ + let delimiters = Delimiter::Semicolon | Delimiter::CurlyBracketBlock; + let result = parse_until_before(input, delimiters, |input| parser.parse_prelude(name, input)); + match result { + Ok(prelude) => { + let result = match input.next() { + Ok(&Token::Semicolon) | Err(_) => parser + .rule_without_block(prelude, start) + .map_err(|()| input.new_unexpected_token_error(Token::Semicolon)), + Ok(&Token::CurlyBracketBlock) => { + parse_nested_block(input, |input| parser.parse_block(prelude, start, input)) + } + Ok(_) => unreachable!(), + }; + result.map_err(|e| (e, input.slice_from(start.position()))) + } + Err(error) => { + let end_position = input.position(); + match input.next() { + Ok(&Token::CurlyBracketBlock) | Ok(&Token::Semicolon) | Err(_) => {} + _ => unreachable!(), + }; + Err((error, input.slice(start.position()..end_position))) + } + } +} + +fn parse_qualified_rule<'i, 't, P, E>( + start: &ParserState, + input: &mut Parser<'i, 't>, + parser: &mut P, + delimiters: Delimiters, +) -> Result<<P as QualifiedRuleParser<'i>>::QualifiedRule, ParseError<'i, E>> +where + P: QualifiedRuleParser<'i, Error = E>, +{ + let prelude = parse_until_before(input, delimiters, |input| parser.parse_prelude(input)); + input.expect_curly_bracket_block()?; + // Do this here so that we consume the `{` even if the prelude is `Err`. + let prelude = prelude?; + parse_nested_block(input, |input| parser.parse_block(prelude, &start, input)) +} diff --git a/third_party/rust/cssparser/src/serializer.rs b/third_party/rust/cssparser/src/serializer.rs new file mode 100644 index 0000000000..ec970d176a --- /dev/null +++ b/third_party/rust/cssparser/src/serializer.rs @@ -0,0 +1,526 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +use dtoa_short::{self, Notation}; +use itoa; +use std::fmt::{self, Write}; +use std::str; + +use super::Token; + +/// Trait for things the can serialize themselves in CSS syntax. +pub trait ToCss { + /// Serialize `self` in CSS syntax, writing to `dest`. + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write; + + /// Serialize `self` in CSS syntax and return a string. + /// + /// (This is a convenience wrapper for `to_css` and probably should not be overridden.) + #[inline] + fn to_css_string(&self) -> String { + let mut s = String::new(); + self.to_css(&mut s).unwrap(); + s + } +} + +#[inline] +fn write_numeric<W>(value: f32, int_value: Option<i32>, has_sign: bool, dest: &mut W) -> fmt::Result +where + W: fmt::Write, +{ + // `value.value >= 0` is true for negative 0. + if has_sign && value.is_sign_positive() { + dest.write_str("+")?; + } + + let notation = if value == 0.0 && value.is_sign_negative() { + // Negative zero. Work around #20596. + dest.write_str("-0")?; + Notation { + decimal_point: false, + scientific: false, + } + } else { + dtoa_short::write(dest, value)? + }; + + if int_value.is_none() && value.fract() == 0. { + if !notation.decimal_point && !notation.scientific { + dest.write_str(".0")?; + } + } + Ok(()) +} + +impl<'a> ToCss for Token<'a> { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + match *self { + Token::Ident(ref value) => serialize_identifier(&**value, dest)?, + Token::AtKeyword(ref value) => { + dest.write_str("@")?; + serialize_identifier(&**value, dest)?; + } + Token::Hash(ref value) => { + dest.write_str("#")?; + serialize_name(value, dest)?; + } + Token::IDHash(ref value) => { + dest.write_str("#")?; + serialize_identifier(&**value, dest)?; + } + Token::QuotedString(ref value) => serialize_string(&**value, dest)?, + Token::UnquotedUrl(ref value) => { + dest.write_str("url(")?; + serialize_unquoted_url(&**value, dest)?; + dest.write_str(")")?; + } + Token::Delim(value) => dest.write_char(value)?, + + Token::Number { + value, + int_value, + has_sign, + } => write_numeric(value, int_value, has_sign, dest)?, + Token::Percentage { + unit_value, + int_value, + has_sign, + } => { + write_numeric(unit_value * 100., int_value, has_sign, dest)?; + dest.write_str("%")?; + } + Token::Dimension { + value, + int_value, + has_sign, + ref unit, + } => { + write_numeric(value, int_value, has_sign, dest)?; + // Disambiguate with scientific notation. + let unit = &**unit; + // TODO(emilio): This doesn't handle e.g. 100E1m, which gets us + // an unit of "E1m"... + if unit == "e" || unit == "E" || unit.starts_with("e-") || unit.starts_with("E-") { + dest.write_str("\\65 ")?; + serialize_name(&unit[1..], dest)?; + } else { + serialize_identifier(unit, dest)?; + } + } + + Token::WhiteSpace(content) => dest.write_str(content)?, + Token::Comment(content) => { + dest.write_str("/*")?; + dest.write_str(content)?; + dest.write_str("*/")? + } + Token::Colon => dest.write_str(":")?, + Token::Semicolon => dest.write_str(";")?, + Token::Comma => dest.write_str(",")?, + Token::IncludeMatch => dest.write_str("~=")?, + Token::DashMatch => dest.write_str("|=")?, + Token::PrefixMatch => dest.write_str("^=")?, + Token::SuffixMatch => dest.write_str("$=")?, + Token::SubstringMatch => dest.write_str("*=")?, + Token::CDO => dest.write_str("<!--")?, + Token::CDC => dest.write_str("-->")?, + + Token::Function(ref name) => { + serialize_identifier(&**name, dest)?; + dest.write_str("(")?; + } + Token::ParenthesisBlock => dest.write_str("(")?, + Token::SquareBracketBlock => dest.write_str("[")?, + Token::CurlyBracketBlock => dest.write_str("{")?, + + Token::BadUrl(ref contents) => { + dest.write_str("url(")?; + dest.write_str(contents)?; + dest.write_char(')')?; + } + Token::BadString(ref value) => { + // During tokenization, an unescaped newline after a quote causes + // the token to be a BadString instead of a QuotedString. + // The BadString token ends just before the newline + // (which is in a separate WhiteSpace token), + // and therefore does not have a closing quote. + dest.write_char('"')?; + CssStringWriter::new(dest).write_str(value)?; + } + Token::CloseParenthesis => dest.write_str(")")?, + Token::CloseSquareBracket => dest.write_str("]")?, + Token::CloseCurlyBracket => dest.write_str("}")?, + } + Ok(()) + } +} + +fn hex_escape<W>(ascii_byte: u8, dest: &mut W) -> fmt::Result +where + W: fmt::Write, +{ + static HEX_DIGITS: &'static [u8; 16] = b"0123456789abcdef"; + let b3; + let b4; + let bytes = if ascii_byte > 0x0F { + let high = (ascii_byte >> 4) as usize; + let low = (ascii_byte & 0x0F) as usize; + b4 = [b'\\', HEX_DIGITS[high], HEX_DIGITS[low], b' ']; + &b4[..] + } else { + b3 = [b'\\', HEX_DIGITS[ascii_byte as usize], b' ']; + &b3[..] + }; + dest.write_str(unsafe { str::from_utf8_unchecked(&bytes) }) +} + +fn char_escape<W>(ascii_byte: u8, dest: &mut W) -> fmt::Result +where + W: fmt::Write, +{ + let bytes = [b'\\', ascii_byte]; + dest.write_str(unsafe { str::from_utf8_unchecked(&bytes) }) +} + +/// Write a CSS identifier, escaping characters as necessary. +pub fn serialize_identifier<W>(mut value: &str, dest: &mut W) -> fmt::Result +where + W: fmt::Write, +{ + if value.is_empty() { + return Ok(()); + } + + if value.starts_with("--") { + dest.write_str("--")?; + serialize_name(&value[2..], dest) + } else if value == "-" { + dest.write_str("\\-") + } else { + if value.as_bytes()[0] == b'-' { + dest.write_str("-")?; + value = &value[1..]; + } + if let digit @ b'0'..=b'9' = value.as_bytes()[0] { + hex_escape(digit, dest)?; + value = &value[1..]; + } + serialize_name(value, dest) + } +} + +/// Write a CSS name, like a custom property name. +/// +/// You should only use this when you know what you're doing, when in doubt, +/// consider using `serialize_identifier`. +pub fn serialize_name<W>(value: &str, dest: &mut W) -> fmt::Result +where + W: fmt::Write, +{ + let mut chunk_start = 0; + for (i, b) in value.bytes().enumerate() { + let escaped = match b { + b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_' | b'-' => continue, + _ if !b.is_ascii() => continue, + b'\0' => Some("\u{FFFD}"), + _ => None, + }; + dest.write_str(&value[chunk_start..i])?; + if let Some(escaped) = escaped { + dest.write_str(escaped)?; + } else if (b >= b'\x01' && b <= b'\x1F') || b == b'\x7F' { + hex_escape(b, dest)?; + } else { + char_escape(b, dest)?; + } + chunk_start = i + 1; + } + dest.write_str(&value[chunk_start..]) +} + +fn serialize_unquoted_url<W>(value: &str, dest: &mut W) -> fmt::Result +where + W: fmt::Write, +{ + let mut chunk_start = 0; + for (i, b) in value.bytes().enumerate() { + let hex = match b { + b'\0'..=b' ' | b'\x7F' => true, + b'(' | b')' | b'"' | b'\'' | b'\\' => false, + _ => continue, + }; + dest.write_str(&value[chunk_start..i])?; + if hex { + hex_escape(b, dest)?; + } else { + char_escape(b, dest)?; + } + chunk_start = i + 1; + } + dest.write_str(&value[chunk_start..]) +} + +/// Write a double-quoted CSS string token, escaping content as necessary. +pub fn serialize_string<W>(value: &str, dest: &mut W) -> fmt::Result +where + W: fmt::Write, +{ + dest.write_str("\"")?; + CssStringWriter::new(dest).write_str(value)?; + dest.write_str("\"")?; + Ok(()) +} + +/// A `fmt::Write` adapter that escapes text for writing as a double-quoted CSS string. +/// Quotes are not included. +/// +/// Typical usage: +/// +/// ```{rust,ignore} +/// fn write_foo<W>(foo: &Foo, dest: &mut W) -> fmt::Result where W: fmt::Write { +/// dest.write_str("\"")?; +/// { +/// let mut string_dest = CssStringWriter::new(dest); +/// // Write into string_dest... +/// } +/// dest.write_str("\"")?; +/// Ok(()) +/// } +/// ``` +pub struct CssStringWriter<'a, W> { + inner: &'a mut W, +} + +impl<'a, W> CssStringWriter<'a, W> +where + W: fmt::Write, +{ + /// Wrap a text writer to create a `CssStringWriter`. + pub fn new(inner: &'a mut W) -> CssStringWriter<'a, W> { + CssStringWriter { inner: inner } + } +} + +impl<'a, W> fmt::Write for CssStringWriter<'a, W> +where + W: fmt::Write, +{ + fn write_str(&mut self, s: &str) -> fmt::Result { + let mut chunk_start = 0; + for (i, b) in s.bytes().enumerate() { + let escaped = match b { + b'"' => Some("\\\""), + b'\\' => Some("\\\\"), + b'\0' => Some("\u{FFFD}"), + b'\x01'..=b'\x1F' | b'\x7F' => None, + _ => continue, + }; + self.inner.write_str(&s[chunk_start..i])?; + match escaped { + Some(x) => self.inner.write_str(x)?, + None => hex_escape(b, self.inner)?, + }; + chunk_start = i + 1; + } + self.inner.write_str(&s[chunk_start..]) + } +} + +macro_rules! impl_tocss_for_int { + ($T: ty) => { + impl<'a> ToCss for $T { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + let mut buf = itoa::Buffer::new(); + dest.write_str(buf.format(*self)) + } + } + }; +} + +impl_tocss_for_int!(i8); +impl_tocss_for_int!(u8); +impl_tocss_for_int!(i16); +impl_tocss_for_int!(u16); +impl_tocss_for_int!(i32); +impl_tocss_for_int!(u32); +impl_tocss_for_int!(i64); +impl_tocss_for_int!(u64); + +macro_rules! impl_tocss_for_float { + ($T: ty) => { + impl<'a> ToCss for $T { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + dtoa_short::write(dest, *self).map(|_| ()) + } + } + }; +} + +impl_tocss_for_float!(f32); +impl_tocss_for_float!(f64); + +/// A category of token. See the `needs_separator_when_before` method. +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct TokenSerializationType(TokenSerializationTypeVariants); + +impl TokenSerializationType { + /// Return a value that represents the absence of a token, e.g. before the start of the input. + pub fn nothing() -> TokenSerializationType { + TokenSerializationType(TokenSerializationTypeVariants::Nothing) + } + + /// If this value is `TokenSerializationType::nothing()`, set it to the given value instead. + pub fn set_if_nothing(&mut self, new_value: TokenSerializationType) { + if self.0 == TokenSerializationTypeVariants::Nothing { + self.0 = new_value.0 + } + } + + /// Return true if, when a token of category `self` is serialized just before + /// a token of category `other` with no whitespace in between, + /// an empty comment `/**/` needs to be inserted between them + /// so that they are not re-parsed as a single token. + /// + /// See https://drafts.csswg.org/css-syntax/#serialization + /// + /// See https://github.com/w3c/csswg-drafts/issues/4088 for the + /// `DelimPercent` bits. + pub fn needs_separator_when_before(self, other: TokenSerializationType) -> bool { + use self::TokenSerializationTypeVariants::*; + match self.0 { + Ident => matches!( + other.0, + Ident + | Function + | UrlOrBadUrl + | DelimMinus + | Number + | Percentage + | Dimension + | CDC + | OpenParen + ), + AtKeywordOrHash | Dimension => matches!( + other.0, + Ident | Function | UrlOrBadUrl | DelimMinus | Number | Percentage | Dimension | CDC + ), + DelimHash | DelimMinus => matches!( + other.0, + Ident | Function | UrlOrBadUrl | DelimMinus | Number | Percentage | Dimension + ), + Number => matches!( + other.0, + Ident + | Function + | UrlOrBadUrl + | DelimMinus + | Number + | Percentage + | DelimPercent + | Dimension + ), + DelimAt => matches!(other.0, Ident | Function | UrlOrBadUrl | DelimMinus), + DelimDotOrPlus => matches!(other.0, Number | Percentage | Dimension), + DelimAssorted | DelimAsterisk => matches!(other.0, DelimEquals), + DelimBar => matches!(other.0, DelimEquals | DelimBar | DashMatch), + DelimSlash => matches!(other.0, DelimAsterisk | SubstringMatch), + Nothing | WhiteSpace | Percentage | UrlOrBadUrl | Function | CDC | OpenParen + | DashMatch | SubstringMatch | DelimQuestion | DelimEquals | DelimPercent | Other => { + false + } + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +enum TokenSerializationTypeVariants { + Nothing, + WhiteSpace, + AtKeywordOrHash, + Number, + Dimension, + Percentage, + UrlOrBadUrl, + Function, + Ident, + CDC, + DashMatch, + SubstringMatch, + OpenParen, // '(' + DelimHash, // '#' + DelimAt, // '@' + DelimDotOrPlus, // '.', '+' + DelimMinus, // '-' + DelimQuestion, // '?' + DelimAssorted, // '$', '^', '~' + DelimEquals, // '=' + DelimBar, // '|' + DelimSlash, // '/' + DelimAsterisk, // '*' + DelimPercent, // '%' + Other, // anything else +} + +impl<'a> Token<'a> { + /// Categorize a token into a type that determines when `/**/` needs to be inserted + /// between two tokens when serialized next to each other without whitespace in between. + /// + /// See the `TokenSerializationType::needs_separator_when_before` method. + pub fn serialization_type(&self) -> TokenSerializationType { + use self::TokenSerializationTypeVariants::*; + TokenSerializationType(match *self { + Token::Ident(_) => Ident, + Token::AtKeyword(_) | Token::Hash(_) | Token::IDHash(_) => AtKeywordOrHash, + Token::UnquotedUrl(_) | Token::BadUrl(_) => UrlOrBadUrl, + Token::Delim('#') => DelimHash, + Token::Delim('@') => DelimAt, + Token::Delim('.') | Token::Delim('+') => DelimDotOrPlus, + Token::Delim('-') => DelimMinus, + Token::Delim('?') => DelimQuestion, + Token::Delim('$') | Token::Delim('^') | Token::Delim('~') => DelimAssorted, + Token::Delim('%') => DelimPercent, + Token::Delim('=') => DelimEquals, + Token::Delim('|') => DelimBar, + Token::Delim('/') => DelimSlash, + Token::Delim('*') => DelimAsterisk, + Token::Number { .. } => Number, + Token::Percentage { .. } => Percentage, + Token::Dimension { .. } => Dimension, + Token::WhiteSpace(_) => WhiteSpace, + Token::Comment(_) => DelimSlash, + Token::DashMatch => DashMatch, + Token::SubstringMatch => SubstringMatch, + Token::CDC => CDC, + Token::Function(_) => Function, + Token::ParenthesisBlock => OpenParen, + Token::SquareBracketBlock + | Token::CurlyBracketBlock + | Token::CloseParenthesis + | Token::CloseSquareBracket + | Token::CloseCurlyBracket + | Token::QuotedString(_) + | Token::BadString(_) + | Token::Delim(_) + | Token::Colon + | Token::Semicolon + | Token::Comma + | Token::CDO + | Token::IncludeMatch + | Token::PrefixMatch + | Token::SuffixMatch => Other, + }) + } +} diff --git a/third_party/rust/cssparser/src/size_of_tests.rs b/third_party/rust/cssparser/src/size_of_tests.rs new file mode 100644 index 0000000000..edd2b439f0 --- /dev/null +++ b/third_party/rust/cssparser/src/size_of_tests.rs @@ -0,0 +1,52 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +use crate::cow_rc_str::CowRcStr; +use crate::tokenizer::Token; + +macro_rules! size_of_test { + ($testname: ident, $t: ty, $expected_min_size: expr, $expected_max_size: expr) => { + #[test] + fn $testname() { + let new = ::std::mem::size_of::<$t>(); + if new < $expected_min_size { + panic!( + "Your changes have decreased the stack size of {} from {} to {}. \ + Good work! Please update the expected size in {}.", + stringify!($t), + $expected_min_size, + new, + file!() + ) + } else if new > $expected_max_size { + panic!( + "Your changes have increased the stack size of {} from {} to {}. \ + Please consider choosing a design which avoids this increase. \ + If you feel that the increase is necessary, update the size in {}.", + stringify!($t), + $expected_max_size, + new, + file!() + ) + } + } + }; + ($testname: ident, $t: ty, $expected_size: expr) => { + size_of_test!($testname, $t, $expected_size, $expected_size); + }; +} + +// Some of these assume 64-bit +size_of_test!(token, Token, 32); +size_of_test!(std_cow_str, std::borrow::Cow<'static, str>, 24, 32); +size_of_test!(cow_rc_str, CowRcStr, 16); + +size_of_test!(tokenizer, crate::tokenizer::Tokenizer, 72); +size_of_test!(parser_input, crate::parser::ParserInput, 136); +size_of_test!(parser, crate::parser::Parser, 16); +size_of_test!(source_position, crate::SourcePosition, 8); +size_of_test!(parser_state, crate::ParserState, 24); + +size_of_test!(basic_parse_error, crate::BasicParseError, 40, 48); +size_of_test!(parse_error_lower_bound, crate::ParseError<()>, 40, 48); diff --git a/third_party/rust/cssparser/src/tests.rs b/third_party/rust/cssparser/src/tests.rs new file mode 100644 index 0000000000..e8f94a4053 --- /dev/null +++ b/third_party/rust/cssparser/src/tests.rs @@ -0,0 +1,1670 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +#[cfg(feature = "bench")] +extern crate test; + +use encoding_rs; +use serde_json::{self, json, Map, Value}; + +use crate::color::{parse_color_with, FromParsedColor}; +use crate::{ColorParser, PredefinedColorSpace}; + +#[cfg(feature = "bench")] +use self::test::Bencher; + +use super::{ + parse_important, parse_nth, parse_one_declaration, parse_one_rule, stylesheet_encoding, + AtRuleParser, BasicParseError, BasicParseErrorKind, Color, CowRcStr, DeclarationParser, + Delimiter, EncodingSupport, ParseError, ParseErrorKind, Parser, ParserInput, ParserState, + QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, SourceLocation, StyleSheetParser, + ToCss, Token, TokenSerializationType, UnicodeRange, RGBA, +}; + +macro_rules! JArray { + ($($e: expr,)*) => { JArray![ $( $e ),* ] }; + ($($e: expr),*) => { Value::Array(vec!( $( $e.to_json() ),* )) } +} + +fn almost_equals(a: &Value, b: &Value) -> bool { + match (a, b) { + (&Value::Number(ref a), &Value::Number(ref b)) => { + let a = a.as_f64().unwrap(); + let b = b.as_f64().unwrap(); + (a - b).abs() <= a.abs() * 1e-6 + } + + (&Value::Bool(a), &Value::Bool(b)) => a == b, + (&Value::String(ref a), &Value::String(ref b)) => a == b, + (&Value::Array(ref a), &Value::Array(ref b)) => { + a.len() == b.len() + && a.iter() + .zip(b.iter()) + .all(|(ref a, ref b)| almost_equals(*a, *b)) + } + (&Value::Object(_), &Value::Object(_)) => panic!("Not implemented"), + (&Value::Null, &Value::Null) => true, + _ => false, + } +} + +fn normalize(json: &mut Value) { + match *json { + Value::Array(ref mut list) => { + for item in list.iter_mut() { + normalize(item) + } + } + Value::String(ref mut s) => { + if *s == "extra-input" || *s == "empty" { + *s = "invalid".to_string() + } + } + _ => {} + } +} + +fn assert_json_eq(results: Value, mut expected: Value, message: &str) { + normalize(&mut expected); + if !almost_equals(&results, &expected) { + println!( + "{}", + ::difference::Changeset::new( + &serde_json::to_string_pretty(&results).unwrap(), + &serde_json::to_string_pretty(&expected).unwrap(), + "\n", + ) + ); + panic!("{}", message) + } +} + +fn run_raw_json_tests<F: Fn(Value, Value) -> ()>(json_data: &str, run: F) { + let items = match serde_json::from_str(json_data) { + Ok(Value::Array(items)) => items, + other => panic!("Invalid JSON: {:?}", other), + }; + assert!(items.len() % 2 == 0); + let mut input = None; + for item in items.into_iter() { + match (&input, item) { + (&None, json_obj) => input = Some(json_obj), + (&Some(_), expected) => { + let input = input.take().unwrap(); + run(input, expected) + } + }; + } +} + +fn run_json_tests<F: Fn(&mut Parser) -> Value>(json_data: &str, parse: F) { + run_raw_json_tests(json_data, |input, expected| match input { + Value::String(input) => { + let mut parse_input = ParserInput::new(&input); + let result = parse(&mut Parser::new(&mut parse_input)); + assert_json_eq(result, expected, &input); + } + _ => panic!("Unexpected JSON"), + }); +} + +#[test] +fn component_value_list() { + run_json_tests( + include_str!("css-parsing-tests/component_value_list.json"), + |input| Value::Array(component_values_to_json(input)), + ); +} + +#[test] +fn one_component_value() { + run_json_tests( + include_str!("css-parsing-tests/one_component_value.json"), + |input| { + let result: Result<Value, ParseError<()>> = input.parse_entirely(|input| { + Ok(one_component_value_to_json(input.next()?.clone(), input)) + }); + result.unwrap_or(JArray!["error", "invalid"]) + }, + ); +} + +#[test] +fn declaration_list() { + run_json_tests( + include_str!("css-parsing-tests/declaration_list.json"), + |input| { + Value::Array( + RuleBodyParser::new(input, &mut JsonParser) + .map(|result| result.unwrap_or(JArray!["error", "invalid"])) + .collect(), + ) + }, + ); +} + +#[test] +fn one_declaration() { + run_json_tests( + include_str!("css-parsing-tests/one_declaration.json"), + |input| { + parse_one_declaration(input, &mut JsonParser).unwrap_or(JArray!["error", "invalid"]) + }, + ); +} + +#[test] +fn rule_list() { + run_json_tests(include_str!("css-parsing-tests/rule_list.json"), |input| { + Value::Array( + RuleBodyParser::new(input, &mut JsonParser) + .map(|result| result.unwrap_or(JArray!["error", "invalid"])) + .collect(), + ) + }); +} + +#[test] +fn stylesheet() { + run_json_tests(include_str!("css-parsing-tests/stylesheet.json"), |input| { + Value::Array( + StyleSheetParser::new(input, &mut JsonParser) + .map(|result| result.unwrap_or(JArray!["error", "invalid"])) + .collect(), + ) + }); +} + +#[test] +fn one_rule() { + run_json_tests(include_str!("css-parsing-tests/one_rule.json"), |input| { + parse_one_rule(input, &mut JsonParser).unwrap_or(JArray!["error", "invalid"]) + }); +} + +#[test] +fn stylesheet_from_bytes() { + pub struct EncodingRs; + + impl EncodingSupport for EncodingRs { + type Encoding = &'static encoding_rs::Encoding; + + fn utf8() -> Self::Encoding { + encoding_rs::UTF_8 + } + + fn is_utf16_be_or_le(encoding: &Self::Encoding) -> bool { + *encoding == encoding_rs::UTF_16LE || *encoding == encoding_rs::UTF_16BE + } + + fn from_label(ascii_label: &[u8]) -> Option<Self::Encoding> { + encoding_rs::Encoding::for_label(ascii_label) + } + } + + run_raw_json_tests( + include_str!("css-parsing-tests/stylesheet_bytes.json"), + |input, expected| { + let map = match input { + Value::Object(map) => map, + _ => panic!("Unexpected JSON"), + }; + + let result = { + let css = get_string(&map, "css_bytes") + .unwrap() + .chars() + .map(|c| { + assert!(c as u32 <= 0xFF); + c as u8 + }) + .collect::<Vec<u8>>(); + let protocol_encoding_label = + get_string(&map, "protocol_encoding").map(|s| s.as_bytes()); + let environment_encoding = get_string(&map, "environment_encoding") + .map(|s| s.as_bytes()) + .and_then(EncodingRs::from_label); + + let encoding = stylesheet_encoding::<EncodingRs>( + &css, + protocol_encoding_label, + environment_encoding, + ); + let (css_unicode, used_encoding, _) = encoding.decode(&css); + let mut input = ParserInput::new(&css_unicode); + let input = &mut Parser::new(&mut input); + let rules = StyleSheetParser::new(input, &mut JsonParser) + .map(|result| result.unwrap_or(JArray!["error", "invalid"])) + .collect::<Vec<_>>(); + JArray![rules, used_encoding.name().to_lowercase()] + }; + assert_json_eq(result, expected, &Value::Object(map).to_string()); + }, + ); + + fn get_string<'a>(map: &'a Map<String, Value>, key: &str) -> Option<&'a str> { + match map.get(key) { + Some(&Value::String(ref s)) => Some(s), + Some(&Value::Null) => None, + None => None, + _ => panic!("Unexpected JSON"), + } + } +} + +#[test] +fn expect_no_error_token() { + let mut input = ParserInput::new("foo 4px ( / { !bar }"); + assert!(Parser::new(&mut input).expect_no_error_token().is_ok()); + let mut input = ParserInput::new(")"); + assert!(Parser::new(&mut input).expect_no_error_token().is_err()); + let mut input = ParserInput::new("}"); + assert!(Parser::new(&mut input).expect_no_error_token().is_err()); + let mut input = ParserInput::new("(a){]"); + assert!(Parser::new(&mut input).expect_no_error_token().is_err()); + let mut input = ParserInput::new("'\n'"); + assert!(Parser::new(&mut input).expect_no_error_token().is_err()); + let mut input = ParserInput::new("url('\n'"); + assert!(Parser::new(&mut input).expect_no_error_token().is_err()); + let mut input = ParserInput::new("url(a b)"); + assert!(Parser::new(&mut input).expect_no_error_token().is_err()); + let mut input = ParserInput::new("url(\u{7F}))"); + assert!(Parser::new(&mut input).expect_no_error_token().is_err()); +} + +/// https://github.com/servo/rust-cssparser/issues/71 +#[test] +fn outer_block_end_consumed() { + let mut input = ParserInput::new("(calc(true))"); + let mut input = Parser::new(&mut input); + assert!(input.expect_parenthesis_block().is_ok()); + assert!(input + .parse_nested_block(|input| input + .expect_function_matching("calc") + .map_err(Into::<ParseError<()>>::into)) + .is_ok()); + println!("{:?}", input.position()); + assert!(input.next().is_err()); +} + +/// https://github.com/servo/rust-cssparser/issues/174 +#[test] +fn bad_url_slice_out_of_bounds() { + let mut input = ParserInput::new("url(\u{1}\\"); + let mut parser = Parser::new(&mut input); + let result = parser.next_including_whitespace_and_comments(); // This used to panic + assert_eq!(result, Ok(&Token::BadUrl("\u{1}\\".into()))); +} + +/// https://bugzilla.mozilla.org/show_bug.cgi?id=1383975 +#[test] +fn bad_url_slice_not_at_char_boundary() { + let mut input = ParserInput::new("url(9\n۰"); + let mut parser = Parser::new(&mut input); + let result = parser.next_including_whitespace_and_comments(); // This used to panic + assert_eq!(result, Ok(&Token::BadUrl("9\n۰".into()))); +} + +#[test] +fn unquoted_url_escaping() { + let token = Token::UnquotedUrl( + "\ + \x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\ + \x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f \ + !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]\ + ^_`abcdefghijklmnopqrstuvwxyz{|}~\x7fé\ + " + .into(), + ); + let serialized = token.to_css_string(); + assert_eq!( + serialized, + "\ + url(\ + \\1 \\2 \\3 \\4 \\5 \\6 \\7 \\8 \\9 \\a \\b \\c \\d \\e \\f \\10 \ + \\11 \\12 \\13 \\14 \\15 \\16 \\17 \\18 \\19 \\1a \\1b \\1c \\1d \\1e \\1f \\20 \ + !\\\"#$%&\\'\\(\\)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]\ + ^_`abcdefghijklmnopqrstuvwxyz{|}~\\7f é\ + )\ + " + ); + let mut input = ParserInput::new(&serialized); + assert_eq!(Parser::new(&mut input).next(), Ok(&token)); +} + +#[test] +fn test_expect_url() { + fn parse<'a>(s: &mut ParserInput<'a>) -> Result<CowRcStr<'a>, BasicParseError<'a>> { + Parser::new(s).expect_url() + } + let mut input = ParserInput::new("url()"); + assert_eq!(parse(&mut input).unwrap(), ""); + let mut input = ParserInput::new("url( "); + assert_eq!(parse(&mut input).unwrap(), ""); + let mut input = ParserInput::new("url( abc"); + assert_eq!(parse(&mut input).unwrap(), "abc"); + let mut input = ParserInput::new("url( abc \t)"); + assert_eq!(parse(&mut input).unwrap(), "abc"); + let mut input = ParserInput::new("url( 'abc' \t)"); + assert_eq!(parse(&mut input).unwrap(), "abc"); + let mut input = ParserInput::new("url(abc more stuff)"); + assert!(parse(&mut input).is_err()); + // The grammar at https://drafts.csswg.org/css-values/#urls plans for `<url-modifier>*` + // at the position of "more stuff", but no such modifier is defined yet. + let mut input = ParserInput::new("url('abc' more stuff)"); + assert!(parse(&mut input).is_err()); +} + +fn run_color_tests<F: Fn(Result<Color, ()>) -> Value>(json_data: &str, to_json: F) { + run_json_tests(json_data, |input| { + let result: Result<_, ParseError<()>> = + input.parse_entirely(|i| Color::parse(i).map_err(Into::into)); + to_json(result.map_err(|_| ())) + }); +} + +#[test] +fn color3() { + run_color_tests(include_str!("css-parsing-tests/color3.json"), |c| { + c.ok() + .map(|v| v.to_css_string().to_json()) + .unwrap_or(Value::Null) + }) +} + +#[test] +fn color3_hsl() { + run_color_tests(include_str!("css-parsing-tests/color3_hsl.json"), |c| { + c.ok() + .map(|v| v.to_css_string().to_json()) + .unwrap_or(Value::Null) + }) +} + +/// color3_keywords.json is different: R, G and B are in 0..255 rather than 0..1 +#[test] +fn color3_keywords() { + run_color_tests( + include_str!("css-parsing-tests/color3_keywords.json"), + |c| { + c.ok() + .map(|v| v.to_css_string().to_json()) + .unwrap_or(Value::Null) + }, + ) +} + +#[test] +fn color4_hwb() { + run_color_tests(include_str!("css-parsing-tests/color4_hwb.json"), |c| { + c.ok() + .map(|v| v.to_css_string().to_json()) + .unwrap_or(Value::Null) + }) +} + +#[test] +fn color4_lab_lch_oklab_oklch() { + run_color_tests( + include_str!("css-parsing-tests/color4_lab_lch_oklab_oklch.json"), + |c| { + c.ok() + .map(|v| v.to_css_string().to_json()) + .unwrap_or(Value::Null) + }, + ) +} + +#[test] +fn color4_color_function() { + run_color_tests( + include_str!("css-parsing-tests/color4_color_function.json"), + |c| { + c.ok() + .map(|v| v.to_css_string().to_json()) + .unwrap_or(Value::Null) + }, + ) +} + +macro_rules! parse_single_color { + ($i:expr) => {{ + let input = $i; + let mut input = ParserInput::new(input); + let mut input = Parser::new(&mut input); + Color::parse(&mut input).map_err(Into::<ParseError<()>>::into) + }}; +} + +#[test] +fn color4_invalid_color_space() { + let result = parse_single_color!("color(invalid 1 1 1)"); + assert!(result.is_err()); +} + +#[test] +fn nth() { + run_json_tests(include_str!("css-parsing-tests/An+B.json"), |input| { + input + .parse_entirely(|i| { + let result: Result<_, ParseError<()>> = parse_nth(i).map_err(Into::into); + result + }) + .ok() + .map(|(v0, v1)| json!([v0, v1])) + .unwrap_or(Value::Null) + }); +} + +#[test] +fn parse_comma_separated_ignoring_errors() { + let input = "red, green something, yellow, whatever, blue"; + let mut input = ParserInput::new(input); + let mut input = Parser::new(&mut input); + let result = input.parse_comma_separated_ignoring_errors(|input| { + Color::parse(input).map_err(Into::<ParseError<()>>::into) + }); + assert_eq!(result.len(), 3); + assert_eq!(result[0].to_css_string(), "rgb(255, 0, 0)"); + assert_eq!(result[1].to_css_string(), "rgb(255, 255, 0)"); + assert_eq!(result[2].to_css_string(), "rgb(0, 0, 255)"); +} + +#[test] +fn unicode_range() { + run_json_tests(include_str!("css-parsing-tests/urange.json"), |input| { + let result: Result<_, ParseError<()>> = input.parse_comma_separated(|input| { + let result = UnicodeRange::parse(input).ok().map(|r| (r.start, r.end)); + if input.is_exhausted() { + Ok(result) + } else { + while let Ok(_) = input.next() {} + Ok(None) + } + }); + result + .unwrap() + .iter() + .map(|v| { + if let Some((v0, v1)) = v { + json!([v0, v1]) + } else { + Value::Null + } + }) + .collect::<Vec<_>>() + .to_json() + }); +} + +#[test] +fn serializer_not_preserving_comments() { + serializer(false) +} + +#[test] +fn serializer_preserving_comments() { + serializer(true) +} + +fn serializer(preserve_comments: bool) { + run_json_tests( + include_str!("css-parsing-tests/component_value_list.json"), + |input| { + fn write_to( + mut previous_token: TokenSerializationType, + input: &mut Parser, + string: &mut String, + preserve_comments: bool, + ) { + while let Ok(token) = if preserve_comments { + input + .next_including_whitespace_and_comments() + .map(|t| t.clone()) + } else { + input.next_including_whitespace().map(|t| t.clone()) + } { + let token_type = token.serialization_type(); + if !preserve_comments && previous_token.needs_separator_when_before(token_type) + { + string.push_str("/**/") + } + previous_token = token_type; + token.to_css(string).unwrap(); + let closing_token = match token { + Token::Function(_) | Token::ParenthesisBlock => { + Some(Token::CloseParenthesis) + } + Token::SquareBracketBlock => Some(Token::CloseSquareBracket), + Token::CurlyBracketBlock => Some(Token::CloseCurlyBracket), + _ => None, + }; + if let Some(closing_token) = closing_token { + let result: Result<_, ParseError<()>> = input.parse_nested_block(|input| { + write_to(previous_token, input, string, preserve_comments); + Ok(()) + }); + result.unwrap(); + closing_token.to_css(string).unwrap(); + } + } + } + let mut serialized = String::new(); + write_to( + TokenSerializationType::nothing(), + input, + &mut serialized, + preserve_comments, + ); + let mut input = ParserInput::new(&serialized); + let parser = &mut Parser::new(&mut input); + Value::Array(component_values_to_json(parser)) + }, + ); +} + +#[test] +fn serialize_bad_tokens() { + let mut input = ParserInput::new("url(foo\\) b\\)ar)'ba\\'\"z\n4"); + let mut parser = Parser::new(&mut input); + + let token = parser.next().unwrap().clone(); + assert!(matches!(token, Token::BadUrl(_))); + assert_eq!(token.to_css_string(), "url(foo\\) b\\)ar)"); + + let token = parser.next().unwrap().clone(); + assert!(matches!(token, Token::BadString(_))); + assert_eq!(token.to_css_string(), "\"ba'\\\"z"); + + let token = parser.next().unwrap().clone(); + assert!(matches!(token, Token::Number { .. })); + assert_eq!(token.to_css_string(), "4"); + + assert!(parser.next().is_err()); +} + +#[test] +fn serialize_current_color() { + let c = Color::CurrentColor; + assert!(c.to_css_string() == "currentcolor"); +} + +#[test] +fn serialize_rgb_full_alpha() { + let c = Color::Rgba(RGBA::new(Some(255), Some(230), Some(204), Some(1.0))); + assert_eq!(c.to_css_string(), "rgb(255, 230, 204)"); +} + +#[test] +fn serialize_rgba() { + let c = Color::Rgba(RGBA::new(Some(26), Some(51), Some(77), Some(0.125))); + assert_eq!(c.to_css_string(), "rgba(26, 51, 77, 0.125)"); +} + +#[test] +fn serialize_rgba_two_digit_float_if_roundtrips() { + let c = Color::Rgba(RGBA::from_floats(Some(0.), Some(0.), Some(0.), Some(0.5))); + assert_eq!(c.to_css_string(), "rgba(0, 0, 0, 0.5)"); +} + +#[test] +fn line_numbers() { + let mut input = ParserInput::new(concat!( + "fo\\30\r\n", + "0o bar/*\n", + "*/baz\r\n", + "\n", + "url(\r\n", + " u \r\n", + ")\"a\\\r\n", + "b\"" + )); + let mut input = Parser::new(&mut input); + assert_eq!( + input.current_source_location(), + SourceLocation { line: 0, column: 1 } + ); + assert_eq!( + input.next_including_whitespace(), + Ok(&Token::Ident("fo00o".into())) + ); + assert_eq!( + input.current_source_location(), + SourceLocation { line: 1, column: 3 } + ); + assert_eq!( + input.next_including_whitespace(), + Ok(&Token::WhiteSpace(" ")) + ); + assert_eq!( + input.current_source_location(), + SourceLocation { line: 1, column: 4 } + ); + assert_eq!( + input.next_including_whitespace(), + Ok(&Token::Ident("bar".into())) + ); + assert_eq!( + input.current_source_location(), + SourceLocation { line: 1, column: 7 } + ); + assert_eq!( + input.next_including_whitespace_and_comments(), + Ok(&Token::Comment("\n")) + ); + assert_eq!( + input.current_source_location(), + SourceLocation { line: 2, column: 3 } + ); + assert_eq!( + input.next_including_whitespace(), + Ok(&Token::Ident("baz".into())) + ); + assert_eq!( + input.current_source_location(), + SourceLocation { line: 2, column: 6 } + ); + let state = input.state(); + + assert_eq!( + input.next_including_whitespace(), + Ok(&Token::WhiteSpace("\r\n\n")) + ); + assert_eq!( + input.current_source_location(), + SourceLocation { line: 4, column: 1 } + ); + + assert_eq!( + state.source_location(), + SourceLocation { line: 2, column: 6 } + ); + + assert_eq!( + input.next_including_whitespace(), + Ok(&Token::UnquotedUrl("u".into())) + ); + assert_eq!( + input.current_source_location(), + SourceLocation { line: 6, column: 2 } + ); + + assert_eq!( + input.next_including_whitespace(), + Ok(&Token::QuotedString("ab".into())) + ); + assert_eq!( + input.current_source_location(), + SourceLocation { line: 7, column: 3 } + ); + assert!(input.next_including_whitespace().is_err()); +} + +#[test] +fn overflow() { + use std::iter::repeat; + + let css = r" + 2147483646 + 2147483647 + 2147483648 + 10000000000000 + 1000000000000000000000000000000000000000 + 1{309 zeros} + + -2147483647 + -2147483648 + -2147483649 + -10000000000000 + -1000000000000000000000000000000000000000 + -1{309 zeros} + + 3.30282347e+38 + 3.40282347e+38 + 3.402824e+38 + + -3.30282347e+38 + -3.40282347e+38 + -3.402824e+38 + + " + .replace("{309 zeros}", &repeat('0').take(309).collect::<String>()); + let mut input = ParserInput::new(&css); + let mut input = Parser::new(&mut input); + + assert_eq!(input.expect_integer(), Ok(2147483646)); + assert_eq!(input.expect_integer(), Ok(2147483647)); + assert_eq!(input.expect_integer(), Ok(2147483647)); // Clamp on overflow + assert_eq!(input.expect_integer(), Ok(2147483647)); + assert_eq!(input.expect_integer(), Ok(2147483647)); + assert_eq!(input.expect_integer(), Ok(2147483647)); + + assert_eq!(input.expect_integer(), Ok(-2147483647)); + assert_eq!(input.expect_integer(), Ok(-2147483648)); + assert_eq!(input.expect_integer(), Ok(-2147483648)); // Clamp on overflow + assert_eq!(input.expect_integer(), Ok(-2147483648)); + assert_eq!(input.expect_integer(), Ok(-2147483648)); + assert_eq!(input.expect_integer(), Ok(-2147483648)); + + assert_eq!(input.expect_number(), Ok(3.30282347e+38)); + assert_eq!(input.expect_number(), Ok(f32::MAX)); + assert_eq!(input.expect_number(), Ok(f32::INFINITY)); + assert!(f32::MAX != f32::INFINITY); + + assert_eq!(input.expect_number(), Ok(-3.30282347e+38)); + assert_eq!(input.expect_number(), Ok(f32::MIN)); + assert_eq!(input.expect_number(), Ok(f32::NEG_INFINITY)); + assert!(f32::MIN != f32::NEG_INFINITY); +} + +#[test] +fn line_delimited() { + let mut input = ParserInput::new(" { foo ; bar } baz;,"); + let mut input = Parser::new(&mut input); + assert_eq!(input.next(), Ok(&Token::CurlyBracketBlock)); + assert!({ + let result: Result<_, ParseError<()>> = + input.parse_until_after(Delimiter::Semicolon, |_| Ok(42)); + result + } + .is_err()); + assert_eq!(input.next(), Ok(&Token::Comma)); + assert!(input.next().is_err()); +} + +#[test] +fn identifier_serialization() { + // Null bytes + assert_eq!(Token::Ident("\0".into()).to_css_string(), "\u{FFFD}"); + assert_eq!(Token::Ident("a\0".into()).to_css_string(), "a\u{FFFD}"); + assert_eq!(Token::Ident("\0b".into()).to_css_string(), "\u{FFFD}b"); + assert_eq!(Token::Ident("a\0b".into()).to_css_string(), "a\u{FFFD}b"); + + // Replacement character + assert_eq!(Token::Ident("\u{FFFD}".into()).to_css_string(), "\u{FFFD}"); + assert_eq!( + Token::Ident("a\u{FFFD}".into()).to_css_string(), + "a\u{FFFD}" + ); + assert_eq!( + Token::Ident("\u{FFFD}b".into()).to_css_string(), + "\u{FFFD}b" + ); + assert_eq!( + Token::Ident("a\u{FFFD}b".into()).to_css_string(), + "a\u{FFFD}b" + ); + + // Number prefix + assert_eq!(Token::Ident("0a".into()).to_css_string(), "\\30 a"); + assert_eq!(Token::Ident("1a".into()).to_css_string(), "\\31 a"); + assert_eq!(Token::Ident("2a".into()).to_css_string(), "\\32 a"); + assert_eq!(Token::Ident("3a".into()).to_css_string(), "\\33 a"); + assert_eq!(Token::Ident("4a".into()).to_css_string(), "\\34 a"); + assert_eq!(Token::Ident("5a".into()).to_css_string(), "\\35 a"); + assert_eq!(Token::Ident("6a".into()).to_css_string(), "\\36 a"); + assert_eq!(Token::Ident("7a".into()).to_css_string(), "\\37 a"); + assert_eq!(Token::Ident("8a".into()).to_css_string(), "\\38 a"); + assert_eq!(Token::Ident("9a".into()).to_css_string(), "\\39 a"); + + // Letter number prefix + assert_eq!(Token::Ident("a0b".into()).to_css_string(), "a0b"); + assert_eq!(Token::Ident("a1b".into()).to_css_string(), "a1b"); + assert_eq!(Token::Ident("a2b".into()).to_css_string(), "a2b"); + assert_eq!(Token::Ident("a3b".into()).to_css_string(), "a3b"); + assert_eq!(Token::Ident("a4b".into()).to_css_string(), "a4b"); + assert_eq!(Token::Ident("a5b".into()).to_css_string(), "a5b"); + assert_eq!(Token::Ident("a6b".into()).to_css_string(), "a6b"); + assert_eq!(Token::Ident("a7b".into()).to_css_string(), "a7b"); + assert_eq!(Token::Ident("a8b".into()).to_css_string(), "a8b"); + assert_eq!(Token::Ident("a9b".into()).to_css_string(), "a9b"); + + // Dash number prefix + assert_eq!(Token::Ident("-0a".into()).to_css_string(), "-\\30 a"); + assert_eq!(Token::Ident("-1a".into()).to_css_string(), "-\\31 a"); + assert_eq!(Token::Ident("-2a".into()).to_css_string(), "-\\32 a"); + assert_eq!(Token::Ident("-3a".into()).to_css_string(), "-\\33 a"); + assert_eq!(Token::Ident("-4a".into()).to_css_string(), "-\\34 a"); + assert_eq!(Token::Ident("-5a".into()).to_css_string(), "-\\35 a"); + assert_eq!(Token::Ident("-6a".into()).to_css_string(), "-\\36 a"); + assert_eq!(Token::Ident("-7a".into()).to_css_string(), "-\\37 a"); + assert_eq!(Token::Ident("-8a".into()).to_css_string(), "-\\38 a"); + assert_eq!(Token::Ident("-9a".into()).to_css_string(), "-\\39 a"); + + // Double dash prefix + assert_eq!(Token::Ident("--a".into()).to_css_string(), "--a"); + + // Various tests + assert_eq!( + Token::Ident("\x01\x02\x1E\x1F".into()).to_css_string(), + "\\1 \\2 \\1e \\1f " + ); + assert_eq!( + Token::Ident("\u{0080}\x2D\x5F\u{00A9}".into()).to_css_string(), + "\u{0080}\x2D\x5F\u{00A9}" + ); + assert_eq!(Token::Ident("\x7F\u{0080}\u{0081}\u{0082}\u{0083}\u{0084}\u{0085}\u{0086}\u{0087}\u{0088}\u{0089}\ + \u{008A}\u{008B}\u{008C}\u{008D}\u{008E}\u{008F}\u{0090}\u{0091}\u{0092}\u{0093}\u{0094}\u{0095}\u{0096}\ + \u{0097}\u{0098}\u{0099}\u{009A}\u{009B}\u{009C}\u{009D}\u{009E}\u{009F}".into()).to_css_string(), + "\\7f \u{0080}\u{0081}\u{0082}\u{0083}\u{0084}\u{0085}\u{0086}\u{0087}\u{0088}\u{0089}\u{008A}\u{008B}\u{008C}\ + \u{008D}\u{008E}\u{008F}\u{0090}\u{0091}\u{0092}\u{0093}\u{0094}\u{0095}\u{0096}\u{0097}\u{0098}\u{0099}\ + \u{009A}\u{009B}\u{009C}\u{009D}\u{009E}\u{009F}"); + assert_eq!( + Token::Ident("\u{00A0}\u{00A1}\u{00A2}".into()).to_css_string(), + "\u{00A0}\u{00A1}\u{00A2}" + ); + assert_eq!( + Token::Ident("a0123456789b".into()).to_css_string(), + "a0123456789b" + ); + assert_eq!( + Token::Ident("abcdefghijklmnopqrstuvwxyz".into()).to_css_string(), + "abcdefghijklmnopqrstuvwxyz" + ); + assert_eq!( + Token::Ident("ABCDEFGHIJKLMNOPQRSTUVWXYZ".into()).to_css_string(), + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + ); + assert_eq!( + Token::Ident("\x20\x21\x78\x79".into()).to_css_string(), + "\\ \\!xy" + ); + + // astral symbol (U+1D306 TETRAGRAM FOR CENTRE) + assert_eq!( + Token::Ident("\u{1D306}".into()).to_css_string(), + "\u{1D306}" + ); +} + +trait ToJson { + fn to_json(&self) -> Value; +} + +impl<T> ToJson for T +where + T: Clone, + Value: From<T>, +{ + fn to_json(&self) -> Value { + Value::from(self.clone()) + } +} + +impl ToJson for Color { + fn to_json(&self) -> Value { + match *self { + Color::CurrentColor => "currentcolor".to_json(), + Color::Rgba(ref rgba) => { + json!([rgba.red, rgba.green, rgba.blue, rgba.alpha]) + } + Color::Hsl(ref c) => json!([c.hue, c.saturation, c.lightness, c.alpha]), + Color::Hwb(ref c) => json!([c.hue, c.whiteness, c.blackness, c.alpha]), + Color::Lab(ref c) => json!([c.lightness, c.a, c.b, c.alpha]), + Color::Lch(ref c) => json!([c.lightness, c.chroma, c.hue, c.alpha]), + Color::Oklab(ref c) => json!([c.lightness, c.a, c.b, c.alpha]), + Color::Oklch(ref c) => json!([c.lightness, c.chroma, c.hue, c.alpha]), + Color::ColorFunction(ref c) => { + json!([c.color_space.as_str(), c.c1, c.c2, c.c3, c.alpha]) + } + } + } +} + +impl<'a> ToJson for CowRcStr<'a> { + fn to_json(&self) -> Value { + let s: &str = &*self; + s.to_json() + } +} + +#[cfg(feature = "bench")] +const BACKGROUND_IMAGE: &'static str = include_str!("big-data-url.css"); + +#[cfg(feature = "bench")] +#[bench] +fn unquoted_url(b: &mut Bencher) { + b.iter(|| { + let mut input = ParserInput::new(BACKGROUND_IMAGE); + let mut input = Parser::new(&mut input); + input.look_for_var_or_env_functions(); + + let result = input.try_parse(|input| input.expect_url()); + + assert!(result.is_ok()); + + input.seen_var_or_env_functions(); + (result.is_ok(), input.seen_var_or_env_functions()) + }) +} + +#[cfg(feature = "bench")] +#[bench] +fn numeric(b: &mut Bencher) { + b.iter(|| { + for _ in 0..1000000 { + let mut input = ParserInput::new("10px"); + let mut input = Parser::new(&mut input); + let _ = test::black_box(input.next()); + } + }) +} + +struct JsonParser; + +#[test] +fn no_stack_overflow_multiple_nested_blocks() { + let mut input: String = "{{".into(); + for _ in 0..20 { + let dup = input.clone(); + input.push_str(&dup); + } + let mut input = ParserInput::new(&input); + let mut input = Parser::new(&mut input); + while let Ok(..) = input.next() {} +} + +impl<'i> DeclarationParser<'i> for JsonParser { + type Declaration = Value; + type Error = (); + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<Value, ParseError<'i, ()>> { + let mut value = vec![]; + let mut important = false; + loop { + let start = input.state(); + if let Ok(mut token) = input.next_including_whitespace().map(|t| t.clone()) { + // Hack to deal with css-parsing-tests assuming that + // `!important` in the middle of a declaration value is OK. + // This can never happen per spec + // (even CSS Variables forbid top-level `!`) + if token == Token::Delim('!') { + input.reset(&start); + if parse_important(input).is_ok() { + if input.is_exhausted() { + important = true; + break; + } + } + input.reset(&start); + token = input.next_including_whitespace().unwrap().clone(); + } + value.push(one_component_value_to_json(token, input)); + } else { + break; + } + } + Ok(JArray!["declaration", name, value, important,]) + } +} + +impl<'i> AtRuleParser<'i> for JsonParser { + type Prelude = Vec<Value>; + type AtRule = Value; + type Error = (); + + fn parse_prelude<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<Vec<Value>, ParseError<'i, ()>> { + let prelude = vec![ + "at-rule".to_json(), + name.to_json(), + Value::Array(component_values_to_json(input)), + ]; + match_ignore_ascii_case! { &*name, + "charset" => { + Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name.clone()).into())) + }, + _ => Ok(prelude), + } + } + + fn rule_without_block( + &mut self, + mut prelude: Vec<Value>, + _: &ParserState, + ) -> Result<Value, ()> { + prelude.push(Value::Null); + Ok(Value::Array(prelude)) + } + + fn parse_block<'t>( + &mut self, + mut prelude: Vec<Value>, + _: &ParserState, + input: &mut Parser<'i, 't>, + ) -> Result<Value, ParseError<'i, ()>> { + prelude.push(Value::Array(component_values_to_json(input))); + Ok(Value::Array(prelude)) + } +} + +impl<'i> QualifiedRuleParser<'i> for JsonParser { + type Prelude = Vec<Value>; + type QualifiedRule = Value; + type Error = (); + + fn parse_prelude<'t>( + &mut self, + input: &mut Parser<'i, 't>, + ) -> Result<Vec<Value>, ParseError<'i, ()>> { + Ok(component_values_to_json(input)) + } + + fn parse_block<'t>( + &mut self, + prelude: Vec<Value>, + _: &ParserState, + input: &mut Parser<'i, 't>, + ) -> Result<Value, ParseError<'i, ()>> { + Ok(JArray![ + "qualified rule", + prelude, + component_values_to_json(input), + ]) + } +} + +impl<'i> RuleBodyItemParser<'i, Value, ()> for JsonParser { + fn parse_qualified(&self) -> bool { + true + } + fn parse_declarations(&self) -> bool { + true + } +} + +fn component_values_to_json(input: &mut Parser) -> Vec<Value> { + let mut values = vec![]; + while let Ok(token) = input.next_including_whitespace().map(|t| t.clone()) { + values.push(one_component_value_to_json(token, input)); + } + values +} + +fn one_component_value_to_json(token: Token, input: &mut Parser) -> Value { + fn numeric(value: f32, int_value: Option<i32>, has_sign: bool) -> Vec<Value> { + vec![ + Token::Number { + value: value, + int_value: int_value, + has_sign: has_sign, + } + .to_css_string() + .to_json(), + match int_value { + Some(i) => i.to_json(), + None => value.to_json(), + }, + match int_value { + Some(_) => "integer", + None => "number", + } + .to_json(), + ] + } + + fn nested(input: &mut Parser) -> Vec<Value> { + let result: Result<_, ParseError<()>> = + input.parse_nested_block(|input| Ok(component_values_to_json(input))); + result.unwrap() + } + + match token { + Token::Ident(value) => JArray!["ident", value], + Token::AtKeyword(value) => JArray!["at-keyword", value], + Token::Hash(value) => JArray!["hash", value, "unrestricted"], + Token::IDHash(value) => JArray!["hash", value, "id"], + Token::QuotedString(value) => JArray!["string", value], + Token::UnquotedUrl(value) => JArray!["url", value], + Token::Delim('\\') => "\\".to_json(), + Token::Delim(value) => value.to_string().to_json(), + + Token::Number { + value, + int_value, + has_sign, + } => Value::Array({ + let mut v = vec!["number".to_json()]; + v.extend(numeric(value, int_value, has_sign)); + v + }), + Token::Percentage { + unit_value, + int_value, + has_sign, + } => Value::Array({ + let mut v = vec!["percentage".to_json()]; + v.extend(numeric(unit_value * 100., int_value, has_sign)); + v + }), + Token::Dimension { + value, + int_value, + has_sign, + unit, + } => Value::Array({ + let mut v = vec!["dimension".to_json()]; + v.extend(numeric(value, int_value, has_sign)); + v.push(unit.to_json()); + v + }), + + Token::WhiteSpace(_) => " ".to_json(), + Token::Comment(_) => "/**/".to_json(), + Token::Colon => ":".to_json(), + Token::Semicolon => ";".to_json(), + Token::Comma => ",".to_json(), + Token::IncludeMatch => "~=".to_json(), + Token::DashMatch => "|=".to_json(), + Token::PrefixMatch => "^=".to_json(), + Token::SuffixMatch => "$=".to_json(), + Token::SubstringMatch => "*=".to_json(), + Token::CDO => "<!--".to_json(), + Token::CDC => "-->".to_json(), + + Token::Function(name) => Value::Array({ + let mut v = vec!["function".to_json(), name.to_json()]; + v.extend(nested(input)); + v + }), + Token::ParenthesisBlock => Value::Array({ + let mut v = vec!["()".to_json()]; + v.extend(nested(input)); + v + }), + Token::SquareBracketBlock => Value::Array({ + let mut v = vec!["[]".to_json()]; + v.extend(nested(input)); + v + }), + Token::CurlyBracketBlock => Value::Array({ + let mut v = vec!["{}".to_json()]; + v.extend(nested(input)); + v + }), + Token::BadUrl(_) => JArray!["error", "bad-url"], + Token::BadString(_) => JArray!["error", "bad-string"], + Token::CloseParenthesis => JArray!["error", ")"], + Token::CloseSquareBracket => JArray!["error", "]"], + Token::CloseCurlyBracket => JArray!["error", "}"], + } +} + +/// A previous version of procedural-masquerade had a bug where it +/// would normalize consecutive whitespace to a single space, +/// including in string literals. +#[test] +fn procedural_masquerade_whitespace() { + ascii_case_insensitive_phf_map! { + map -> () = { + " \t\n" => () + } + } + assert_eq!(map(" \t\n"), Some(&())); + assert_eq!(map(" "), None); + + match_ignore_ascii_case! { " \t\n", + " " => panic!("1"), + " \t\n" => {}, + _ => panic!("2"), + } + + match_ignore_ascii_case! { " ", + " \t\n" => panic!("3"), + " " => {}, + _ => panic!("4"), + } +} + +#[test] +fn parse_until_before_stops_at_delimiter_or_end_of_input() { + // For all j and k, inputs[i].1[j] should parse the same as inputs[i].1[k] + // when we use delimiters inputs[i].0. + let inputs = vec![ + ( + Delimiter::Bang | Delimiter::Semicolon, + // Note that the ';extra' is fine, because the ';' acts the same as + // the end of input. + vec!["token stream;extra", "token stream!", "token stream"], + ), + (Delimiter::Bang | Delimiter::Semicolon, vec![";", "!", ""]), + ]; + for equivalent in inputs { + for (j, x) in equivalent.1.iter().enumerate() { + for y in equivalent.1[j + 1..].iter() { + let mut ix = ParserInput::new(x); + let mut ix = Parser::new(&mut ix); + + let mut iy = ParserInput::new(y); + let mut iy = Parser::new(&mut iy); + + let _ = ix.parse_until_before::<_, _, ()>(equivalent.0, |ix| { + iy.parse_until_before::<_, _, ()>(equivalent.0, |iy| { + loop { + let ox = ix.next(); + let oy = iy.next(); + assert_eq!(ox, oy); + if let Err(_) = ox { + break; + } + } + Ok(()) + }) + }); + } + } + } +} + +#[test] +fn parser_maintains_current_line() { + let mut input = ParserInput::new("ident ident;\nident ident ident;\nident"); + let mut parser = Parser::new(&mut input); + assert_eq!(parser.current_line(), "ident ident;"); + assert_eq!(parser.next(), Ok(&Token::Ident("ident".into()))); + assert_eq!(parser.next(), Ok(&Token::Ident("ident".into()))); + assert_eq!(parser.next(), Ok(&Token::Semicolon)); + + assert_eq!(parser.next(), Ok(&Token::Ident("ident".into()))); + assert_eq!(parser.current_line(), "ident ident ident;"); + assert_eq!(parser.next(), Ok(&Token::Ident("ident".into()))); + assert_eq!(parser.next(), Ok(&Token::Ident("ident".into()))); + assert_eq!(parser.next(), Ok(&Token::Semicolon)); + + assert_eq!(parser.next(), Ok(&Token::Ident("ident".into()))); + assert_eq!(parser.current_line(), "ident"); +} + +#[test] +fn parser_with_line_number_offset() { + let mut input = ParserInput::new_with_line_number_offset("ident\nident", 72); + let mut parser = Parser::new(&mut input); + assert_eq!( + parser.current_source_location(), + SourceLocation { + line: 72, + column: 1 + } + ); + assert_eq!( + parser.next_including_whitespace_and_comments(), + Ok(&Token::Ident("ident".into())) + ); + assert_eq!( + parser.current_source_location(), + SourceLocation { + line: 72, + column: 6 + } + ); + assert_eq!( + parser.next_including_whitespace_and_comments(), + Ok(&Token::WhiteSpace("\n".into())) + ); + assert_eq!( + parser.current_source_location(), + SourceLocation { + line: 73, + column: 1 + } + ); + assert_eq!( + parser.next_including_whitespace_and_comments(), + Ok(&Token::Ident("ident".into())) + ); + assert_eq!( + parser.current_source_location(), + SourceLocation { + line: 73, + column: 6 + } + ); +} + +#[test] +fn cdc_regression_test() { + let mut input = ParserInput::new("-->x"); + let mut parser = Parser::new(&mut input); + parser.skip_cdc_and_cdo(); + assert_eq!(parser.next(), Ok(&Token::Ident("x".into()))); + assert_eq!( + parser.next(), + Err(BasicParseError { + kind: BasicParseErrorKind::EndOfInput, + location: SourceLocation { line: 0, column: 5 } + }) + ); +} + +#[test] +fn parse_entirely_reports_first_error() { + #[derive(PartialEq, Debug)] + enum E { + Foo, + } + let mut input = ParserInput::new("ident"); + let mut parser = Parser::new(&mut input); + let result: Result<(), _> = parser.parse_entirely(|p| Err(p.new_custom_error(E::Foo))); + assert_eq!( + result, + Err(ParseError { + kind: ParseErrorKind::Custom(E::Foo), + location: SourceLocation { line: 0, column: 1 }, + }) + ); +} + +#[test] +fn parse_sourcemapping_comments() { + let tests = vec![ + ("/*# sourceMappingURL=here*/", Some("here")), + ("/*# sourceMappingURL=here */", Some("here")), + ("/*@ sourceMappingURL=here*/", Some("here")), + ( + "/*@ sourceMappingURL=there*/ /*# sourceMappingURL=here*/", + Some("here"), + ), + ("/*# sourceMappingURL=here there */", Some("here")), + ("/*# sourceMappingURL= here */", Some("")), + ("/*# sourceMappingURL=*/", Some("")), + ("/*# sourceMappingUR=here */", None), + ("/*! sourceMappingURL=here */", None), + ("/*# sourceMappingURL = here */", None), + ("/* # sourceMappingURL=here */", None), + ]; + + for test in tests { + let mut input = ParserInput::new(test.0); + let mut parser = Parser::new(&mut input); + while let Ok(_) = parser.next_including_whitespace() {} + assert_eq!(parser.current_source_map_url(), test.1); + } +} + +#[test] +fn parse_sourceurl_comments() { + let tests = vec![ + ("/*# sourceURL=here*/", Some("here")), + ("/*# sourceURL=here */", Some("here")), + ("/*@ sourceURL=here*/", Some("here")), + ("/*@ sourceURL=there*/ /*# sourceURL=here*/", Some("here")), + ("/*# sourceURL=here there */", Some("here")), + ("/*# sourceURL= here */", Some("")), + ("/*# sourceURL=*/", Some("")), + ("/*# sourceMappingUR=here */", None), + ("/*! sourceURL=here */", None), + ("/*# sourceURL = here */", None), + ("/* # sourceURL=here */", None), + ]; + + for test in tests { + let mut input = ParserInput::new(test.0); + let mut parser = Parser::new(&mut input); + while let Ok(_) = parser.next_including_whitespace() {} + assert_eq!(parser.current_source_url(), test.1); + } +} + +#[test] +fn roundtrip_percentage_token() { + fn test_roundtrip(value: &str) { + let mut input = ParserInput::new(value); + let mut parser = Parser::new(&mut input); + let token = parser.next().unwrap(); + assert_eq!(token.to_css_string(), value); + } + // Test simple number serialization + for i in 0..101 { + test_roundtrip(&format!("{}%", i)); + for j in 0..10 { + if j != 0 { + test_roundtrip(&format!("{}.{}%", i, j)); + } + for k in 1..10 { + test_roundtrip(&format!("{}.{}{}%", i, j, k)); + } + } + } +} + +#[test] +fn utf16_columns() { + // This particular test serves two purposes. First, it checks + // that the column number computations are correct. Second, it + // checks that tokenizer code paths correctly differentiate + // between the different UTF-8 encoding bytes. In particular + // different leader bytes and continuation bytes are treated + // differently, so we make sure to include all lengths in the + // tests, using the string "QΡ✈🆒". Also, remember that because + // the column is in units of UTF-16, the 4-byte sequence results + // in two columns. + let tests = vec![ + ("", 1), + ("ascii", 6), + ("/*QΡ✈🆒*/", 10), + ("'QΡ✈🆒*'", 9), + ("\"\\\"'QΡ✈🆒*'", 12), + ("\\Q\\Ρ\\✈\\🆒", 10), + ("QΡ✈🆒", 6), + ("QΡ✈🆒\\Q\\Ρ\\✈\\🆒", 15), + ("newline\r\nQΡ✈🆒", 6), + ("url(QΡ✈🆒\\Q\\Ρ\\✈\\🆒)", 20), + ("url(QΡ✈🆒)", 11), + ("url(\r\nQΡ✈🆒\\Q\\Ρ\\✈\\🆒)", 16), + ("url(\r\nQΡ✈🆒\\Q\\Ρ\\✈\\🆒", 15), + ("url(\r\nQΡ✈🆒\\Q\\Ρ\\✈\\🆒 x", 17), + ("QΡ✈🆒()", 8), + // Test that under/over-flow of current_line_start_position is + // handled properly; see the special case in consume_4byte_intro. + ("🆒", 3), + ]; + + for test in tests { + let mut input = ParserInput::new(test.0); + let mut parser = Parser::new(&mut input); + + // Read all tokens. + loop { + match parser.next() { + Err(BasicParseError { + kind: BasicParseErrorKind::EndOfInput, + .. + }) => { + break; + } + Err(_) => { + assert!(false); + } + Ok(_) => {} + }; + } + + // Check the resulting column. + assert_eq!(parser.current_source_location().column, test.1); + } +} + +#[test] +fn servo_define_css_keyword_enum() { + macro_rules! define_css_keyword_enum { + (pub enum $name:ident { $($variant:ident = $css:pat,)+ }) => { + #[derive(PartialEq, Debug)] + pub enum $name { + $($variant),+ + } + + impl $name { + pub fn from_ident(ident: &str) -> Result<$name, ()> { + match_ignore_ascii_case! { ident, + $($css => Ok($name::$variant),)+ + _ => Err(()) + } + } + } + } + } + define_css_keyword_enum! { + pub enum UserZoom { + Zoom = "zoom", + Fixed = "fixed", + } + } + + assert_eq!(UserZoom::from_ident("fixed"), Ok(UserZoom::Fixed)); +} + +#[test] +fn generic_parser() { + #[derive(Debug, PartialEq)] + enum OutputType { + CurrentColor, + Rgba(Option<u8>, Option<u8>, Option<u8>, Option<f32>), + Hsl(Option<f32>, Option<f32>, Option<f32>, Option<f32>), + Hwb(Option<f32>, Option<f32>, Option<f32>, Option<f32>), + Lab(Option<f32>, Option<f32>, Option<f32>, Option<f32>), + Lch(Option<f32>, Option<f32>, Option<f32>, Option<f32>), + Oklab(Option<f32>, Option<f32>, Option<f32>, Option<f32>), + Oklch(Option<f32>, Option<f32>, Option<f32>, Option<f32>), + ColorFunction( + PredefinedColorSpace, + Option<f32>, + Option<f32>, + Option<f32>, + Option<f32>, + ), + } + + impl FromParsedColor for OutputType { + fn from_current_color() -> Self { + OutputType::CurrentColor + } + + fn from_rgba( + red: Option<u8>, + green: Option<u8>, + blue: Option<u8>, + alpha: Option<f32>, + ) -> Self { + OutputType::Rgba(red, green, blue, alpha) + } + + fn from_hsl( + hue: Option<f32>, + saturation: Option<f32>, + lightness: Option<f32>, + alpha: Option<f32>, + ) -> Self { + OutputType::Hsl(hue, saturation, lightness, alpha) + } + + fn from_hwb( + hue: Option<f32>, + blackness: Option<f32>, + whiteness: Option<f32>, + alpha: Option<f32>, + ) -> Self { + OutputType::Hwb(hue, blackness, whiteness, alpha) + } + + fn from_lab( + lightness: Option<f32>, + a: Option<f32>, + b: Option<f32>, + alpha: Option<f32>, + ) -> Self { + OutputType::Lab(lightness, a, b, alpha) + } + + fn from_lch( + lightness: Option<f32>, + chroma: Option<f32>, + hue: Option<f32>, + alpha: Option<f32>, + ) -> Self { + OutputType::Lch(lightness, chroma, hue, alpha) + } + + fn from_oklab( + lightness: Option<f32>, + a: Option<f32>, + b: Option<f32>, + alpha: Option<f32>, + ) -> Self { + OutputType::Oklab(lightness, a, b, alpha) + } + + fn from_oklch( + lightness: Option<f32>, + chroma: Option<f32>, + hue: Option<f32>, + alpha: Option<f32>, + ) -> Self { + OutputType::Oklch(lightness, chroma, hue, alpha) + } + + fn from_color_function( + color_space: PredefinedColorSpace, + c1: Option<f32>, + c2: Option<f32>, + c3: Option<f32>, + alpha: Option<f32>, + ) -> Self { + OutputType::ColorFunction(color_space, c1, c2, c3, alpha) + } + } + + struct TestColorParser; + impl<'i> ColorParser<'i> for TestColorParser { + type Output = OutputType; + type Error = (); + } + + #[rustfmt::skip] + const TESTS: &[(&str, OutputType)] = &[ + ("currentColor", OutputType::CurrentColor), + ("rgb(1, 2, 3)", OutputType::Rgba(Some(1), Some(2), Some(3), Some(1.0))), + ("rgba(1, 2, 3, 0.4)", OutputType::Rgba(Some(1), Some(2), Some(3), Some(0.4))), + ("rgb(none none none / none)", OutputType::Rgba(None, None, None, None)), + ("rgb(1 none 3 / none)", OutputType::Rgba(Some(1), None, Some(3), None)), + + ("hsla(45deg, 20%, 30%, 0.4)", OutputType::Hsl(Some(45.0), Some(0.2), Some(0.3), Some(0.4))), + ("hsl(45deg none none)", OutputType::Hsl(Some(45.0), None, None, Some(1.0))), + ("hsl(none 10% none / none)", OutputType::Hsl(None, Some(0.1), None, None)), + ("hsl(120 100.0% 50.0%)", OutputType::Hsl(Some(120.0), Some(1.0), Some(0.5), Some(1.0))), + + ("hwb(45deg 20% 30% / 0.4)", OutputType::Hwb(Some(45.0), Some(0.2), Some(0.3), Some(0.4))), + + ("lab(100 20 30 / 0.4)", OutputType::Lab(Some(100.0), Some(20.0), Some(30.0), Some(0.4))), + ("lch(100 20 30 / 0.4)", OutputType::Lch(Some(100.0), Some(20.0), Some(30.0), Some(0.4))), + + ("oklab(100 20 30 / 0.4)", OutputType::Oklab(Some(100.0), Some(20.0), Some(30.0), Some(0.4))), + ("oklch(100 20 30 / 0.4)", OutputType::Oklch(Some(100.0), Some(20.0), Some(30.0), Some(0.4))), + + ("color(srgb 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::Srgb, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), + ("color(srgb none none none)", OutputType::ColorFunction(PredefinedColorSpace::Srgb, None, None, None, Some(1.0))), + ("color(srgb none none none / none)", OutputType::ColorFunction(PredefinedColorSpace::Srgb, None, None, None, None)), + ("color(srgb-linear 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::SrgbLinear, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), + ("color(display-p3 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::DisplayP3, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), + ("color(a98-rgb 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::A98Rgb, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), + ("color(prophoto-rgb 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::ProphotoRgb, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), + ("color(rec2020 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::Rec2020, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), + ("color(xyz-d50 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::XyzD50, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), + ("color(xyz-d65 0.1 0.2 0.3 / 0.4)", OutputType::ColorFunction(PredefinedColorSpace::XyzD65, Some(0.1), Some(0.2), Some(0.3), Some(0.4))), + ]; + + for (input, expected) in TESTS { + let mut input = ParserInput::new(*input); + let mut input = Parser::new(&mut input); + + let actual: OutputType = parse_color_with(&TestColorParser, &mut input).unwrap(); + assert_eq!(actual, *expected); + } +} diff --git a/third_party/rust/cssparser/src/tokenizer.rs b/third_party/rust/cssparser/src/tokenizer.rs new file mode 100644 index 0000000000..4cc6c178bf --- /dev/null +++ b/third_party/rust/cssparser/src/tokenizer.rs @@ -0,0 +1,1405 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +// https://drafts.csswg.org/css-syntax/#tokenization + +use self::Token::*; +use crate::cow_rc_str::CowRcStr; +use crate::parser::ParserState; +use std::char; +use std::ops::Range; + +#[cfg(not(feature = "dummy_match_byte"))] +use cssparser_macros::match_byte; + +#[cfg(feature = "dummy_match_byte")] +macro_rules! match_byte { + ($value:expr, $($rest:tt)* ) => { + match $value { + $( + $rest + )+ + } + }; +} + +/// One of the pieces the CSS input is broken into. +/// +/// Some components use `Cow` in order to borrow from the original input string +/// and avoid allocating/copying when possible. +#[derive(PartialEq, Debug, Clone)] +pub enum Token<'a> { + /// A [`<ident-token>`](https://drafts.csswg.org/css-syntax/#ident-token-diagram) + Ident(CowRcStr<'a>), + + /// A [`<at-keyword-token>`](https://drafts.csswg.org/css-syntax/#at-keyword-token-diagram) + /// + /// The value does not include the `@` marker. + AtKeyword(CowRcStr<'a>), + + /// A [`<hash-token>`](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type flag set to "unrestricted" + /// + /// The value does not include the `#` marker. + Hash(CowRcStr<'a>), + + /// A [`<hash-token>`](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type flag set to "id" + /// + /// The value does not include the `#` marker. + IDHash(CowRcStr<'a>), // Hash that is a valid ID selector. + + /// A [`<string-token>`](https://drafts.csswg.org/css-syntax/#string-token-diagram) + /// + /// The value does not include the quotes. + QuotedString(CowRcStr<'a>), + + /// A [`<url-token>`](https://drafts.csswg.org/css-syntax/#url-token-diagram) + /// + /// The value does not include the `url(` `)` markers. Note that `url( <string-token> )` is represented by a + /// `Function` token. + UnquotedUrl(CowRcStr<'a>), + + /// A `<delim-token>` + Delim(char), + + /// A [`<number-token>`](https://drafts.csswg.org/css-syntax/#number-token-diagram) + Number { + /// Whether the number had a `+` or `-` sign. + /// + /// This is used is some cases like the <An+B> micro syntax. (See the `parse_nth` function.) + has_sign: bool, + + /// The value as a float + value: f32, + + /// If the origin source did not include a fractional part, the value as an integer. + int_value: Option<i32>, + }, + + /// A [`<percentage-token>`](https://drafts.csswg.org/css-syntax/#percentage-token-diagram) + Percentage { + /// Whether the number had a `+` or `-` sign. + has_sign: bool, + + /// The value as a float, divided by 100 so that the nominal range is 0.0 to 1.0. + unit_value: f32, + + /// If the origin source did not include a fractional part, the value as an integer. + /// It is **not** divided by 100. + int_value: Option<i32>, + }, + + /// A [`<dimension-token>`](https://drafts.csswg.org/css-syntax/#dimension-token-diagram) + Dimension { + /// Whether the number had a `+` or `-` sign. + /// + /// This is used is some cases like the <An+B> micro syntax. (See the `parse_nth` function.) + has_sign: bool, + + /// The value as a float + value: f32, + + /// If the origin source did not include a fractional part, the value as an integer. + int_value: Option<i32>, + + /// The unit, e.g. "px" in `12px` + unit: CowRcStr<'a>, + }, + + /// A [`<whitespace-token>`](https://drafts.csswg.org/css-syntax/#whitespace-token-diagram) + WhiteSpace(&'a str), + + /// A comment. + /// + /// The CSS Syntax spec does not generate tokens for comments, + /// But we do, because we can (borrowed &str makes it cheap). + /// + /// The value does not include the `/*` `*/` markers. + Comment(&'a str), + + /// A `:` `<colon-token>` + Colon, // : + + /// A `;` `<semicolon-token>` + Semicolon, // ; + + /// A `,` `<comma-token>` + Comma, // , + + /// A `~=` [`<include-match-token>`](https://drafts.csswg.org/css-syntax/#include-match-token-diagram) + IncludeMatch, + + /// A `|=` [`<dash-match-token>`](https://drafts.csswg.org/css-syntax/#dash-match-token-diagram) + DashMatch, + + /// A `^=` [`<prefix-match-token>`](https://drafts.csswg.org/css-syntax/#prefix-match-token-diagram) + PrefixMatch, + + /// A `$=` [`<suffix-match-token>`](https://drafts.csswg.org/css-syntax/#suffix-match-token-diagram) + SuffixMatch, + + /// A `*=` [`<substring-match-token>`](https://drafts.csswg.org/css-syntax/#substring-match-token-diagram) + SubstringMatch, + + /// A `<!--` [`<CDO-token>`](https://drafts.csswg.org/css-syntax/#CDO-token-diagram) + CDO, + + /// A `-->` [`<CDC-token>`](https://drafts.csswg.org/css-syntax/#CDC-token-diagram) + CDC, + + /// A [`<function-token>`](https://drafts.csswg.org/css-syntax/#function-token-diagram) + /// + /// The value (name) does not include the `(` marker. + Function(CowRcStr<'a>), + + /// A `<(-token>` + ParenthesisBlock, + + /// A `<[-token>` + SquareBracketBlock, + + /// A `<{-token>` + CurlyBracketBlock, + + /// A `<bad-url-token>` + /// + /// This token always indicates a parse error. + BadUrl(CowRcStr<'a>), + + /// A `<bad-string-token>` + /// + /// This token always indicates a parse error. + BadString(CowRcStr<'a>), + + /// A `<)-token>` + /// + /// When obtained from one of the `Parser::next*` methods, + /// this token is always unmatched and indicates a parse error. + CloseParenthesis, + + /// A `<]-token>` + /// + /// When obtained from one of the `Parser::next*` methods, + /// this token is always unmatched and indicates a parse error. + CloseSquareBracket, + + /// A `<}-token>` + /// + /// When obtained from one of the `Parser::next*` methods, + /// this token is always unmatched and indicates a parse error. + CloseCurlyBracket, +} + +impl<'a> Token<'a> { + /// Return whether this token represents a parse error. + /// + /// `BadUrl` and `BadString` are tokenizer-level parse errors. + /// + /// `CloseParenthesis`, `CloseSquareBracket`, and `CloseCurlyBracket` are *unmatched* + /// and therefore parse errors when returned by one of the `Parser::next*` methods. + pub fn is_parse_error(&self) -> bool { + matches!( + *self, + BadUrl(_) | BadString(_) | CloseParenthesis | CloseSquareBracket | CloseCurlyBracket + ) + } +} + +#[derive(Clone)] +pub struct Tokenizer<'a> { + input: &'a str, + /// Counted in bytes, not code points. From 0. + position: usize, + /// The position at the start of the current line; but adjusted to + /// ensure that computing the column will give the result in units + /// of UTF-16 characters. + current_line_start_position: usize, + current_line_number: u32, + var_or_env_functions: SeenStatus, + source_map_url: Option<&'a str>, + source_url: Option<&'a str>, +} + +#[derive(Copy, Clone, PartialEq, Eq)] +enum SeenStatus { + DontCare, + LookingForThem, + SeenAtLeastOne, +} + +impl<'a> Tokenizer<'a> { + #[inline] + pub fn new(input: &str) -> Tokenizer { + Tokenizer::with_first_line_number(input, 0) + } + + #[inline] + pub fn with_first_line_number(input: &str, first_line_number: u32) -> Tokenizer { + Tokenizer { + input, + position: 0, + current_line_start_position: 0, + current_line_number: first_line_number, + var_or_env_functions: SeenStatus::DontCare, + source_map_url: None, + source_url: None, + } + } + + #[inline] + pub fn look_for_var_or_env_functions(&mut self) { + self.var_or_env_functions = SeenStatus::LookingForThem; + } + + #[inline] + pub fn seen_var_or_env_functions(&mut self) -> bool { + let seen = self.var_or_env_functions == SeenStatus::SeenAtLeastOne; + self.var_or_env_functions = SeenStatus::DontCare; + seen + } + + #[inline] + pub fn see_function(&mut self, name: &str) { + if self.var_or_env_functions == SeenStatus::LookingForThem { + if name.eq_ignore_ascii_case("var") || name.eq_ignore_ascii_case("env") { + self.var_or_env_functions = SeenStatus::SeenAtLeastOne; + } + } + } + + #[inline] + pub fn next(&mut self) -> Result<Token<'a>, ()> { + next_token(self) + } + + #[inline] + pub fn position(&self) -> SourcePosition { + SourcePosition(self.position) + } + + #[inline] + pub fn current_source_location(&self) -> SourceLocation { + SourceLocation { + line: self.current_line_number, + column: (self.position - self.current_line_start_position + 1) as u32, + } + } + + #[inline] + pub fn current_source_map_url(&self) -> Option<&'a str> { + self.source_map_url + } + + #[inline] + pub fn current_source_url(&self) -> Option<&'a str> { + self.source_url + } + + #[inline] + pub fn state(&self) -> ParserState { + ParserState { + position: self.position, + current_line_start_position: self.current_line_start_position, + current_line_number: self.current_line_number, + at_start_of: None, + } + } + + #[inline] + pub fn reset(&mut self, state: &ParserState) { + self.position = state.position; + self.current_line_start_position = state.current_line_start_position; + self.current_line_number = state.current_line_number; + } + + #[inline] + pub fn slice_from(&self, start_pos: SourcePosition) -> &'a str { + &self.input[start_pos.0..self.position] + } + + #[inline] + pub fn slice(&self, range: Range<SourcePosition>) -> &'a str { + &self.input[range.start.0..range.end.0] + } + + pub fn current_source_line(&self) -> &'a str { + let current = self.position; + let start = self.input[0..current] + .rfind(|c| matches!(c, '\r' | '\n' | '\x0C')) + .map_or(0, |start| start + 1); + let end = self.input[current..] + .find(|c| matches!(c, '\r' | '\n' | '\x0C')) + .map_or(self.input.len(), |end| current + end); + &self.input[start..end] + } + + #[inline] + pub fn next_byte(&self) -> Option<u8> { + if self.is_eof() { + None + } else { + Some(self.input.as_bytes()[self.position]) + } + } + + // If false, `tokenizer.next_char()` will not panic. + #[inline] + fn is_eof(&self) -> bool { + !self.has_at_least(0) + } + + // If true, the input has at least `n` bytes left *after* the current one. + // That is, `tokenizer.char_at(n)` will not panic. + #[inline] + fn has_at_least(&self, n: usize) -> bool { + self.position + n < self.input.len() + } + + // Advance over N bytes in the input. This function can advance + // over ASCII bytes (excluding newlines), or UTF-8 sequence + // leaders (excluding leaders for 4-byte sequences). + #[inline] + pub fn advance(&mut self, n: usize) { + if cfg!(debug_assertions) { + // Each byte must either be an ASCII byte or a sequence + // leader, but not a 4-byte leader; also newlines are + // rejected. + for i in 0..n { + let b = self.byte_at(i); + debug_assert!(b.is_ascii() || (b & 0xF0 != 0xF0 && b & 0xC0 != 0x80)); + debug_assert!(b != b'\r' && b != b'\n' && b != b'\x0C'); + } + } + self.position += n + } + + // Assumes non-EOF + #[inline] + fn next_byte_unchecked(&self) -> u8 { + self.byte_at(0) + } + + #[inline] + fn byte_at(&self, offset: usize) -> u8 { + self.input.as_bytes()[self.position + offset] + } + + // Advance over a single byte; the byte must be a UTF-8 sequence + // leader for a 4-byte sequence. + #[inline] + fn consume_4byte_intro(&mut self) { + debug_assert!(self.next_byte_unchecked() & 0xF0 == 0xF0); + // This takes two UTF-16 characters to represent, so we + // actually have an undercount. + self.current_line_start_position = self.current_line_start_position.wrapping_sub(1); + self.position += 1; + } + + // Advance over a single byte; the byte must be a UTF-8 + // continuation byte. + #[inline] + fn consume_continuation_byte(&mut self) { + debug_assert!(self.next_byte_unchecked() & 0xC0 == 0x80); + // Continuation bytes contribute to column overcount. Note + // that due to the special case for the 4-byte sequence intro, + // we must use wrapping add here. + self.current_line_start_position = self.current_line_start_position.wrapping_add(1); + self.position += 1; + } + + // Advance over any kind of byte, excluding newlines. + #[inline(never)] + fn consume_known_byte(&mut self, byte: u8) { + debug_assert!(byte != b'\r' && byte != b'\n' && byte != b'\x0C'); + self.position += 1; + // Continuation bytes contribute to column overcount. + if byte & 0xF0 == 0xF0 { + // This takes two UTF-16 characters to represent, so we + // actually have an undercount. + self.current_line_start_position = self.current_line_start_position.wrapping_sub(1); + } else if byte & 0xC0 == 0x80 { + // Note that due to the special case for the 4-byte + // sequence intro, we must use wrapping add here. + self.current_line_start_position = self.current_line_start_position.wrapping_add(1); + } + } + + #[inline] + fn next_char(&self) -> char { + self.input[self.position..].chars().next().unwrap() + } + + // Given that a newline has been seen, advance over the newline + // and update the state. + #[inline] + fn consume_newline(&mut self) { + let byte = self.next_byte_unchecked(); + debug_assert!(byte == b'\r' || byte == b'\n' || byte == b'\x0C'); + self.position += 1; + if byte == b'\r' && self.next_byte() == Some(b'\n') { + self.position += 1; + } + self.current_line_start_position = self.position; + self.current_line_number += 1; + } + + #[inline] + fn has_newline_at(&self, offset: usize) -> bool { + self.position + offset < self.input.len() + && matches!(self.byte_at(offset), b'\n' | b'\r' | b'\x0C') + } + + #[inline] + fn consume_char(&mut self) -> char { + let c = self.next_char(); + let len_utf8 = c.len_utf8(); + self.position += len_utf8; + // Note that due to the special case for the 4-byte sequence + // intro, we must use wrapping add here. + self.current_line_start_position = self + .current_line_start_position + .wrapping_add(len_utf8 - c.len_utf16()); + c + } + + #[inline] + fn starts_with(&self, needle: &[u8]) -> bool { + self.input.as_bytes()[self.position..].starts_with(needle) + } + + pub fn skip_whitespace(&mut self) { + while !self.is_eof() { + match_byte! { self.next_byte_unchecked(), + b' ' | b'\t' => { + self.advance(1) + }, + b'\n' | b'\x0C' | b'\r' => { + self.consume_newline(); + }, + b'/' => { + if self.starts_with(b"/*") { + consume_comment(self); + } else { + return + } + } + _ => return, + } + } + } + + pub fn skip_cdc_and_cdo(&mut self) { + while !self.is_eof() { + match_byte! { self.next_byte_unchecked(), + b' ' | b'\t' => { + self.advance(1) + }, + b'\n' | b'\x0C' | b'\r' => { + self.consume_newline(); + }, + b'/' => { + if self.starts_with(b"/*") { + consume_comment(self); + } else { + return + } + } + b'<' => { + if self.starts_with(b"<!--") { + self.advance(4) + } else { + return + } + } + b'-' => { + if self.starts_with(b"-->") { + self.advance(3) + } else { + return + } + } + _ => { + return + } + } + } + } +} + +/// A position from the start of the input, counted in UTF-8 bytes. +#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)] +pub struct SourcePosition(pub(crate) usize); + +impl SourcePosition { + /// Returns the current byte index in the original input. + #[inline] + pub fn byte_index(&self) -> usize { + self.0 + } +} + +/// The line and column number for a given position within the input. +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub struct SourceLocation { + /// The line number, starting at 0 for the first line, unless `with_first_line_number` was used. + pub line: u32, + + /// The column number within a line, starting at 1 for first the character of the line. + /// Column numbers are counted in UTF-16 code units. + pub column: u32, +} + +fn next_token<'a>(tokenizer: &mut Tokenizer<'a>) -> Result<Token<'a>, ()> { + if tokenizer.is_eof() { + return Err(()); + } + let b = tokenizer.next_byte_unchecked(); + let token = match_byte! { b, + b' ' | b'\t' => { + consume_whitespace(tokenizer, false) + }, + b'\n' | b'\x0C' | b'\r' => consume_whitespace(tokenizer, true), + b'"' => consume_string(tokenizer, false), + b'#' => { + tokenizer.advance(1); + if is_ident_start(tokenizer) { IDHash(consume_name(tokenizer)) } + else if !tokenizer.is_eof() && match tokenizer.next_byte_unchecked() { + // Any other valid case here already resulted in IDHash. + b'0'..=b'9' | b'-' => true, + _ => false, + } { Hash(consume_name(tokenizer)) } + else { Delim('#') } + }, + b'$' => { + if tokenizer.starts_with(b"$=") { tokenizer.advance(2); SuffixMatch } + else { tokenizer.advance(1); Delim('$') } + }, + b'\'' => consume_string(tokenizer, true), + b'(' => { tokenizer.advance(1); ParenthesisBlock }, + b')' => { tokenizer.advance(1); CloseParenthesis }, + b'*' => { + if tokenizer.starts_with(b"*=") { tokenizer.advance(2); SubstringMatch } + else { tokenizer.advance(1); Delim('*') } + }, + b'+' => { + if ( + tokenizer.has_at_least(1) + && matches!(tokenizer.byte_at(1), b'0'..=b'9') + ) || ( + tokenizer.has_at_least(2) + && tokenizer.byte_at(1) == b'.' + && matches!(tokenizer.byte_at(2), b'0'..=b'9') + ) { + consume_numeric(tokenizer) + } else { + tokenizer.advance(1); + Delim('+') + } + }, + b',' => { tokenizer.advance(1); Comma }, + b'-' => { + if ( + tokenizer.has_at_least(1) + && matches!(tokenizer.byte_at(1), b'0'..=b'9') + ) || ( + tokenizer.has_at_least(2) + && tokenizer.byte_at(1) == b'.' + && matches!(tokenizer.byte_at(2), b'0'..=b'9') + ) { + consume_numeric(tokenizer) + } else if tokenizer.starts_with(b"-->") { + tokenizer.advance(3); + CDC + } else if is_ident_start(tokenizer) { + consume_ident_like(tokenizer) + } else { + tokenizer.advance(1); + Delim('-') + } + }, + b'.' => { + if tokenizer.has_at_least(1) + && matches!(tokenizer.byte_at(1), b'0'..=b'9' + ) { + consume_numeric(tokenizer) + } else { + tokenizer.advance(1); + Delim('.') + } + } + b'/' => { + if tokenizer.starts_with(b"/*") { + Comment(consume_comment(tokenizer)) + } else { + tokenizer.advance(1); + Delim('/') + } + } + b'0'..=b'9' => consume_numeric(tokenizer), + b':' => { tokenizer.advance(1); Colon }, + b';' => { tokenizer.advance(1); Semicolon }, + b'<' => { + if tokenizer.starts_with(b"<!--") { + tokenizer.advance(4); + CDO + } else { + tokenizer.advance(1); + Delim('<') + } + }, + b'@' => { + tokenizer.advance(1); + if is_ident_start(tokenizer) { AtKeyword(consume_name(tokenizer)) } + else { Delim('@') } + }, + b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'\0' => consume_ident_like(tokenizer), + b'[' => { tokenizer.advance(1); SquareBracketBlock }, + b'\\' => { + if !tokenizer.has_newline_at(1) { consume_ident_like(tokenizer) } + else { tokenizer.advance(1); Delim('\\') } + }, + b']' => { tokenizer.advance(1); CloseSquareBracket }, + b'^' => { + if tokenizer.starts_with(b"^=") { tokenizer.advance(2); PrefixMatch } + else { tokenizer.advance(1); Delim('^') } + }, + b'{' => { tokenizer.advance(1); CurlyBracketBlock }, + b'|' => { + if tokenizer.starts_with(b"|=") { tokenizer.advance(2); DashMatch } + else { tokenizer.advance(1); Delim('|') } + }, + b'}' => { tokenizer.advance(1); CloseCurlyBracket }, + b'~' => { + if tokenizer.starts_with(b"~=") { tokenizer.advance(2); IncludeMatch } + else { tokenizer.advance(1); Delim('~') } + }, + _ => { + if !b.is_ascii() { + consume_ident_like(tokenizer) + } else { + tokenizer.advance(1); + Delim(b as char) + } + }, + }; + Ok(token) +} + +fn consume_whitespace<'a>(tokenizer: &mut Tokenizer<'a>, newline: bool) -> Token<'a> { + let start_position = tokenizer.position(); + if newline { + tokenizer.consume_newline(); + } else { + tokenizer.advance(1); + } + while !tokenizer.is_eof() { + let b = tokenizer.next_byte_unchecked(); + match_byte! { b, + b' ' | b'\t' => { + tokenizer.advance(1); + } + b'\n' | b'\x0C' | b'\r' => { + tokenizer.consume_newline(); + } + _ => { + break + } + } + } + WhiteSpace(tokenizer.slice_from(start_position)) +} + +// Check for sourceMappingURL or sourceURL comments and update the +// tokenizer appropriately. +fn check_for_source_map<'a>(tokenizer: &mut Tokenizer<'a>, contents: &'a str) { + let directive = "# sourceMappingURL="; + let directive_old = "@ sourceMappingURL="; + + // If there is a source map directive, extract the URL. + if contents.starts_with(directive) || contents.starts_with(directive_old) { + let contents = &contents[directive.len()..]; + tokenizer.source_map_url = contents + .split(|c| c == ' ' || c == '\t' || c == '\x0C' || c == '\r' || c == '\n') + .next() + } + + let directive = "# sourceURL="; + let directive_old = "@ sourceURL="; + + // If there is a source map directive, extract the URL. + if contents.starts_with(directive) || contents.starts_with(directive_old) { + let contents = &contents[directive.len()..]; + tokenizer.source_url = contents + .split(|c| c == ' ' || c == '\t' || c == '\x0C' || c == '\r' || c == '\n') + .next() + } +} + +fn consume_comment<'a>(tokenizer: &mut Tokenizer<'a>) -> &'a str { + tokenizer.advance(2); // consume "/*" + let start_position = tokenizer.position(); + while !tokenizer.is_eof() { + match_byte! { tokenizer.next_byte_unchecked(), + b'*' => { + let end_position = tokenizer.position(); + tokenizer.advance(1); + if tokenizer.next_byte() == Some(b'/') { + tokenizer.advance(1); + let contents = tokenizer.slice(start_position..end_position); + check_for_source_map(tokenizer, contents); + return contents + } + } + b'\n' | b'\x0C' | b'\r' => { + tokenizer.consume_newline(); + } + b'\x80'..=b'\xBF' => { tokenizer.consume_continuation_byte(); } + b'\xF0'..=b'\xFF' => { tokenizer.consume_4byte_intro(); } + _ => { + // ASCII or other leading byte. + tokenizer.advance(1); + } + } + } + let contents = tokenizer.slice_from(start_position); + check_for_source_map(tokenizer, contents); + contents +} + +fn consume_string<'a>(tokenizer: &mut Tokenizer<'a>, single_quote: bool) -> Token<'a> { + match consume_quoted_string(tokenizer, single_quote) { + Ok(value) => QuotedString(value), + Err(value) => BadString(value), + } +} + +/// Return `Err(())` on syntax error (ie. unescaped newline) +fn consume_quoted_string<'a>( + tokenizer: &mut Tokenizer<'a>, + single_quote: bool, +) -> Result<CowRcStr<'a>, CowRcStr<'a>> { + tokenizer.advance(1); // Skip the initial quote + // start_pos is at code point boundary, after " or ' + let start_pos = tokenizer.position(); + let mut string_bytes; + loop { + if tokenizer.is_eof() { + return Ok(tokenizer.slice_from(start_pos).into()); + } + match_byte! { tokenizer.next_byte_unchecked(), + b'"' => { + if !single_quote { + let value = tokenizer.slice_from(start_pos); + tokenizer.advance(1); + return Ok(value.into()) + } + tokenizer.advance(1); + } + b'\'' => { + if single_quote { + let value = tokenizer.slice_from(start_pos); + tokenizer.advance(1); + return Ok(value.into()) + } + tokenizer.advance(1); + } + b'\\' | b'\0' => { + // * The tokenizer’s input is UTF-8 since it’s `&str`. + // * start_pos is at a code point boundary + // * so is the current position (which is before '\\' or '\0' + // + // So `string_bytes` is well-formed UTF-8. + string_bytes = tokenizer.slice_from(start_pos).as_bytes().to_owned(); + break + } + b'\n' | b'\r' | b'\x0C' => { + return Err(tokenizer.slice_from(start_pos).into()) + }, + b'\x80'..=b'\xBF' => { tokenizer.consume_continuation_byte(); } + b'\xF0'..=b'\xFF' => { tokenizer.consume_4byte_intro(); } + _ => { + // ASCII or other leading byte. + tokenizer.advance(1); + } + } + } + + while !tokenizer.is_eof() { + let b = tokenizer.next_byte_unchecked(); + match_byte! { b, + b'\n' | b'\r' | b'\x0C' => { + return Err( + // string_bytes is well-formed UTF-8, see other comments. + unsafe { + from_utf8_release_unchecked(string_bytes) + }.into() + ); + } + b'"' => { + tokenizer.advance(1); + if !single_quote { + break; + } + } + b'\'' => { + tokenizer.advance(1); + if single_quote { + break; + } + } + b'\\' => { + tokenizer.advance(1); + if !tokenizer.is_eof() { + match tokenizer.next_byte_unchecked() { + // Escaped newline + b'\n' | b'\x0C' | b'\r' => { + tokenizer.consume_newline(); + } + // This pushes one well-formed code point + _ => consume_escape_and_write(tokenizer, &mut string_bytes) + } + } + // else: escaped EOF, do nothing. + continue; + } + b'\0' => { + tokenizer.advance(1); + string_bytes.extend("\u{FFFD}".as_bytes()); + continue; + } + b'\x80'..=b'\xBF' => { tokenizer.consume_continuation_byte(); } + b'\xF0'..=b'\xFF' => { tokenizer.consume_4byte_intro(); } + _ => { + // ASCII or other leading byte. + tokenizer.advance(1); + }, + } + + // If this byte is part of a multi-byte code point, + // we’ll end up copying the whole code point before this loop does something else. + string_bytes.push(b); + } + + Ok( + // string_bytes is well-formed UTF-8, see other comments. + unsafe { from_utf8_release_unchecked(string_bytes) }.into(), + ) +} + +#[inline] +fn is_ident_start(tokenizer: &mut Tokenizer) -> bool { + !tokenizer.is_eof() + && match_byte! { tokenizer.next_byte_unchecked(), + b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'\0' => true, + b'-' => { + tokenizer.has_at_least(1) && match_byte! { tokenizer.byte_at(1), + b'a'..=b'z' | b'A'..=b'Z' | b'-' | b'_' | b'\0' => { + true + } + b'\\' => !tokenizer.has_newline_at(1), + b => !b.is_ascii(), + } + }, + b'\\' => !tokenizer.has_newline_at(1), + b => !b.is_ascii(), + } +} + +fn consume_ident_like<'a>(tokenizer: &mut Tokenizer<'a>) -> Token<'a> { + let value = consume_name(tokenizer); + if !tokenizer.is_eof() && tokenizer.next_byte_unchecked() == b'(' { + tokenizer.advance(1); + if value.eq_ignore_ascii_case("url") { + consume_unquoted_url(tokenizer).unwrap_or(Function(value)) + } else { + tokenizer.see_function(&value); + Function(value) + } + } else { + Ident(value) + } +} + +fn consume_name<'a>(tokenizer: &mut Tokenizer<'a>) -> CowRcStr<'a> { + // start_pos is the end of the previous token, therefore at a code point boundary + let start_pos = tokenizer.position(); + let mut value_bytes; + loop { + if tokenizer.is_eof() { + return tokenizer.slice_from(start_pos).into(); + } + match_byte! { tokenizer.next_byte_unchecked(), + b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-' => tokenizer.advance(1), + b'\\' | b'\0' => { + // * The tokenizer’s input is UTF-8 since it’s `&str`. + // * start_pos is at a code point boundary + // * so is the current position (which is before '\\' or '\0' + // + // So `value_bytes` is well-formed UTF-8. + value_bytes = tokenizer.slice_from(start_pos).as_bytes().to_owned(); + break + } + b'\x80'..=b'\xBF' => { tokenizer.consume_continuation_byte(); } + b'\xC0'..=b'\xEF' => { tokenizer.advance(1); } + b'\xF0'..=b'\xFF' => { tokenizer.consume_4byte_intro(); } + _b => { + return tokenizer.slice_from(start_pos).into(); + } + } + } + + while !tokenizer.is_eof() { + let b = tokenizer.next_byte_unchecked(); + match_byte! { b, + b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-' => { + tokenizer.advance(1); + value_bytes.push(b) // ASCII + } + b'\\' => { + if tokenizer.has_newline_at(1) { break } + tokenizer.advance(1); + // This pushes one well-formed code point + consume_escape_and_write(tokenizer, &mut value_bytes) + } + b'\0' => { + tokenizer.advance(1); + value_bytes.extend("\u{FFFD}".as_bytes()); + }, + b'\x80'..=b'\xBF' => { + // This byte *is* part of a multi-byte code point, + // we’ll end up copying the whole code point before this loop does something else. + tokenizer.consume_continuation_byte(); + value_bytes.push(b) + } + b'\xC0'..=b'\xEF' => { + // This byte *is* part of a multi-byte code point, + // we’ll end up copying the whole code point before this loop does something else. + tokenizer.advance(1); + value_bytes.push(b) + } + b'\xF0'..=b'\xFF' => { + tokenizer.consume_4byte_intro(); + value_bytes.push(b) + } + _ => { + // ASCII + break; + } + } + } + // string_bytes is well-formed UTF-8, see other comments. + unsafe { from_utf8_release_unchecked(value_bytes) }.into() +} + +fn byte_to_hex_digit(b: u8) -> Option<u32> { + Some(match_byte! { b, + b'0' ..= b'9' => b - b'0', + b'a' ..= b'f' => b - b'a' + 10, + b'A' ..= b'F' => b - b'A' + 10, + _ => { + return None + } + } as u32) +} + +fn byte_to_decimal_digit(b: u8) -> Option<u32> { + if b >= b'0' && b <= b'9' { + Some((b - b'0') as u32) + } else { + None + } +} + +fn consume_numeric<'a>(tokenizer: &mut Tokenizer<'a>) -> Token<'a> { + // Parse [+-]?\d*(\.\d+)?([eE][+-]?\d+)? + // But this is always called so that there is at least one digit in \d*(\.\d+)? + + // Do all the math in f64 so that large numbers overflow to +/-inf + // and i32::{MIN, MAX} are within range. + + let (has_sign, sign) = match tokenizer.next_byte_unchecked() { + b'-' => (true, -1.), + b'+' => (true, 1.), + _ => (false, 1.), + }; + if has_sign { + tokenizer.advance(1); + } + + let mut integral_part: f64 = 0.; + while let Some(digit) = byte_to_decimal_digit(tokenizer.next_byte_unchecked()) { + integral_part = integral_part * 10. + digit as f64; + tokenizer.advance(1); + if tokenizer.is_eof() { + break; + } + } + + let mut is_integer = true; + + let mut fractional_part: f64 = 0.; + if tokenizer.has_at_least(1) + && tokenizer.next_byte_unchecked() == b'.' + && matches!(tokenizer.byte_at(1), b'0'..=b'9') + { + is_integer = false; + tokenizer.advance(1); // Consume '.' + let mut factor = 0.1; + while let Some(digit) = byte_to_decimal_digit(tokenizer.next_byte_unchecked()) { + fractional_part += digit as f64 * factor; + factor *= 0.1; + tokenizer.advance(1); + if tokenizer.is_eof() { + break; + } + } + } + + let mut value = sign * (integral_part + fractional_part); + + if tokenizer.has_at_least(1) && matches!(tokenizer.next_byte_unchecked(), b'e' | b'E') { + if matches!(tokenizer.byte_at(1), b'0'..=b'9') + || (tokenizer.has_at_least(2) + && matches!(tokenizer.byte_at(1), b'+' | b'-') + && matches!(tokenizer.byte_at(2), b'0'..=b'9')) + { + is_integer = false; + tokenizer.advance(1); + let (has_sign, sign) = match tokenizer.next_byte_unchecked() { + b'-' => (true, -1.), + b'+' => (true, 1.), + _ => (false, 1.), + }; + if has_sign { + tokenizer.advance(1); + } + let mut exponent: f64 = 0.; + while let Some(digit) = byte_to_decimal_digit(tokenizer.next_byte_unchecked()) { + exponent = exponent * 10. + digit as f64; + tokenizer.advance(1); + if tokenizer.is_eof() { + break; + } + } + value *= f64::powf(10., sign * exponent); + } + } + + let int_value = if is_integer { + Some(if value >= i32::MAX as f64 { + i32::MAX + } else if value <= i32::MIN as f64 { + i32::MIN + } else { + value as i32 + }) + } else { + None + }; + + if !tokenizer.is_eof() && tokenizer.next_byte_unchecked() == b'%' { + tokenizer.advance(1); + return Percentage { + unit_value: (value / 100.) as f32, + int_value, + has_sign, + }; + } + let value = value as f32; + if is_ident_start(tokenizer) { + let unit = consume_name(tokenizer); + Dimension { + value, + int_value, + has_sign, + unit, + } + } else { + Number { + value, + int_value, + has_sign, + } + } +} + +#[inline] +unsafe fn from_utf8_release_unchecked(string_bytes: Vec<u8>) -> String { + if cfg!(debug_assertions) { + String::from_utf8(string_bytes).unwrap() + } else { + String::from_utf8_unchecked(string_bytes) + } +} + +fn consume_unquoted_url<'a>(tokenizer: &mut Tokenizer<'a>) -> Result<Token<'a>, ()> { + // This is only called after "url(", so the current position is a code point boundary. + let start_position = tokenizer.position; + let from_start = &tokenizer.input[tokenizer.position..]; + let mut newlines = 0; + let mut last_newline = 0; + let mut found_printable_char = false; + let mut iter = from_start.bytes().enumerate(); + loop { + let (offset, b) = match iter.next() { + Some(item) => item, + None => { + tokenizer.position = tokenizer.input.len(); + break; + } + }; + match_byte! { b, + b' ' | b'\t' => {}, + b'\n' | b'\x0C' => { + newlines += 1; + last_newline = offset; + } + b'\r' => { + if from_start.as_bytes().get(offset + 1) != Some(&b'\n') { + newlines += 1; + last_newline = offset; + } + } + b'"' | b'\'' => return Err(()), // Do not advance + b')' => { + // Don't use advance, because we may be skipping + // newlines here, and we want to avoid the assert. + tokenizer.position += offset + 1; + break + } + _ => { + // Don't use advance, because we may be skipping + // newlines here, and we want to avoid the assert. + tokenizer.position += offset; + found_printable_char = true; + break + } + } + } + + if newlines > 0 { + tokenizer.current_line_number += newlines; + // No need for wrapping_add here, because there's no possible + // way to wrap. + tokenizer.current_line_start_position = start_position + last_newline + 1; + } + + if found_printable_char { + // This function only consumed ASCII (whitespace) bytes, + // so the current position is a code point boundary. + return Ok(consume_unquoted_url_internal(tokenizer)); + } else { + return Ok(UnquotedUrl("".into())); + } + + fn consume_unquoted_url_internal<'a>(tokenizer: &mut Tokenizer<'a>) -> Token<'a> { + // This function is only called with start_pos at a code point boundary. + let start_pos = tokenizer.position(); + let mut string_bytes: Vec<u8>; + loop { + if tokenizer.is_eof() { + return UnquotedUrl(tokenizer.slice_from(start_pos).into()); + } + match_byte! { tokenizer.next_byte_unchecked(), + b' ' | b'\t' | b'\n' | b'\r' | b'\x0C' => { + let value = tokenizer.slice_from(start_pos); + return consume_url_end(tokenizer, start_pos, value.into()) + } + b')' => { + let value = tokenizer.slice_from(start_pos); + tokenizer.advance(1); + return UnquotedUrl(value.into()) + } + b'\x01'..=b'\x08' | b'\x0B' | b'\x0E'..=b'\x1F' | b'\x7F' // non-printable + | b'"' | b'\'' | b'(' => { + tokenizer.advance(1); + return consume_bad_url(tokenizer, start_pos) + }, + b'\\' | b'\0' => { + // * The tokenizer’s input is UTF-8 since it’s `&str`. + // * start_pos is at a code point boundary + // * so is the current position (which is before '\\' or '\0' + // + // So `string_bytes` is well-formed UTF-8. + string_bytes = tokenizer.slice_from(start_pos).as_bytes().to_owned(); + break + } + b'\x80'..=b'\xBF' => { tokenizer.consume_continuation_byte(); } + b'\xF0'..=b'\xFF' => { tokenizer.consume_4byte_intro(); } + _ => { + // ASCII or other leading byte. + tokenizer.advance(1); + } + } + } + while !tokenizer.is_eof() { + let b = tokenizer.next_byte_unchecked(); + match_byte! { b, + b' ' | b'\t' | b'\n' | b'\r' | b'\x0C' => { + // string_bytes is well-formed UTF-8, see other comments. + let string = unsafe { from_utf8_release_unchecked(string_bytes) }.into(); + return consume_url_end(tokenizer, start_pos, string) + } + b')' => { + tokenizer.advance(1); + break; + } + b'\x01'..=b'\x08' | b'\x0B' | b'\x0E'..=b'\x1F' | b'\x7F' // non-printable + | b'"' | b'\'' | b'(' => { + tokenizer.advance(1); + return consume_bad_url(tokenizer, start_pos); + } + b'\\' => { + tokenizer.advance(1); + if tokenizer.has_newline_at(0) { + return consume_bad_url(tokenizer, start_pos) + } + + // This pushes one well-formed code point to string_bytes + consume_escape_and_write(tokenizer, &mut string_bytes) + }, + b'\0' => { + tokenizer.advance(1); + string_bytes.extend("\u{FFFD}".as_bytes()); + } + b'\x80'..=b'\xBF' => { + // We’ll end up copying the whole code point + // before this loop does something else. + tokenizer.consume_continuation_byte(); + string_bytes.push(b); + } + b'\xF0'..=b'\xFF' => { + // We’ll end up copying the whole code point + // before this loop does something else. + tokenizer.consume_4byte_intro(); + string_bytes.push(b); + } + // If this byte is part of a multi-byte code point, + // we’ll end up copying the whole code point before this loop does something else. + b => { + // ASCII or other leading byte. + tokenizer.advance(1); + string_bytes.push(b) + } + } + } + UnquotedUrl( + // string_bytes is well-formed UTF-8, see other comments. + unsafe { from_utf8_release_unchecked(string_bytes) }.into(), + ) + } + + fn consume_url_end<'a>( + tokenizer: &mut Tokenizer<'a>, + start_pos: SourcePosition, + string: CowRcStr<'a>, + ) -> Token<'a> { + while !tokenizer.is_eof() { + match_byte! { tokenizer.next_byte_unchecked(), + b')' => { + tokenizer.advance(1); + break + } + b' ' | b'\t' => { tokenizer.advance(1); } + b'\n' | b'\x0C' | b'\r' => { + tokenizer.consume_newline(); + } + b => { + tokenizer.consume_known_byte(b); + return consume_bad_url(tokenizer, start_pos); + } + } + } + UnquotedUrl(string) + } + + fn consume_bad_url<'a>(tokenizer: &mut Tokenizer<'a>, start_pos: SourcePosition) -> Token<'a> { + // Consume up to the closing ) + while !tokenizer.is_eof() { + match_byte! { tokenizer.next_byte_unchecked(), + b')' => { + let contents = tokenizer.slice_from(start_pos).into(); + tokenizer.advance(1); + return BadUrl(contents) + } + b'\\' => { + tokenizer.advance(1); + if matches!(tokenizer.next_byte(), Some(b')') | Some(b'\\')) { + tokenizer.advance(1); // Skip an escaped ')' or '\' + } + } + b'\n' | b'\x0C' | b'\r' => { + tokenizer.consume_newline(); + } + b => { + tokenizer.consume_known_byte(b); + } + } + } + BadUrl(tokenizer.slice_from(start_pos).into()) + } +} + +// (value, number of digits up to 6) +fn consume_hex_digits<'a>(tokenizer: &mut Tokenizer<'a>) -> (u32, u32) { + let mut value = 0; + let mut digits = 0; + while digits < 6 && !tokenizer.is_eof() { + match byte_to_hex_digit(tokenizer.next_byte_unchecked()) { + Some(digit) => { + value = value * 16 + digit; + digits += 1; + tokenizer.advance(1); + } + None => break, + } + } + (value, digits) +} + +// Same constraints as consume_escape except it writes into `bytes` the result +// instead of returning it. +fn consume_escape_and_write(tokenizer: &mut Tokenizer, bytes: &mut Vec<u8>) { + bytes.extend( + consume_escape(tokenizer) + .encode_utf8(&mut [0; 4]) + .as_bytes(), + ) +} + +// Assumes that the U+005C REVERSE SOLIDUS (\) has already been consumed +// and that the next input character has already been verified +// to not be a newline. +fn consume_escape(tokenizer: &mut Tokenizer) -> char { + if tokenizer.is_eof() { + return '\u{FFFD}'; + } // Escaped EOF + match_byte! { tokenizer.next_byte_unchecked(), + b'0'..=b'9' | b'A'..=b'F' | b'a'..=b'f' => { + let (c, _) = consume_hex_digits(tokenizer); + if !tokenizer.is_eof() { + match_byte! { tokenizer.next_byte_unchecked(), + b' ' | b'\t' => { + tokenizer.advance(1) + } + b'\n' | b'\x0C' | b'\r' => { + tokenizer.consume_newline(); + } + _ => {} + } + } + static REPLACEMENT_CHAR: char = '\u{FFFD}'; + if c != 0 { + let c = char::from_u32(c); + c.unwrap_or(REPLACEMENT_CHAR) + } else { + REPLACEMENT_CHAR + } + }, + b'\0' => { + tokenizer.advance(1); + '\u{FFFD}' + } + _ => tokenizer.consume_char(), + } +} diff --git a/third_party/rust/cssparser/src/unicode_range.rs b/third_party/rust/cssparser/src/unicode_range.rs new file mode 100644 index 0000000000..b0a2017cbf --- /dev/null +++ b/third_party/rust/cssparser/src/unicode_range.rs @@ -0,0 +1,181 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +//! https://drafts.csswg.org/css-syntax/#urange + +use crate::tokenizer::Token; +use crate::{BasicParseError, Parser, ToCss}; +use std::char; +use std::fmt; + +/// One contiguous range of code points. +/// +/// Can not be empty. Can represent a single code point when start == end. +#[derive(PartialEq, Eq, Clone, Hash)] +#[repr(C)] +pub struct UnicodeRange { + /// Inclusive start of the range. In [0, end]. + pub start: u32, + + /// Inclusive end of the range. In [0, 0x10FFFF]. + pub end: u32, +} + +impl UnicodeRange { + /// https://drafts.csswg.org/css-syntax/#urange-syntax + pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, BasicParseError<'i>> { + // <urange> = + // u '+' <ident-token> '?'* | + // u <dimension-token> '?'* | + // u <number-token> '?'* | + // u <number-token> <dimension-token> | + // u <number-token> <number-token> | + // u '+' '?'+ + + input.expect_ident_matching("u")?; + let after_u = input.position(); + parse_tokens(input)?; + + // This deviates from the spec in case there are CSS comments + // between tokens in the middle of one <unicode-range>, + // but oh well… + let concatenated_tokens = input.slice_from(after_u); + + let range = match parse_concatenated(concatenated_tokens.as_bytes()) { + Ok(range) => range, + Err(()) => { + return Err(input + .new_basic_unexpected_token_error(Token::Ident(concatenated_tokens.into()))) + } + }; + if range.end > char::MAX as u32 || range.start > range.end { + Err(input.new_basic_unexpected_token_error(Token::Ident(concatenated_tokens.into()))) + } else { + Ok(range) + } + } +} + +fn parse_tokens<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), BasicParseError<'i>> { + match input.next_including_whitespace()?.clone() { + Token::Delim('+') => { + match *input.next_including_whitespace()? { + Token::Ident(_) => {} + Token::Delim('?') => {} + ref t => { + let t = t.clone(); + return Err(input.new_basic_unexpected_token_error(t)); + } + } + parse_question_marks(input) + } + Token::Dimension { .. } => parse_question_marks(input), + Token::Number { .. } => { + let after_number = input.state(); + match input.next_including_whitespace() { + Ok(&Token::Delim('?')) => parse_question_marks(input), + Ok(&Token::Dimension { .. }) => {} + Ok(&Token::Number { .. }) => {} + _ => input.reset(&after_number), + } + } + t => return Err(input.new_basic_unexpected_token_error(t)), + } + Ok(()) +} + +/// Consume as many '?' as possible +fn parse_question_marks(input: &mut Parser) { + loop { + let start = input.state(); + match input.next_including_whitespace() { + Ok(&Token::Delim('?')) => {} + _ => { + input.reset(&start); + return; + } + } + } +} + +fn parse_concatenated(text: &[u8]) -> Result<UnicodeRange, ()> { + let mut text = match text.split_first() { + Some((&b'+', text)) => text, + _ => return Err(()), + }; + let (first_hex_value, hex_digit_count) = consume_hex(&mut text); + let question_marks = consume_question_marks(&mut text); + let consumed = hex_digit_count + question_marks; + if consumed == 0 || consumed > 6 { + return Err(()); + } + + if question_marks > 0 { + if text.is_empty() { + return Ok(UnicodeRange { + start: first_hex_value << (question_marks * 4), + end: ((first_hex_value + 1) << (question_marks * 4)) - 1, + }); + } + } else if text.is_empty() { + return Ok(UnicodeRange { + start: first_hex_value, + end: first_hex_value, + }); + } else { + if let Some((&b'-', mut text)) = text.split_first() { + let (second_hex_value, hex_digit_count) = consume_hex(&mut text); + if hex_digit_count > 0 && hex_digit_count <= 6 && text.is_empty() { + return Ok(UnicodeRange { + start: first_hex_value, + end: second_hex_value, + }); + } + } + } + Err(()) +} + +fn consume_hex(text: &mut &[u8]) -> (u32, usize) { + let mut value = 0; + let mut digits = 0; + while let Some((&byte, rest)) = text.split_first() { + if let Some(digit_value) = (byte as char).to_digit(16) { + value = value * 0x10 + digit_value; + digits += 1; + *text = rest + } else { + break; + } + } + (value, digits) +} + +fn consume_question_marks(text: &mut &[u8]) -> usize { + let mut question_marks = 0; + while let Some((&b'?', rest)) = text.split_first() { + question_marks += 1; + *text = rest + } + question_marks +} + +impl fmt::Debug for UnicodeRange { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + self.to_css(formatter) + } +} + +impl ToCss for UnicodeRange { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + write!(dest, "U+{:X}", self.start)?; + if self.end != self.start { + write!(dest, "-{:X}", self.end)?; + } + Ok(()) + } +} |