Skip to main content

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::NoCalcAngle, 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(None) {
108                    Ok(SpecifiedColor::from_absolute_color(resolved))
109                } else {
110                    // The color could not be eagerly resolved, so preserve the original color.
111                    Ok(SpecifiedColor::ColorFunction(Box::new(color_function)))
112                }
113            });
114        },
115        _ => Err(()),
116    }
117    .map_err(|()| location.new_unexpected_token_error(token.clone()))
118}
119
120/// Parse one of the color functions: rgba(), lab(), color(), etc.
121#[inline]
122fn parse_color_function<'i, 't>(
123    context: &ParserContext,
124    name: CowRcStr<'i>,
125    arguments: &mut Parser<'i, 't>,
126) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
127    let origin_color = parse_origin_color(context, arguments)?;
128    let has_origin_color = origin_color.is_some();
129
130    let color = match_ignore_ascii_case! { &name,
131        "rgb" | "rgba" => parse_rgb(context, arguments, origin_color),
132        "hsl" | "hsla" => parse_hsl(context, arguments, origin_color),
133        "hwb" => parse_hwb(context, arguments, origin_color),
134        "lab" => parse_lab_like(context, arguments, origin_color, ColorFunction::Lab),
135        "lch" => parse_lch_like(context, arguments, origin_color, ColorFunction::Lch),
136        "oklab" => parse_lab_like(context, arguments, origin_color, ColorFunction::Oklab),
137        "oklch" => parse_lch_like(context, arguments, origin_color, ColorFunction::Oklch),
138        "color" => parse_color_with_color_space(context, arguments, origin_color),
139        _ => return Err(arguments.new_unexpected_token_error(Token::Ident(name))),
140    }?;
141
142    if has_origin_color {
143        // Validate the channels and calc expressions by trying to resolve them against
144        // transparent.
145        // FIXME(emilio, bug 1925572): This could avoid cloning, or be done earlier.
146        let abs = color
147            .map_origin_color(|_| Ok(AbsoluteColor::TRANSPARENT_BLACK))
148            .unwrap();
149        if abs.resolve_to_absolute(None).is_err() {
150            return Err(arguments.new_custom_error(StyleParseErrorKind::UnspecifiedError));
151        }
152    }
153
154    arguments.expect_exhausted()?;
155
156    Ok(color)
157}
158
159/// Parse the relative color syntax "from" syntax `from <color>`.
160fn parse_origin_color<'i, 't>(
161    context: &ParserContext,
162    arguments: &mut Parser<'i, 't>,
163) -> Result<Option<SpecifiedColor>, ParseError<'i>> {
164    if !rcs_enabled() {
165        return Ok(None);
166    }
167
168    // Not finding the from keyword is not an error, it just means we don't
169    // have an origin color.
170    if arguments
171        .try_parse(|p| p.expect_ident_matching("from"))
172        .is_err()
173    {
174        return Ok(None);
175    }
176
177    SpecifiedColor::parse(context, arguments).map(Option::Some)
178}
179
180#[inline]
181fn parse_rgb<'i, 't>(
182    context: &ParserContext,
183    arguments: &mut Parser<'i, 't>,
184    origin_color: Option<SpecifiedColor>,
185) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
186    let allow_channel_keyword = origin_color.is_some();
187    let maybe_red = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
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, allow_channel_keyword)?;
199            arguments.expect_comma()?;
200            let blue = parse_percentage(context, arguments, false, allow_channel_keyword)?;
201            (green, blue)
202        } else {
203            let green = parse_number(context, arguments, false, allow_channel_keyword)?;
204            arguments.expect_comma()?;
205            let blue = parse_number(context, arguments, false, allow_channel_keyword)?;
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, allow_channel_keyword)?;
214        let blue = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
215
216        let alpha = parse_modern_alpha(context, arguments, allow_channel_keyword)?;
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 allow_channel_keyword = origin_color.is_some();
232    let hue = parse_number_or_angle(context, arguments, true, allow_channel_keyword)?;
233
234    // If the hue is not "none" and is followed by a comma, then we are parsing
235    // the legacy syntax. Legacy syntax also doesn't support an origin color.
236    let is_legacy_syntax = origin_color.is_none()
237        && !hue.is_none()
238        && arguments.try_parse(|p| p.expect_comma()).is_ok();
239
240    let (saturation, lightness, alpha) = if is_legacy_syntax {
241        let saturation = parse_percentage(context, arguments, false, allow_channel_keyword)?;
242        arguments.expect_comma()?;
243        let lightness = parse_percentage(context, arguments, false, allow_channel_keyword)?;
244        let alpha = parse_legacy_alpha(context, arguments)?;
245        (saturation, lightness, alpha)
246    } else {
247        let saturation =
248            parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
249        let lightness =
250            parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
251        let alpha = parse_modern_alpha(context, arguments, allow_channel_keyword)?;
252        (saturation, lightness, alpha)
253    };
254
255    Ok(ColorFunction::Hsl(
256        origin_color.into(),
257        hue,
258        saturation,
259        lightness,
260        alpha,
261    ))
262}
263
264/// Parses hwb syntax.
265///
266/// <https://drafts.csswg.org/css-color/#the-hbw-notation>
267#[inline]
268fn parse_hwb<'i, 't>(
269    context: &ParserContext,
270    arguments: &mut Parser<'i, 't>,
271    origin_color: Option<SpecifiedColor>,
272) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
273    let allow_channel_keyword = origin_color.is_some();
274    let hue = parse_number_or_angle(context, arguments, true, allow_channel_keyword)?;
275    let whiteness = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
276    let blackness = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
277
278    let alpha = parse_modern_alpha(context, arguments, allow_channel_keyword)?;
279
280    Ok(ColorFunction::Hwb(
281        origin_color.into(),
282        hue,
283        whiteness,
284        blackness,
285        alpha,
286    ))
287}
288
289type IntoLabFn<Output> = fn(
290    origin: Optional<SpecifiedColor>,
291    l: ColorComponent<NumberOrPercentageComponent>,
292    a: ColorComponent<NumberOrPercentageComponent>,
293    b: ColorComponent<NumberOrPercentageComponent>,
294    alpha: ColorComponent<NumberOrPercentageComponent>,
295) -> Output;
296
297#[inline]
298fn parse_lab_like<'i, 't>(
299    context: &ParserContext,
300    arguments: &mut Parser<'i, 't>,
301    origin_color: Option<SpecifiedColor>,
302    into_color: IntoLabFn<ColorFunction<SpecifiedColor>>,
303) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
304    let allow_channel_keyword = origin_color.is_some();
305    let lightness = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
306    let a = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
307    let b = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
308
309    let alpha = parse_modern_alpha(context, arguments, allow_channel_keyword)?;
310
311    Ok(into_color(origin_color.into(), lightness, a, b, alpha))
312}
313
314type IntoLchFn<Output> = fn(
315    origin: Optional<SpecifiedColor>,
316    l: ColorComponent<NumberOrPercentageComponent>,
317    a: ColorComponent<NumberOrPercentageComponent>,
318    b: ColorComponent<NumberOrAngleComponent>,
319    alpha: ColorComponent<NumberOrPercentageComponent>,
320) -> Output;
321
322#[inline]
323fn parse_lch_like<'i, 't>(
324    context: &ParserContext,
325    arguments: &mut Parser<'i, 't>,
326    origin_color: Option<SpecifiedColor>,
327    into_color: IntoLchFn<ColorFunction<SpecifiedColor>>,
328) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
329    let allow_channel_keyword = origin_color.is_some();
330    let lightness = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
331    let chroma = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
332    let hue = parse_number_or_angle(context, arguments, true, allow_channel_keyword)?;
333
334    let alpha = parse_modern_alpha(context, arguments, allow_channel_keyword)?;
335
336    Ok(into_color(
337        origin_color.into(),
338        lightness,
339        chroma,
340        hue,
341        alpha,
342    ))
343}
344
345/// Parse the color() function.
346#[inline]
347fn parse_color_with_color_space<'i, 't>(
348    context: &ParserContext,
349    arguments: &mut Parser<'i, 't>,
350    origin_color: Option<SpecifiedColor>,
351) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
352    let allow_channel_keyword = origin_color.is_some();
353    let color_space = PredefinedColorSpace::parse(arguments)?;
354
355    let c1 = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
356    let c2 = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
357    let c3 = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
358
359    let alpha = parse_modern_alpha(context, arguments, allow_channel_keyword)?;
360
361    Ok(ColorFunction::Color(
362        origin_color.into(),
363        c1,
364        c2,
365        c3,
366        alpha,
367        color_space.into(),
368    ))
369}
370
371/// Either a percentage or a number.
372#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
373#[repr(u8)]
374pub enum NumberOrPercentageComponent {
375    /// `<number>`.
376    Number(f32),
377    /// `<percentage>`
378    /// The value as a float, divided by 100 so that the nominal range is 0.0 to 1.0.
379    Percentage(f32),
380}
381
382impl NumberOrPercentageComponent {
383    /// Return the value as a number. Percentages will be adjusted to the range
384    /// [0..percent_basis].
385    pub fn to_number(&self, percentage_basis: f32) -> f32 {
386        match *self {
387            Self::Number(value) => value,
388            Self::Percentage(unit_value) => unit_value * percentage_basis,
389        }
390    }
391}
392
393impl ColorComponentType for NumberOrPercentageComponent {
394    fn from_value(value: f32) -> Self {
395        Self::Number(value)
396    }
397
398    fn units() -> CalcUnits {
399        CalcUnits::PERCENTAGE
400    }
401
402    fn try_from_token(token: &Token) -> Result<Self, ()> {
403        Ok(match *token {
404            Token::Number { value, .. } => Self::Number(value),
405            Token::Percentage { unit_value, .. } => Self::Percentage(unit_value),
406            _ => {
407                return Err(());
408            },
409        })
410    }
411
412    fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
413        Ok(match *leaf {
414            Leaf::Percentage(p) => Self::Percentage(p.get()),
415            Leaf::Number(n) => Self::Number(n.value()),
416            _ => return Err(()),
417        })
418    }
419}
420
421/// Either an angle or a number.
422#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
423#[repr(u8)]
424pub enum NumberOrAngleComponent {
425    /// `<number>`.
426    Number(f32),
427    /// `<angle>`
428    /// The value as a number of degrees.
429    Angle(f32),
430}
431
432impl NumberOrAngleComponent {
433    /// Return the angle in degrees. `NumberOrAngle::Number` is returned as
434    /// degrees, because it is the canonical unit.
435    pub fn degrees(&self) -> f32 {
436        match *self {
437            Self::Number(value) => value,
438            Self::Angle(degrees) => degrees,
439        }
440    }
441}
442
443impl ColorComponentType for NumberOrAngleComponent {
444    fn from_value(value: f32) -> Self {
445        Self::Number(value)
446    }
447
448    fn units() -> CalcUnits {
449        CalcUnits::ANGLE
450    }
451
452    fn try_from_token(token: &Token) -> Result<Self, ()> {
453        Ok(match *token {
454            Token::Number { value, .. } => Self::Number(value),
455            Token::Dimension {
456                value, ref unit, ..
457            } => {
458                let degrees = NoCalcAngle::parse_dimension(value, unit)?.degrees();
459                NumberOrAngleComponent::Angle(degrees)
460            },
461            _ => {
462                return Err(());
463            },
464        })
465    }
466
467    fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
468        Ok(match *leaf {
469            Leaf::Angle(angle) => Self::Angle(angle.degrees()),
470            Leaf::Number(n) => Self::Number(n.value()),
471            _ => return Err(()),
472        })
473    }
474}
475
476/// The raw f32 here is for <number>.
477impl ColorComponentType for f32 {
478    fn from_value(value: f32) -> Self {
479        value
480    }
481
482    fn units() -> CalcUnits {
483        CalcUnits::empty()
484    }
485
486    fn try_from_token(token: &Token) -> Result<Self, ()> {
487        if let Token::Number { value, .. } = *token {
488            Ok(value)
489        } else {
490            Err(())
491        }
492    }
493
494    fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
495        if let Leaf::Number(n) = *leaf {
496            Ok(n.value())
497        } else {
498            Err(())
499        }
500    }
501}
502
503/// Parse an `<number>` or `<angle>` value.
504fn parse_number_or_angle<'i, 't>(
505    context: &ParserContext,
506    input: &mut Parser<'i, 't>,
507    allow_none: bool,
508    allow_channel_keyword: bool,
509) -> Result<ColorComponent<NumberOrAngleComponent>, ParseError<'i>> {
510    ColorComponent::parse(context, input, allow_none, allow_channel_keyword)
511}
512
513/// Parse a `<percentage>` value.
514fn parse_percentage<'i, 't>(
515    context: &ParserContext,
516    input: &mut Parser<'i, 't>,
517    allow_none: bool,
518    allow_channel_keyword: bool,
519) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
520    let location = input.current_source_location();
521
522    let value = ColorComponent::<NumberOrPercentageComponent>::parse(
523        context,
524        input,
525        allow_none,
526        allow_channel_keyword,
527    )?;
528    if !value.could_be_percentage() {
529        return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
530    }
531
532    Ok(value)
533}
534
535/// Parse a `<number>` value.
536fn parse_number<'i, 't>(
537    context: &ParserContext,
538    input: &mut Parser<'i, 't>,
539    allow_none: bool,
540    allow_channel_keyword: bool,
541) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
542    let location = input.current_source_location();
543
544    let value = ColorComponent::<NumberOrPercentageComponent>::parse(
545        context,
546        input,
547        allow_none,
548        allow_channel_keyword,
549    )?;
550
551    if !value.could_be_number() {
552        return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
553    }
554
555    Ok(value)
556}
557
558/// Parse a `<number>` or `<percentage>` value.
559fn parse_number_or_percentage<'i, 't>(
560    context: &ParserContext,
561    input: &mut Parser<'i, 't>,
562    allow_none: bool,
563    allow_channel_keyword: bool,
564) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
565    ColorComponent::parse(context, input, allow_none, allow_channel_keyword)
566}
567
568fn parse_legacy_alpha<'i, 't>(
569    context: &ParserContext,
570    arguments: &mut Parser<'i, 't>,
571) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
572    if !arguments.is_exhausted() {
573        arguments.expect_comma()?;
574        parse_number_or_percentage(context, arguments, false, false)
575    } else {
576        Ok(ColorComponent::AlphaOmitted)
577    }
578}
579
580fn parse_modern_alpha<'i, 't>(
581    context: &ParserContext,
582    arguments: &mut Parser<'i, 't>,
583    allow_channel_keyword: bool,
584) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
585    if !arguments.is_exhausted() {
586        arguments.expect_delim('/')?;
587        parse_number_or_percentage(context, arguments, true, allow_channel_keyword)
588    } else {
589        Ok(ColorComponent::AlphaOmitted)
590    }
591}
592
593impl ColorComponent<NumberOrPercentageComponent> {
594    /// Return true if the value contained inside is/can resolve to a number.
595    /// Also returns false if the node is invalid somehow.
596    fn could_be_number(&self) -> bool {
597        match self {
598            Self::None | Self::AlphaOmitted => true,
599            Self::Value(value) => matches!(value, NumberOrPercentageComponent::Number { .. }),
600            Self::ChannelKeyword(_) => {
601                // Channel keywords always resolve to numbers.
602                true
603            },
604            Self::Calc(node) => {
605                if let Ok(unit) = node.unit() {
606                    unit.is_empty()
607                } else {
608                    false
609                }
610            },
611        }
612    }
613
614    /// Return true if the value contained inside is/can resolve to a percentage.
615    /// Also returns false if the node is invalid somehow.
616    fn could_be_percentage(&self) -> bool {
617        match self {
618            Self::None | Self::AlphaOmitted => true,
619            Self::Value(value) => matches!(value, NumberOrPercentageComponent::Percentage { .. }),
620            Self::ChannelKeyword(_) => {
621                // Channel keywords always resolve to numbers.
622                false
623            },
624            Self::Calc(node) => {
625                if let Ok(unit) = node.unit() {
626                    unit == CalcUnits::PERCENTAGE
627                } else {
628                    false
629                }
630            },
631        }
632    }
633}