tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

parsing.rs (19987B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 #![deny(missing_docs)]
      6 
      7 //! Parsing for CSS colors.
      8 
      9 use super::{
     10    color_function::ColorFunction,
     11    component::{ColorComponent, ColorComponentType},
     12    AbsoluteColor,
     13 };
     14 use crate::derives::*;
     15 use crate::{
     16    parser::{Parse, ParserContext},
     17    values::{
     18        generics::{calc::CalcUnits, Optional},
     19        specified::{angle::Angle as SpecifiedAngle, calc::Leaf, color::Color as SpecifiedColor},
     20    },
     21 };
     22 use cssparser::{
     23    color::{parse_hash_color, PredefinedColorSpace, OPAQUE},
     24    match_ignore_ascii_case, CowRcStr, Parser, Token,
     25 };
     26 use style_traits::{ParseError, StyleParseErrorKind};
     27 
     28 /// Returns true if the relative color syntax pref is enabled.
     29 #[inline]
     30 pub fn rcs_enabled() -> bool {
     31    static_prefs::pref!("layout.css.relative-color-syntax.enabled")
     32 }
     33 
     34 /// Represents a channel keyword inside a color.
     35 #[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, PartialOrd, ToCss, ToShmem)]
     36 #[repr(u8)]
     37 pub enum ChannelKeyword {
     38    /// alpha
     39    Alpha,
     40    /// a
     41    A,
     42    /// b, blackness, blue
     43    B,
     44    /// chroma
     45    C,
     46    /// green
     47    G,
     48    /// hue
     49    H,
     50    /// lightness
     51    L,
     52    /// red
     53    R,
     54    /// saturation
     55    S,
     56    /// whiteness
     57    W,
     58    /// x
     59    X,
     60    /// y
     61    Y,
     62    /// z
     63    Z,
     64 }
     65 
     66 /// Return the named color with the given name.
     67 ///
     68 /// Matching is case-insensitive in the ASCII range.
     69 /// CSS escaping (if relevant) should be resolved before calling this function.
     70 /// (For example, the value of an `Ident` token is fine.)
     71 #[inline]
     72 pub fn parse_color_keyword(ident: &str) -> Result<SpecifiedColor, ()> {
     73    Ok(match_ignore_ascii_case! { ident,
     74        "transparent" => {
     75            SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(0u8, 0u8, 0u8, 0.0))
     76        },
     77        "currentcolor" => SpecifiedColor::CurrentColor,
     78        _ => {
     79            let (r, g, b) = cssparser::color::parse_named_color(ident)?;
     80            SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(r, g, b, OPAQUE))
     81        },
     82    })
     83 }
     84 
     85 /// Parse a CSS color using the specified [`ColorParser`] and return a new color
     86 /// value on success.
     87 pub fn parse_color_with<'i, 't>(
     88    context: &ParserContext,
     89    input: &mut Parser<'i, 't>,
     90 ) -> Result<SpecifiedColor, ParseError<'i>> {
     91    let location = input.current_source_location();
     92    let token = input.next()?;
     93    match *token {
     94        Token::Hash(ref value) | Token::IDHash(ref value) => parse_hash_color(value.as_bytes())
     95            .map(|(r, g, b, a)| {
     96                SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(r, g, b, a))
     97            }),
     98        Token::Ident(ref value) => parse_color_keyword(value),
     99        Token::Function(ref name) => {
    100            let name = name.clone();
    101            return input.parse_nested_block(|arguments| {
    102                let color_function = parse_color_function(context, name, arguments)?;
    103 
    104                if color_function.has_origin_color() {
    105                    // Preserve the color as it was parsed.
    106                    Ok(SpecifiedColor::ColorFunction(Box::new(color_function)))
    107                } else if let Ok(resolved) = color_function.resolve_to_absolute() {
    108                    Ok(SpecifiedColor::from_absolute_color(resolved))
    109                } else {
    110                    // This will only happen when the parsed color contains errors like calc units
    111                    // that cannot be resolved at parse time, but will fail when trying to resolve
    112                    // them, etc. This should be rare, but for now just failing the color value
    113                    // makes sense.
    114                    Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError))
    115                }
    116            });
    117        },
    118        _ => Err(()),
    119    }
    120    .map_err(|()| location.new_unexpected_token_error(token.clone()))
    121 }
    122 
    123 /// Parse one of the color functions: rgba(), lab(), color(), etc.
    124 #[inline]
    125 fn parse_color_function<'i, 't>(
    126    context: &ParserContext,
    127    name: CowRcStr<'i>,
    128    arguments: &mut Parser<'i, 't>,
    129 ) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
    130    let origin_color = parse_origin_color(context, arguments)?;
    131    let has_origin_color = origin_color.is_some();
    132 
    133    let color = match_ignore_ascii_case! { &name,
    134        "rgb" | "rgba" => parse_rgb(context, arguments, origin_color),
    135        "hsl" | "hsla" => parse_hsl(context, arguments, origin_color),
    136        "hwb" => parse_hwb(context, arguments, origin_color),
    137        "lab" => parse_lab_like(context, arguments, origin_color, ColorFunction::Lab),
    138        "lch" => parse_lch_like(context, arguments, origin_color, ColorFunction::Lch),
    139        "oklab" => parse_lab_like(context, arguments, origin_color, ColorFunction::Oklab),
    140        "oklch" => parse_lch_like(context, arguments, origin_color, ColorFunction::Oklch),
    141        "color" => parse_color_with_color_space(context, arguments, origin_color),
    142        _ => return Err(arguments.new_unexpected_token_error(Token::Ident(name))),
    143    }?;
    144 
    145    if has_origin_color {
    146        // Validate the channels and calc expressions by trying to resolve them against
    147        // transparent.
    148        // FIXME(emilio, bug 1925572): This could avoid cloning, or be done earlier.
    149        let abs = color.map_origin_color(|_| Some(AbsoluteColor::TRANSPARENT_BLACK));
    150        if abs.resolve_to_absolute().is_err() {
    151            return Err(arguments.new_custom_error(StyleParseErrorKind::UnspecifiedError));
    152        }
    153    }
    154 
    155    arguments.expect_exhausted()?;
    156 
    157    Ok(color)
    158 }
    159 
    160 /// Parse the relative color syntax "from" syntax `from <color>`.
    161 fn parse_origin_color<'i, 't>(
    162    context: &ParserContext,
    163    arguments: &mut Parser<'i, 't>,
    164 ) -> Result<Option<SpecifiedColor>, ParseError<'i>> {
    165    if !rcs_enabled() {
    166        return Ok(None);
    167    }
    168 
    169    // Not finding the from keyword is not an error, it just means we don't
    170    // have an origin color.
    171    if arguments
    172        .try_parse(|p| p.expect_ident_matching("from"))
    173        .is_err()
    174    {
    175        return Ok(None);
    176    }
    177 
    178    SpecifiedColor::parse(context, arguments).map(Option::Some)
    179 }
    180 
    181 #[inline]
    182 fn parse_rgb<'i, 't>(
    183    context: &ParserContext,
    184    arguments: &mut Parser<'i, 't>,
    185    origin_color: Option<SpecifiedColor>,
    186 ) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
    187    let maybe_red = parse_number_or_percentage(context, arguments, true)?;
    188 
    189    // If the first component is not "none" and is followed by a comma, then we
    190    // are parsing the legacy syntax.  Legacy syntax also doesn't support an
    191    // origin color.
    192    let is_legacy_syntax = origin_color.is_none()
    193        && !maybe_red.is_none()
    194        && arguments.try_parse(|p| p.expect_comma()).is_ok();
    195 
    196    Ok(if is_legacy_syntax {
    197        let (green, blue) = if maybe_red.could_be_percentage() {
    198            let green = parse_percentage(context, arguments, false)?;
    199            arguments.expect_comma()?;
    200            let blue = parse_percentage(context, arguments, false)?;
    201            (green, blue)
    202        } else {
    203            let green = parse_number(context, arguments, false)?;
    204            arguments.expect_comma()?;
    205            let blue = parse_number(context, arguments, false)?;
    206            (green, blue)
    207        };
    208 
    209        let alpha = parse_legacy_alpha(context, arguments)?;
    210 
    211        ColorFunction::Rgb(origin_color.into(), maybe_red, green, blue, alpha)
    212    } else {
    213        let green = parse_number_or_percentage(context, arguments, true)?;
    214        let blue = parse_number_or_percentage(context, arguments, true)?;
    215 
    216        let alpha = parse_modern_alpha(context, arguments)?;
    217 
    218        ColorFunction::Rgb(origin_color.into(), maybe_red, green, blue, alpha)
    219    })
    220 }
    221 
    222 /// Parses hsl syntax.
    223 ///
    224 /// <https://drafts.csswg.org/css-color/#the-hsl-notation>
    225 #[inline]
    226 fn parse_hsl<'i, 't>(
    227    context: &ParserContext,
    228    arguments: &mut Parser<'i, 't>,
    229    origin_color: Option<SpecifiedColor>,
    230 ) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
    231    let hue = parse_number_or_angle(context, arguments, true)?;
    232 
    233    // If the hue is not "none" and is followed by a comma, then we are parsing
    234    // the legacy syntax. Legacy syntax also doesn't support an origin color.
    235    let is_legacy_syntax = origin_color.is_none()
    236        && !hue.is_none()
    237        && arguments.try_parse(|p| p.expect_comma()).is_ok();
    238 
    239    let (saturation, lightness, alpha) = if is_legacy_syntax {
    240        let saturation = parse_percentage(context, arguments, false)?;
    241        arguments.expect_comma()?;
    242        let lightness = parse_percentage(context, arguments, false)?;
    243        let alpha = parse_legacy_alpha(context, arguments)?;
    244        (saturation, lightness, alpha)
    245    } else {
    246        let saturation = parse_number_or_percentage(context, arguments, true)?;
    247        let lightness = parse_number_or_percentage(context, arguments, true)?;
    248        let alpha = parse_modern_alpha(context, arguments)?;
    249        (saturation, lightness, alpha)
    250    };
    251 
    252    Ok(ColorFunction::Hsl(
    253        origin_color.into(),
    254        hue,
    255        saturation,
    256        lightness,
    257        alpha,
    258    ))
    259 }
    260 
    261 /// Parses hwb syntax.
    262 ///
    263 /// <https://drafts.csswg.org/css-color/#the-hbw-notation>
    264 #[inline]
    265 fn parse_hwb<'i, 't>(
    266    context: &ParserContext,
    267    arguments: &mut Parser<'i, 't>,
    268    origin_color: Option<SpecifiedColor>,
    269 ) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
    270    let hue = parse_number_or_angle(context, arguments, true)?;
    271    let whiteness = parse_number_or_percentage(context, arguments, true)?;
    272    let blackness = parse_number_or_percentage(context, arguments, true)?;
    273 
    274    let alpha = parse_modern_alpha(context, arguments)?;
    275 
    276    Ok(ColorFunction::Hwb(
    277        origin_color.into(),
    278        hue,
    279        whiteness,
    280        blackness,
    281        alpha,
    282    ))
    283 }
    284 
    285 type IntoLabFn<Output> = fn(
    286    origin: Optional<SpecifiedColor>,
    287    l: ColorComponent<NumberOrPercentageComponent>,
    288    a: ColorComponent<NumberOrPercentageComponent>,
    289    b: ColorComponent<NumberOrPercentageComponent>,
    290    alpha: ColorComponent<NumberOrPercentageComponent>,
    291 ) -> Output;
    292 
    293 #[inline]
    294 fn parse_lab_like<'i, 't>(
    295    context: &ParserContext,
    296    arguments: &mut Parser<'i, 't>,
    297    origin_color: Option<SpecifiedColor>,
    298    into_color: IntoLabFn<ColorFunction<SpecifiedColor>>,
    299 ) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
    300    let lightness = parse_number_or_percentage(context, arguments, true)?;
    301    let a = parse_number_or_percentage(context, arguments, true)?;
    302    let b = parse_number_or_percentage(context, arguments, true)?;
    303 
    304    let alpha = parse_modern_alpha(context, arguments)?;
    305 
    306    Ok(into_color(origin_color.into(), lightness, a, b, alpha))
    307 }
    308 
    309 type IntoLchFn<Output> = fn(
    310    origin: Optional<SpecifiedColor>,
    311    l: ColorComponent<NumberOrPercentageComponent>,
    312    a: ColorComponent<NumberOrPercentageComponent>,
    313    b: ColorComponent<NumberOrAngleComponent>,
    314    alpha: ColorComponent<NumberOrPercentageComponent>,
    315 ) -> Output;
    316 
    317 #[inline]
    318 fn parse_lch_like<'i, 't>(
    319    context: &ParserContext,
    320    arguments: &mut Parser<'i, 't>,
    321    origin_color: Option<SpecifiedColor>,
    322    into_color: IntoLchFn<ColorFunction<SpecifiedColor>>,
    323 ) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
    324    let lightness = parse_number_or_percentage(context, arguments, true)?;
    325    let chroma = parse_number_or_percentage(context, arguments, true)?;
    326    let hue = parse_number_or_angle(context, arguments, true)?;
    327 
    328    let alpha = parse_modern_alpha(context, arguments)?;
    329 
    330    Ok(into_color(
    331        origin_color.into(),
    332        lightness,
    333        chroma,
    334        hue,
    335        alpha,
    336    ))
    337 }
    338 
    339 /// Parse the color() function.
    340 #[inline]
    341 fn parse_color_with_color_space<'i, 't>(
    342    context: &ParserContext,
    343    arguments: &mut Parser<'i, 't>,
    344    origin_color: Option<SpecifiedColor>,
    345 ) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
    346    let color_space = PredefinedColorSpace::parse(arguments)?;
    347 
    348    let c1 = parse_number_or_percentage(context, arguments, true)?;
    349    let c2 = parse_number_or_percentage(context, arguments, true)?;
    350    let c3 = parse_number_or_percentage(context, arguments, true)?;
    351 
    352    let alpha = parse_modern_alpha(context, arguments)?;
    353 
    354    Ok(ColorFunction::Color(
    355        origin_color.into(),
    356        c1,
    357        c2,
    358        c3,
    359        alpha,
    360        color_space.into(),
    361    ))
    362 }
    363 
    364 /// Either a percentage or a number.
    365 #[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
    366 #[repr(u8)]
    367 pub enum NumberOrPercentageComponent {
    368    /// `<number>`.
    369    Number(f32),
    370    /// `<percentage>`
    371    /// The value as a float, divided by 100 so that the nominal range is 0.0 to 1.0.
    372    Percentage(f32),
    373 }
    374 
    375 impl NumberOrPercentageComponent {
    376    /// Return the value as a number. Percentages will be adjusted to the range
    377    /// [0..percent_basis].
    378    pub fn to_number(&self, percentage_basis: f32) -> f32 {
    379        match *self {
    380            Self::Number(value) => value,
    381            Self::Percentage(unit_value) => unit_value * percentage_basis,
    382        }
    383    }
    384 }
    385 
    386 impl ColorComponentType for NumberOrPercentageComponent {
    387    fn from_value(value: f32) -> Self {
    388        Self::Number(value)
    389    }
    390 
    391    fn units() -> CalcUnits {
    392        CalcUnits::PERCENTAGE
    393    }
    394 
    395    fn try_from_token(token: &Token) -> Result<Self, ()> {
    396        Ok(match *token {
    397            Token::Number { value, .. } => Self::Number(value),
    398            Token::Percentage { unit_value, .. } => Self::Percentage(unit_value),
    399            _ => {
    400                return Err(());
    401            },
    402        })
    403    }
    404 
    405    fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
    406        Ok(match *leaf {
    407            Leaf::Percentage(unit_value) => Self::Percentage(unit_value),
    408            Leaf::Number(value) => Self::Number(value),
    409            _ => return Err(()),
    410        })
    411    }
    412 }
    413 
    414 /// Either an angle or a number.
    415 #[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
    416 #[repr(u8)]
    417 pub enum NumberOrAngleComponent {
    418    /// `<number>`.
    419    Number(f32),
    420    /// `<angle>`
    421    /// The value as a number of degrees.
    422    Angle(f32),
    423 }
    424 
    425 impl NumberOrAngleComponent {
    426    /// Return the angle in degrees. `NumberOrAngle::Number` is returned as
    427    /// degrees, because it is the canonical unit.
    428    pub fn degrees(&self) -> f32 {
    429        match *self {
    430            Self::Number(value) => value,
    431            Self::Angle(degrees) => degrees,
    432        }
    433    }
    434 }
    435 
    436 impl ColorComponentType for NumberOrAngleComponent {
    437    fn from_value(value: f32) -> Self {
    438        Self::Number(value)
    439    }
    440 
    441    fn units() -> CalcUnits {
    442        CalcUnits::ANGLE
    443    }
    444 
    445    fn try_from_token(token: &Token) -> Result<Self, ()> {
    446        Ok(match *token {
    447            Token::Number { value, .. } => Self::Number(value),
    448            Token::Dimension {
    449                value, ref unit, ..
    450            } => {
    451                let degrees =
    452                    SpecifiedAngle::parse_dimension(value, unit, /* from_calc = */ false)
    453                        .map(|angle| angle.degrees())?;
    454 
    455                NumberOrAngleComponent::Angle(degrees)
    456            },
    457            _ => {
    458                return Err(());
    459            },
    460        })
    461    }
    462 
    463    fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
    464        Ok(match *leaf {
    465            Leaf::Angle(angle) => Self::Angle(angle.degrees()),
    466            Leaf::Number(value) => Self::Number(value),
    467            _ => return Err(()),
    468        })
    469    }
    470 }
    471 
    472 /// The raw f32 here is for <number>.
    473 impl ColorComponentType for f32 {
    474    fn from_value(value: f32) -> Self {
    475        value
    476    }
    477 
    478    fn units() -> CalcUnits {
    479        CalcUnits::empty()
    480    }
    481 
    482    fn try_from_token(token: &Token) -> Result<Self, ()> {
    483        if let Token::Number { value, .. } = *token {
    484            Ok(value)
    485        } else {
    486            Err(())
    487        }
    488    }
    489 
    490    fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
    491        if let Leaf::Number(value) = *leaf {
    492            Ok(value)
    493        } else {
    494            Err(())
    495        }
    496    }
    497 }
    498 
    499 /// Parse an `<number>` or `<angle>` value.
    500 fn parse_number_or_angle<'i, 't>(
    501    context: &ParserContext,
    502    input: &mut Parser<'i, 't>,
    503    allow_none: bool,
    504 ) -> Result<ColorComponent<NumberOrAngleComponent>, ParseError<'i>> {
    505    ColorComponent::parse(context, input, allow_none)
    506 }
    507 
    508 /// Parse a `<percentage>` value.
    509 fn parse_percentage<'i, 't>(
    510    context: &ParserContext,
    511    input: &mut Parser<'i, 't>,
    512    allow_none: bool,
    513 ) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
    514    let location = input.current_source_location();
    515 
    516    let value = ColorComponent::<NumberOrPercentageComponent>::parse(context, input, allow_none)?;
    517    if !value.could_be_percentage() {
    518        return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
    519    }
    520 
    521    Ok(value)
    522 }
    523 
    524 /// Parse a `<number>` value.
    525 fn parse_number<'i, 't>(
    526    context: &ParserContext,
    527    input: &mut Parser<'i, 't>,
    528    allow_none: bool,
    529 ) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
    530    let location = input.current_source_location();
    531 
    532    let value = ColorComponent::<NumberOrPercentageComponent>::parse(context, input, allow_none)?;
    533 
    534    if !value.could_be_number() {
    535        return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
    536    }
    537 
    538    Ok(value)
    539 }
    540 
    541 /// Parse a `<number>` or `<percentage>` value.
    542 fn parse_number_or_percentage<'i, 't>(
    543    context: &ParserContext,
    544    input: &mut Parser<'i, 't>,
    545    allow_none: bool,
    546 ) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
    547    ColorComponent::parse(context, input, allow_none)
    548 }
    549 
    550 fn parse_legacy_alpha<'i, 't>(
    551    context: &ParserContext,
    552    arguments: &mut Parser<'i, 't>,
    553 ) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
    554    if !arguments.is_exhausted() {
    555        arguments.expect_comma()?;
    556        parse_number_or_percentage(context, arguments, false)
    557    } else {
    558        Ok(ColorComponent::AlphaOmitted)
    559    }
    560 }
    561 
    562 fn parse_modern_alpha<'i, 't>(
    563    context: &ParserContext,
    564    arguments: &mut Parser<'i, 't>,
    565 ) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
    566    if !arguments.is_exhausted() {
    567        arguments.expect_delim('/')?;
    568        parse_number_or_percentage(context, arguments, true)
    569    } else {
    570        Ok(ColorComponent::AlphaOmitted)
    571    }
    572 }
    573 
    574 impl ColorComponent<NumberOrPercentageComponent> {
    575    /// Return true if the value contained inside is/can resolve to a number.
    576    /// Also returns false if the node is invalid somehow.
    577    fn could_be_number(&self) -> bool {
    578        match self {
    579            Self::None | Self::AlphaOmitted => true,
    580            Self::Value(value) => matches!(value, NumberOrPercentageComponent::Number { .. }),
    581            Self::ChannelKeyword(_) => {
    582                // Channel keywords always resolve to numbers.
    583                true
    584            },
    585            Self::Calc(node) => {
    586                if let Ok(unit) = node.unit() {
    587                    unit.is_empty()
    588                } else {
    589                    false
    590                }
    591            },
    592        }
    593    }
    594 
    595    /// Return true if the value contained inside is/can resolve to a percentage.
    596    /// Also returns false if the node is invalid somehow.
    597    fn could_be_percentage(&self) -> bool {
    598        match self {
    599            Self::None | Self::AlphaOmitted => true,
    600            Self::Value(value) => matches!(value, NumberOrPercentageComponent::Percentage { .. }),
    601            Self::ChannelKeyword(_) => {
    602                // Channel keywords always resolve to numbers.
    603                false
    604            },
    605            Self::Calc(node) => {
    606                if let Ok(unit) = node.unit() {
    607                    unit == CalcUnits::PERCENTAGE
    608                } else {
    609                    false
    610                }
    611            },
    612        }
    613    }
    614 }