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 std::fmt::Write;
10
11use super::{
12    color_function::ColorFunction,
13    component::{ColorComponent, ColorComponentType},
14    AbsoluteColor,
15};
16use crate::derives::*;
17use crate::{
18    parser::{Parse, ParserContext},
19    values::{
20        computed::Color as ComputedColor,
21        generics::{calc::CalcUnits, Optional},
22        specified::{angle::NoCalcAngle, calc::Leaf, color::Color as SpecifiedColor},
23    },
24};
25use cssparser::{
26    color::{parse_hash_color, PredefinedColorSpace, OPAQUE},
27    match_ignore_ascii_case, CowRcStr, Parser, Token,
28};
29use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
30
31/// Returns true if the relative color syntax pref is enabled.
32#[inline]
33pub fn rcs_enabled() -> bool {
34    static_prefs::pref!("layout.css.relative-color-syntax.enabled")
35}
36
37/// Represents a channel keyword inside a color.
38#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToShmem)]
39#[repr(C)]
40pub struct ChannelKeyword(u16);
41bitflags! {
42    impl ChannelKeyword: u16 {
43        /// alpha
44        const ALPHA = 1 << 0;
45        /// a
46        const A = 1 << 1;
47        /// b, blackness, blue
48        const B = 1 << 2;
49        /// chroma
50        const C = 1 << 3;
51        /// green
52        const G = 1 << 4;
53        /// hue
54        const H = 1 << 5;
55        /// lightness
56        const L = 1 << 6;
57        /// red
58        const R = 1 << 7;
59        /// saturation
60        const S = 1 << 8;
61        /// whiteness
62        const W = 1 << 9;
63        /// x
64        const X = 1 << 10;
65        /// y
66        const Y = 1 << 11;
67        /// z
68        const Z = 1 << 12;
69    }
70}
71
72impl ChannelKeyword {
73    /// Channel keywords allowed in sRGB colors.
74    /// https://drafts.csswg.org/css-color-5/#relative-RGB
75    pub fn rgb() -> Self {
76        Self::R | Self::G | Self::B | Self::ALPHA
77    }
78
79    /// Channel keywords allowed in HSL colors.
80    /// https://drafts.csswg.org/css-color-5/#relative-HSL
81    pub fn hsl() -> Self {
82        Self::H | Self::S | Self::L | Self::ALPHA
83    }
84
85    /// Channel keywords allowed in HWB colors.
86    /// https://drafts.csswg.org/css-color-5/#relative-HWB
87    pub fn hwb() -> Self {
88        Self::H | Self::W | Self::B | Self::ALPHA
89    }
90
91    /// Channel keywords allowed in Lab and Oklab colors.
92    /// https://drafts.csswg.org/css-color-5/#relative-Lab
93    pub fn lab() -> Self {
94        Self::L | Self::A | Self::B | Self::ALPHA
95    }
96
97    /// Channel keywords allowed in LCH and OkLCh colors.
98    /// https://drafts.csswg.org/css-color-5/#relative-LCH
99    pub fn lch() -> Self {
100        Self::L | Self::C | Self::H | Self::ALPHA
101    }
102
103    /// Channel keywords allowed in XYZ colors.
104    /// https://drafts.csswg.org/css-color-5/#relative-color-function
105    pub fn xyz() -> Self {
106        Self::X | Self::Y | Self::Z | Self::ALPHA
107    }
108
109    /// Parse a channel keyword from an ident.
110    pub fn from_ident(ident: &str) -> Result<Self, ()> {
111        Ok(match_ignore_ascii_case! { ident,
112            "alpha" => Self::ALPHA,
113            "a" => Self::A,
114            "b" => Self::B,
115            "c" => Self::C,
116            "g" => Self::G,
117            "h" => Self::H,
118            "l" => Self::L,
119            "r" => Self::R,
120            "s" => Self::S,
121            "w" => Self::W,
122            "x" => Self::X,
123            "y" => Self::Y,
124            "z" => Self::Z,
125            _ => return Err(())
126        })
127    }
128}
129
130impl Parse for ChannelKeyword {
131    fn parse<'i, 't>(
132        _: &ParserContext,
133        input: &mut Parser<'i, 't>,
134    ) -> Result<Self, ParseError<'i>> {
135        let location = input.current_source_location();
136        let ident = input.expect_ident()?;
137        Self::from_ident(ident.as_ref())
138            .map_err(|()| location.new_unexpected_token_error(Token::Ident(ident.clone())))
139    }
140}
141
142impl ToCss for ChannelKeyword {
143    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> std::fmt::Result
144    where
145        W: std::fmt::Write,
146    {
147        dest.write_str(match *self {
148            Self::ALPHA => "alpha",
149            Self::A => "a",
150            Self::B => "b",
151            Self::C => "c",
152            Self::G => "g",
153            Self::H => "h",
154            Self::L => "l",
155            Self::R => "r",
156            Self::S => "s",
157            Self::W => "w",
158            Self::X => "x",
159            Self::Y => "y",
160            Self::Z => "z",
161            _ => {
162                debug_assert!(
163                    false,
164                    "tried to serialize unexpected multi-value ChannelKeyword"
165                );
166                ""
167            },
168        })
169    }
170}
171
172/// Return the named color with the given name.
173///
174/// Matching is case-insensitive in the ASCII range.
175/// CSS escaping (if relevant) should be resolved before calling this function.
176/// (For example, the value of an `Ident` token is fine.)
177#[inline]
178pub fn parse_color_keyword(ident: &str) -> Result<SpecifiedColor, ()> {
179    Ok(match_ignore_ascii_case! { ident,
180        "transparent" => {
181            SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(0u8, 0u8, 0u8, 0.0))
182        },
183        "currentcolor" => SpecifiedColor::CurrentColor,
184        _ => {
185            let (r, g, b) = cssparser::color::parse_named_color(ident)?;
186            SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(r, g, b, OPAQUE))
187        },
188    })
189}
190
191/// Parse a CSS color using the specified [`ColorParser`] and return a new color
192/// value on success.
193pub fn parse_color_with<'i, 't>(
194    context: &ParserContext,
195    input: &mut Parser<'i, 't>,
196) -> Result<SpecifiedColor, ParseError<'i>> {
197    let location = input.current_source_location();
198    let token = input.next()?;
199    match *token {
200        Token::Hash(ref value) | Token::IDHash(ref value) => parse_hash_color(value.as_bytes())
201            .map(|(r, g, b, a)| {
202                SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(r, g, b, a))
203            }),
204        Token::Ident(ref value) => parse_color_keyword(value),
205        Token::Function(ref name) => {
206            let name = name.clone();
207            return input.parse_nested_block(|arguments| {
208                let color_function = parse_color_function(context, name, arguments)?;
209                if !color_function.has_origin_color() {
210                    if let Ok(ComputedColor::Absolute(resolved)) =
211                        color_function.to_computed_color(None)
212                    {
213                        return Ok(SpecifiedColor::from_absolute_color(resolved));
214                    }
215                }
216                // Preserve the color as it was parsed.
217                Ok(SpecifiedColor::ColorFunction(Box::new(color_function)))
218            });
219        },
220        _ => Err(()),
221    }
222    .map_err(|()| location.new_unexpected_token_error(token.clone()))
223}
224
225/// Parse one of the color functions: rgba(), lab(), color(), etc.
226#[inline]
227fn parse_color_function<'i, 't>(
228    context: &ParserContext,
229    name: CowRcStr<'i>,
230    arguments: &mut Parser<'i, 't>,
231) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
232    let origin_color = parse_origin_color(context, arguments)?;
233    let color = match_ignore_ascii_case! { &name,
234        "rgb" | "rgba" => parse_rgb(context, arguments, origin_color),
235        "hsl" | "hsla" => parse_hsl(context, arguments, origin_color),
236        "hwb" => parse_hwb(context, arguments, origin_color),
237        "lab" => parse_lab_like(context, arguments, origin_color, ColorFunction::Lab),
238        "lch" => parse_lch_like(context, arguments, origin_color, ColorFunction::Lch),
239        "oklab" => parse_lab_like(context, arguments, origin_color, ColorFunction::Oklab),
240        "oklch" => parse_lch_like(context, arguments, origin_color, ColorFunction::Oklch),
241        "color" => parse_color_with_color_space(context, arguments, origin_color),
242        "alpha" if static_prefs::pref!("layout.css.alpha-color-function.enabled") => {
243            parse_relative_alpha(
244                context,
245                arguments,
246                origin_color.ok_or_else(|| arguments.new_custom_error(StyleParseErrorKind::UnspecifiedError))?
247            )
248        },
249        _ => return Err(arguments.new_unexpected_token_error(Token::Ident(name))),
250    }?;
251    arguments.expect_exhausted()?;
252    Ok(color)
253}
254
255/// Parse the relative color syntax "from" syntax `from <color>`.
256fn parse_origin_color<'i, 't>(
257    context: &ParserContext,
258    arguments: &mut Parser<'i, 't>,
259) -> Result<Option<SpecifiedColor>, ParseError<'i>> {
260    if !rcs_enabled() {
261        return Ok(None);
262    }
263
264    // Not finding the from keyword is not an error, it just means we don't
265    // have an origin color.
266    if arguments
267        .try_parse(|p| p.expect_ident_matching("from"))
268        .is_err()
269    {
270        return Ok(None);
271    }
272
273    SpecifiedColor::parse(context, arguments).map(Option::Some)
274}
275
276#[inline]
277fn parse_rgb<'i, 't>(
278    context: &ParserContext,
279    arguments: &mut Parser<'i, 't>,
280    origin_color: Option<SpecifiedColor>,
281) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
282    let allowed_channel_keywords = if origin_color.is_some() {
283        ChannelKeyword::rgb()
284    } else {
285        ChannelKeyword::empty()
286    };
287    let maybe_red = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
288
289    // If the first component is not "none" and is followed by a comma, then we
290    // are parsing the legacy syntax.  Legacy syntax also doesn't support an
291    // origin color.
292    let is_legacy_syntax = origin_color.is_none()
293        && !maybe_red.is_none()
294        && arguments.try_parse(|p| p.expect_comma()).is_ok();
295
296    Ok(if is_legacy_syntax {
297        let (green, blue) = if maybe_red.could_be_percentage() {
298            let green = parse_percentage(context, arguments, false, allowed_channel_keywords)?;
299            arguments.expect_comma()?;
300            let blue = parse_percentage(context, arguments, false, allowed_channel_keywords)?;
301            (green, blue)
302        } else {
303            let green = parse_number(context, arguments, false, allowed_channel_keywords)?;
304            arguments.expect_comma()?;
305            let blue = parse_number(context, arguments, false, allowed_channel_keywords)?;
306            (green, blue)
307        };
308
309        let alpha = parse_legacy_alpha(context, arguments)?;
310
311        ColorFunction::Rgb(origin_color.into(), maybe_red, green, blue, alpha)
312    } else {
313        let green = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
314        let blue = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
315
316        let alpha = parse_modern_alpha(context, arguments, allowed_channel_keywords)?;
317
318        ColorFunction::Rgb(origin_color.into(), maybe_red, green, blue, alpha)
319    })
320}
321
322/// Parses hsl syntax.
323///
324/// <https://drafts.csswg.org/css-color/#the-hsl-notation>
325#[inline]
326fn parse_hsl<'i, 't>(
327    context: &ParserContext,
328    arguments: &mut Parser<'i, 't>,
329    origin_color: Option<SpecifiedColor>,
330) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
331    let allowed_channel_keywords = if origin_color.is_some() {
332        ChannelKeyword::hsl()
333    } else {
334        ChannelKeyword::empty()
335    };
336    let hue = parse_number_or_angle(context, arguments, true, allowed_channel_keywords)?;
337
338    // If the hue is not "none" and is followed by a comma, then we are parsing
339    // the legacy syntax. Legacy syntax also doesn't support an origin color.
340    let is_legacy_syntax = origin_color.is_none()
341        && !hue.is_none()
342        && arguments.try_parse(|p| p.expect_comma()).is_ok();
343
344    let (saturation, lightness, alpha) = if is_legacy_syntax {
345        let saturation = parse_percentage(context, arguments, false, allowed_channel_keywords)?;
346        arguments.expect_comma()?;
347        let lightness = parse_percentage(context, arguments, false, allowed_channel_keywords)?;
348        let alpha = parse_legacy_alpha(context, arguments)?;
349        (saturation, lightness, alpha)
350    } else {
351        let saturation =
352            parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
353        let lightness =
354            parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
355        let alpha = parse_modern_alpha(context, arguments, allowed_channel_keywords)?;
356        (saturation, lightness, alpha)
357    };
358
359    Ok(ColorFunction::Hsl(
360        origin_color.into(),
361        hue,
362        saturation,
363        lightness,
364        alpha,
365    ))
366}
367
368/// Parses hwb syntax.
369///
370/// <https://drafts.csswg.org/css-color/#the-hbw-notation>
371#[inline]
372fn parse_hwb<'i, 't>(
373    context: &ParserContext,
374    arguments: &mut Parser<'i, 't>,
375    origin_color: Option<SpecifiedColor>,
376) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
377    let allowed_channel_keywords = if origin_color.is_some() {
378        ChannelKeyword::hwb()
379    } else {
380        ChannelKeyword::empty()
381    };
382    let hue = parse_number_or_angle(context, arguments, true, allowed_channel_keywords)?;
383    let whiteness = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
384    let blackness = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
385
386    let alpha = parse_modern_alpha(context, arguments, allowed_channel_keywords)?;
387
388    Ok(ColorFunction::Hwb(
389        origin_color.into(),
390        hue,
391        whiteness,
392        blackness,
393        alpha,
394    ))
395}
396
397type IntoLabFn<Output> = fn(
398    origin: Optional<SpecifiedColor>,
399    l: ColorComponent<NumberOrPercentageComponent>,
400    a: ColorComponent<NumberOrPercentageComponent>,
401    b: ColorComponent<NumberOrPercentageComponent>,
402    alpha: ColorComponent<NumberOrPercentageComponent>,
403) -> Output;
404
405#[inline]
406fn parse_lab_like<'i, 't>(
407    context: &ParserContext,
408    arguments: &mut Parser<'i, 't>,
409    origin_color: Option<SpecifiedColor>,
410    into_color: IntoLabFn<ColorFunction<SpecifiedColor>>,
411) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
412    let allowed_channel_keywords = if origin_color.is_some() {
413        ChannelKeyword::lab()
414    } else {
415        ChannelKeyword::empty()
416    };
417    let lightness = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
418    let a = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
419    let b = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
420
421    let alpha = parse_modern_alpha(context, arguments, allowed_channel_keywords)?;
422
423    Ok(into_color(origin_color.into(), lightness, a, b, alpha))
424}
425
426type IntoLchFn<Output> = fn(
427    origin: Optional<SpecifiedColor>,
428    l: ColorComponent<NumberOrPercentageComponent>,
429    a: ColorComponent<NumberOrPercentageComponent>,
430    b: ColorComponent<NumberOrAngleComponent>,
431    alpha: ColorComponent<NumberOrPercentageComponent>,
432) -> Output;
433
434#[inline]
435fn parse_lch_like<'i, 't>(
436    context: &ParserContext,
437    arguments: &mut Parser<'i, 't>,
438    origin_color: Option<SpecifiedColor>,
439    into_color: IntoLchFn<ColorFunction<SpecifiedColor>>,
440) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
441    let allowed_channel_keywords = if origin_color.is_some() {
442        ChannelKeyword::lch()
443    } else {
444        ChannelKeyword::empty()
445    };
446    let lightness = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
447    let chroma = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
448    let hue = parse_number_or_angle(context, arguments, true, allowed_channel_keywords)?;
449
450    let alpha = parse_modern_alpha(context, arguments, allowed_channel_keywords)?;
451
452    Ok(into_color(
453        origin_color.into(),
454        lightness,
455        chroma,
456        hue,
457        alpha,
458    ))
459}
460
461/// Parse the color() function.
462#[inline]
463fn parse_color_with_color_space<'i, 't>(
464    context: &ParserContext,
465    arguments: &mut Parser<'i, 't>,
466    origin_color: Option<SpecifiedColor>,
467) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
468    let color_space = PredefinedColorSpace::parse(arguments)?;
469    let allowed_channel_keywords = if origin_color.is_some() {
470        match color_space {
471            PredefinedColorSpace::Srgb
472            | PredefinedColorSpace::SrgbLinear
473            | PredefinedColorSpace::DisplayP3
474            | PredefinedColorSpace::DisplayP3Linear
475            | PredefinedColorSpace::A98Rgb
476            | PredefinedColorSpace::ProphotoRgb
477            | PredefinedColorSpace::Rec2020 => ChannelKeyword::rgb(),
478            PredefinedColorSpace::XyzD50 | PredefinedColorSpace::XyzD65 => ChannelKeyword::xyz(),
479        }
480    } else {
481        ChannelKeyword::empty()
482    };
483
484    let c1 = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
485    let c2 = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
486    let c3 = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
487
488    let alpha = parse_modern_alpha(context, arguments, allowed_channel_keywords)?;
489
490    Ok(ColorFunction::Color(
491        origin_color.into(),
492        c1,
493        c2,
494        c3,
495        alpha,
496        color_space.into(),
497    ))
498}
499
500/// Parse the alpha() function.
501#[inline]
502fn parse_relative_alpha<'i, 't>(
503    context: &ParserContext,
504    arguments: &mut Parser<'i, 't>,
505    origin_color: SpecifiedColor,
506) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
507    let alpha = parse_modern_alpha(context, arguments, ChannelKeyword::ALPHA)?;
508    Ok(ColorFunction::Alpha(origin_color.into(), alpha))
509}
510
511/// Either a percentage or a number.
512#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
513#[repr(u8)]
514pub enum NumberOrPercentageComponent {
515    /// `<number>`.
516    Number(f32),
517    /// `<percentage>`
518    /// The value as a float, divided by 100 so that the nominal range is 0.0 to 1.0.
519    Percentage(f32),
520}
521
522impl NumberOrPercentageComponent {
523    /// Return the value as a number. Percentages will be adjusted to the range
524    /// [0..percent_basis].
525    pub fn to_number(&self, percentage_basis: f32) -> f32 {
526        match *self {
527            Self::Number(value) => value,
528            Self::Percentage(unit_value) => unit_value * percentage_basis,
529        }
530    }
531}
532
533impl ColorComponentType for NumberOrPercentageComponent {
534    fn from_value(value: f32) -> Self {
535        Self::Number(value)
536    }
537
538    fn units() -> CalcUnits {
539        CalcUnits::PERCENTAGE
540    }
541
542    fn try_from_token(token: &Token) -> Result<Self, ()> {
543        Ok(match *token {
544            Token::Number { value, .. } => Self::Number(value),
545            Token::Percentage { unit_value, .. } => Self::Percentage(unit_value),
546            _ => {
547                return Err(());
548            },
549        })
550    }
551
552    fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
553        Ok(match *leaf {
554            Leaf::Percentage(p) => Self::Percentage(p.get()),
555            Leaf::Number(n) => Self::Number(n.value()),
556            _ => return Err(()),
557        })
558    }
559}
560
561/// Either an angle or a number.
562#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
563#[repr(u8)]
564pub enum NumberOrAngleComponent {
565    /// `<number>`.
566    Number(f32),
567    /// `<angle>`
568    /// The value as a number of degrees.
569    Angle(f32),
570}
571
572impl NumberOrAngleComponent {
573    /// Return the angle in degrees. `NumberOrAngle::Number` is returned as
574    /// degrees, because it is the canonical unit.
575    pub fn degrees(&self) -> f32 {
576        match *self {
577            Self::Number(value) => value,
578            Self::Angle(degrees) => degrees,
579        }
580    }
581}
582
583impl ColorComponentType for NumberOrAngleComponent {
584    fn from_value(value: f32) -> Self {
585        Self::Number(value)
586    }
587
588    fn units() -> CalcUnits {
589        CalcUnits::ANGLE
590    }
591
592    fn try_from_token(token: &Token) -> Result<Self, ()> {
593        Ok(match *token {
594            Token::Number { value, .. } => Self::Number(value),
595            Token::Dimension {
596                value, ref unit, ..
597            } => {
598                let degrees = NoCalcAngle::parse_dimension(value, unit)?.degrees();
599                NumberOrAngleComponent::Angle(degrees)
600            },
601            _ => {
602                return Err(());
603            },
604        })
605    }
606
607    fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
608        Ok(match *leaf {
609            Leaf::Angle(angle) => Self::Angle(angle.degrees()),
610            Leaf::Number(n) => Self::Number(n.value()),
611            _ => return Err(()),
612        })
613    }
614}
615
616/// The raw f32 here is for <number>.
617impl ColorComponentType for f32 {
618    fn from_value(value: f32) -> Self {
619        value
620    }
621
622    fn units() -> CalcUnits {
623        CalcUnits::empty()
624    }
625
626    fn try_from_token(token: &Token) -> Result<Self, ()> {
627        if let Token::Number { value, .. } = *token {
628            Ok(value)
629        } else {
630            Err(())
631        }
632    }
633
634    fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
635        if let Leaf::Number(n) = *leaf {
636            Ok(n.value())
637        } else {
638            Err(())
639        }
640    }
641}
642
643/// Parse an `<number>` or `<angle>` value.
644fn parse_number_or_angle<'i, 't>(
645    context: &ParserContext,
646    input: &mut Parser<'i, 't>,
647    allow_none: bool,
648    allowed_channel_keywords: ChannelKeyword,
649) -> Result<ColorComponent<NumberOrAngleComponent>, ParseError<'i>> {
650    ColorComponent::parse(context, input, allow_none, allowed_channel_keywords)
651}
652
653/// Parse a `<percentage>` value.
654fn parse_percentage<'i, 't>(
655    context: &ParserContext,
656    input: &mut Parser<'i, 't>,
657    allow_none: bool,
658    allowed_channel_keywords: ChannelKeyword,
659) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
660    let location = input.current_source_location();
661
662    let value = ColorComponent::<NumberOrPercentageComponent>::parse(
663        context,
664        input,
665        allow_none,
666        allowed_channel_keywords,
667    )?;
668    if !value.could_be_percentage() {
669        return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
670    }
671
672    Ok(value)
673}
674
675/// Parse a `<number>` value.
676fn parse_number<'i, 't>(
677    context: &ParserContext,
678    input: &mut Parser<'i, 't>,
679    allow_none: bool,
680    allowed_channel_keywords: ChannelKeyword,
681) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
682    let location = input.current_source_location();
683
684    let value = ColorComponent::<NumberOrPercentageComponent>::parse(
685        context,
686        input,
687        allow_none,
688        allowed_channel_keywords,
689    )?;
690
691    if !value.could_be_number() {
692        return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
693    }
694
695    Ok(value)
696}
697
698/// Parse a `<number>` or `<percentage>` value.
699fn parse_number_or_percentage<'i, 't>(
700    context: &ParserContext,
701    input: &mut Parser<'i, 't>,
702    allow_none: bool,
703    allowed_channel_keywords: ChannelKeyword,
704) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
705    ColorComponent::parse(context, input, allow_none, allowed_channel_keywords)
706}
707
708fn parse_legacy_alpha<'i, 't>(
709    context: &ParserContext,
710    arguments: &mut Parser<'i, 't>,
711) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
712    if !arguments.is_exhausted() {
713        arguments.expect_comma()?;
714        parse_number_or_percentage(context, arguments, false, ChannelKeyword::empty())
715    } else {
716        Ok(ColorComponent::AlphaOmitted)
717    }
718}
719
720fn parse_modern_alpha<'i, 't>(
721    context: &ParserContext,
722    arguments: &mut Parser<'i, 't>,
723    allowed_channel_keywords: ChannelKeyword,
724) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
725    if !arguments.is_exhausted() {
726        arguments.expect_delim('/')?;
727        parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)
728    } else {
729        Ok(ColorComponent::AlphaOmitted)
730    }
731}
732
733impl ColorComponent<NumberOrPercentageComponent> {
734    /// Return true if the value contained inside is/can resolve to a number.
735    /// Also returns false if the node is invalid somehow.
736    fn could_be_number(&self) -> bool {
737        match self {
738            Self::None | Self::AlphaOmitted => true,
739            Self::Value(value) => matches!(value, NumberOrPercentageComponent::Number { .. }),
740            Self::ChannelKeyword(_) => {
741                // Channel keywords always resolve to numbers.
742                true
743            },
744            Self::Calc(node) => {
745                if let Ok(unit) = node.unit() {
746                    unit.is_empty()
747                } else {
748                    false
749                }
750            },
751        }
752    }
753
754    /// Return true if the value contained inside is/can resolve to a percentage.
755    /// Also returns false if the node is invalid somehow.
756    fn could_be_percentage(&self) -> bool {
757        match self {
758            Self::None | Self::AlphaOmitted => true,
759            Self::Value(value) => matches!(value, NumberOrPercentageComponent::Percentage { .. }),
760            Self::ChannelKeyword(_) => {
761                // Channel keywords always resolve to numbers.
762                false
763            },
764            Self::Calc(node) => {
765                if let Ok(unit) = node.unit() {
766                    unit == CalcUnits::PERCENTAGE
767                } else {
768                    false
769                }
770            },
771        }
772    }
773}