style/color/
parsing.rs

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
9use super::{
10    color_function::ColorFunction,
11    component::{ColorComponent, ColorComponentType},
12    AbsoluteColor,
13};
14use crate::derives::*;
15use 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};
22use cssparser::{
23    color::{parse_hash_color, PredefinedColorSpace, OPAQUE},
24    match_ignore_ascii_case, CowRcStr, Parser, Token,
25};
26use style_traits::{ParseError, StyleParseErrorKind};
27
28/// Returns true if the relative color syntax pref is enabled.
29#[inline]
30pub 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)]
37pub 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]
72pub 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.
87pub 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]
125fn 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>`.
161fn 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]
182fn 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]
226fn 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]
265fn 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
285type 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]
294fn 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
309type 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]
318fn 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]
341fn 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)]
367pub 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
375impl 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
386impl 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)]
417pub enum NumberOrAngleComponent {
418    /// `<number>`.
419    Number(f32),
420    /// `<angle>`
421    /// The value as a number of degrees.
422    Angle(f32),
423}
424
425impl 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
436impl 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>.
473impl 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.
500fn 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.
509fn 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.
525fn 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.
542fn 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
550fn 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
562fn 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
574impl 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}