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