1#![deny(missing_docs)]
6
7use 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#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToShmem)]
38#[repr(C)]
39pub struct ChannelKeyword(u16);
40bitflags! {
41 impl ChannelKeyword: u16 {
42 const ALPHA = 1 << 0;
44 const A = 1 << 1;
46 const B = 1 << 2;
48 const C = 1 << 3;
50 const G = 1 << 4;
52 const H = 1 << 5;
54 const L = 1 << 6;
56 const R = 1 << 7;
58 const S = 1 << 8;
60 const W = 1 << 9;
62 const X = 1 << 10;
64 const Y = 1 << 11;
66 const Z = 1 << 12;
68 }
69}
70
71impl ChannelKeyword {
72 pub fn rgb() -> Self {
75 Self::R | Self::G | Self::B | Self::ALPHA
76 }
77
78 pub fn hsl() -> Self {
81 Self::H | Self::S | Self::L | Self::ALPHA
82 }
83
84 pub fn hwb() -> Self {
87 Self::H | Self::W | Self::B | Self::ALPHA
88 }
89
90 pub fn lab() -> Self {
93 Self::L | Self::A | Self::B | Self::ALPHA
94 }
95
96 pub fn lch() -> Self {
99 Self::L | Self::C | Self::H | Self::ALPHA
100 }
101
102 pub fn xyz() -> Self {
105 Self::X | Self::Y | Self::Z | Self::ALPHA
106 }
107
108 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#[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
185pub 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 Ok(SpecifiedColor::ColorFunction(Box::new(color_function)))
211 });
212 },
213 _ => Err(()),
214 }
215 .map_err(|()| ParseError::unexpected_token())
216}
217
218#[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
248fn parse_origin_color(
250 context: &ParserContext,
251 arguments: &mut Parser,
252) -> Result<Option<SpecifiedColor>, ParseError> {
253 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 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#[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 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#[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#[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#[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 return Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError));
500 }
501 Ok(ColorFunction::Alpha(origin_color, alpha))
502}
503
504#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
506#[repr(u8)]
507pub enum NumberOrPercentageComponent {
508 Number(f32),
510 Percentage(f32),
513}
514
515impl NumberOrPercentageComponent {
516 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#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
557#[repr(u8)]
558pub enum NumberOrAngleComponent {
559 Number(f32),
561 Angle(f32),
564}
565
566impl NumberOrAngleComponent {
567 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
611impl 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
638fn 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
654fn 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
675fn 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
697fn 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 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 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 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 false
764 },
765 Self::Calc(node) => node
766 .numeric_type_as_calc_type()
767 .is_ok_and(|ty| ty == CalcType::Percentage),
768 }
769 }
770}