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    AbsoluteColor,
13    color_function::ColorFunction,
14    component::{ColorComponent, ColorComponentType},
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::{Optional, calc::CalcType},
23        specified::{
24            angle::NoCalcAngle,
25            calc::{Leaf, PercentageContext},
26            color::Color as SpecifiedColor,
27        },
28    },
29};
30use cssparser::{
31    CowRcStr, Parser, Token,
32    color::{OPAQUE, PredefinedColorSpace, parse_hash_color},
33    match_ignore_ascii_case,
34};
35use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
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(_: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
132        let ident = input.expect_ident()?;
133        Self::from_ident(ident.as_ref()).map_err(|()| ParseError::unexpected_token())
134    }
135}
136
137impl ToCss for ChannelKeyword {
138    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> std::fmt::Result
139    where
140        W: std::fmt::Write,
141    {
142        dest.write_str(match *self {
143            Self::ALPHA => "alpha",
144            Self::A => "a",
145            Self::B => "b",
146            Self::C => "c",
147            Self::G => "g",
148            Self::H => "h",
149            Self::L => "l",
150            Self::R => "r",
151            Self::S => "s",
152            Self::W => "w",
153            Self::X => "x",
154            Self::Y => "y",
155            Self::Z => "z",
156            _ => {
157                debug_assert!(
158                    false,
159                    "tried to serialize unexpected multi-value ChannelKeyword"
160                );
161                ""
162            },
163        })
164    }
165}
166
167/// Return the named color with the given name.
168///
169/// Matching is case-insensitive in the ASCII range.
170/// CSS escaping (if relevant) should be resolved before calling this function.
171/// (For example, the value of an `Ident` token is fine.)
172#[inline]
173pub fn parse_color_keyword(ident: &str) -> Result<SpecifiedColor, ()> {
174    Ok(match_ignore_ascii_case! { ident,
175        "transparent" => {
176            SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(0u8, 0u8, 0u8, 0.0))
177        },
178        "currentcolor" => SpecifiedColor::CurrentColor,
179        _ => {
180            let (r, g, b) = cssparser::color::parse_named_color(ident)?;
181            SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(r, g, b, OPAQUE))
182        },
183    })
184}
185
186/// Parse a CSS color using the specified [`ColorParser`] and return a new color
187/// value on success.
188pub fn parse_color_with(
189    context: &ParserContext,
190    input: &mut Parser,
191) -> Result<SpecifiedColor, ParseError> {
192    let token = input.next()?;
193    match *token {
194        Token::Hash(ref value) | Token::IDHash(ref value) => parse_hash_color(value.as_bytes())
195            .map(|(r, g, b, a)| {
196                SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(r, g, b, a))
197            }),
198        Token::Ident(ref value) => parse_color_keyword(value),
199        Token::Function(ref name) => {
200            let name = name.clone();
201            return input.parse_nested_block(|arguments| {
202                let color_function = parse_color_function(context, name, arguments)?;
203                if !color_function.has_origin_color()
204                    && let Ok(ComputedColor::Absolute(resolved)) =
205                        color_function.to_computed_color(None)
206                {
207                    return Ok(SpecifiedColor::from_absolute_color(resolved));
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}