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