1#![deny(missing_docs)]
6
7use super::{
10 color_function::ColorFunction,
11 component::{ColorComponent, ColorComponentType},
12 AbsoluteColor,
13};
14use crate::derives::*;
15use crate::{
16 parser::{Parse, ParserContext},
17 values::{
18 generics::{calc::CalcUnits, Optional},
19 specified::{angle::NoCalcAngle, calc::Leaf, color::Color as SpecifiedColor},
20 },
21};
22use cssparser::{
23 color::{parse_hash_color, PredefinedColorSpace, OPAQUE},
24 match_ignore_ascii_case, CowRcStr, Parser, Token,
25};
26use style_traits::{ParseError, StyleParseErrorKind};
27
28#[inline]
30pub fn rcs_enabled() -> bool {
31 static_prefs::pref!("layout.css.relative-color-syntax.enabled")
32}
33
34#[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, PartialOrd, ToCss, ToShmem)]
36#[repr(u8)]
37pub enum ChannelKeyword {
38 Alpha,
40 A,
42 B,
44 C,
46 G,
48 H,
50 L,
52 R,
54 S,
56 W,
58 X,
60 Y,
62 Z,
64}
65
66#[inline]
72pub fn parse_color_keyword(ident: &str) -> Result<SpecifiedColor, ()> {
73 Ok(match_ignore_ascii_case! { ident,
74 "transparent" => {
75 SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(0u8, 0u8, 0u8, 0.0))
76 },
77 "currentcolor" => SpecifiedColor::CurrentColor,
78 _ => {
79 let (r, g, b) = cssparser::color::parse_named_color(ident)?;
80 SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(r, g, b, OPAQUE))
81 },
82 })
83}
84
85pub fn parse_color_with<'i, 't>(
88 context: &ParserContext,
89 input: &mut Parser<'i, 't>,
90) -> Result<SpecifiedColor, ParseError<'i>> {
91 let location = input.current_source_location();
92 let token = input.next()?;
93 match *token {
94 Token::Hash(ref value) | Token::IDHash(ref value) => parse_hash_color(value.as_bytes())
95 .map(|(r, g, b, a)| {
96 SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(r, g, b, a))
97 }),
98 Token::Ident(ref value) => parse_color_keyword(value),
99 Token::Function(ref name) => {
100 let name = name.clone();
101 return input.parse_nested_block(|arguments| {
102 let color_function = parse_color_function(context, name, arguments)?;
103
104 if color_function.has_origin_color() {
105 Ok(SpecifiedColor::ColorFunction(Box::new(color_function)))
107 } else if let Ok(resolved) = color_function.resolve_to_absolute(None) {
108 Ok(SpecifiedColor::from_absolute_color(resolved))
109 } else {
110 Ok(SpecifiedColor::ColorFunction(Box::new(color_function)))
112 }
113 });
114 },
115 _ => Err(()),
116 }
117 .map_err(|()| location.new_unexpected_token_error(token.clone()))
118}
119
120#[inline]
122fn parse_color_function<'i, 't>(
123 context: &ParserContext,
124 name: CowRcStr<'i>,
125 arguments: &mut Parser<'i, 't>,
126) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
127 let origin_color = parse_origin_color(context, arguments)?;
128 let has_origin_color = origin_color.is_some();
129
130 let color = match_ignore_ascii_case! { &name,
131 "rgb" | "rgba" => parse_rgb(context, arguments, origin_color),
132 "hsl" | "hsla" => parse_hsl(context, arguments, origin_color),
133 "hwb" => parse_hwb(context, arguments, origin_color),
134 "lab" => parse_lab_like(context, arguments, origin_color, ColorFunction::Lab),
135 "lch" => parse_lch_like(context, arguments, origin_color, ColorFunction::Lch),
136 "oklab" => parse_lab_like(context, arguments, origin_color, ColorFunction::Oklab),
137 "oklch" => parse_lch_like(context, arguments, origin_color, ColorFunction::Oklch),
138 "color" => parse_color_with_color_space(context, arguments, origin_color),
139 _ => return Err(arguments.new_unexpected_token_error(Token::Ident(name))),
140 }?;
141
142 if has_origin_color {
143 let abs = color
147 .map_origin_color(|_| Ok(AbsoluteColor::TRANSPARENT_BLACK))
148 .unwrap();
149 if abs.resolve_to_absolute(None).is_err() {
150 return Err(arguments.new_custom_error(StyleParseErrorKind::UnspecifiedError));
151 }
152 }
153
154 arguments.expect_exhausted()?;
155
156 Ok(color)
157}
158
159fn parse_origin_color<'i, 't>(
161 context: &ParserContext,
162 arguments: &mut Parser<'i, 't>,
163) -> Result<Option<SpecifiedColor>, ParseError<'i>> {
164 if !rcs_enabled() {
165 return Ok(None);
166 }
167
168 if arguments
171 .try_parse(|p| p.expect_ident_matching("from"))
172 .is_err()
173 {
174 return Ok(None);
175 }
176
177 SpecifiedColor::parse(context, arguments).map(Option::Some)
178}
179
180#[inline]
181fn parse_rgb<'i, 't>(
182 context: &ParserContext,
183 arguments: &mut Parser<'i, 't>,
184 origin_color: Option<SpecifiedColor>,
185) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
186 let allow_channel_keyword = origin_color.is_some();
187 let maybe_red = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
188
189 let is_legacy_syntax = origin_color.is_none()
193 && !maybe_red.is_none()
194 && arguments.try_parse(|p| p.expect_comma()).is_ok();
195
196 Ok(if is_legacy_syntax {
197 let (green, blue) = if maybe_red.could_be_percentage() {
198 let green = parse_percentage(context, arguments, false, allow_channel_keyword)?;
199 arguments.expect_comma()?;
200 let blue = parse_percentage(context, arguments, false, allow_channel_keyword)?;
201 (green, blue)
202 } else {
203 let green = parse_number(context, arguments, false, allow_channel_keyword)?;
204 arguments.expect_comma()?;
205 let blue = parse_number(context, arguments, false, allow_channel_keyword)?;
206 (green, blue)
207 };
208
209 let alpha = parse_legacy_alpha(context, arguments)?;
210
211 ColorFunction::Rgb(origin_color.into(), maybe_red, green, blue, alpha)
212 } else {
213 let green = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
214 let blue = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
215
216 let alpha = parse_modern_alpha(context, arguments, allow_channel_keyword)?;
217
218 ColorFunction::Rgb(origin_color.into(), maybe_red, green, blue, alpha)
219 })
220}
221
222#[inline]
226fn parse_hsl<'i, 't>(
227 context: &ParserContext,
228 arguments: &mut Parser<'i, 't>,
229 origin_color: Option<SpecifiedColor>,
230) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
231 let allow_channel_keyword = origin_color.is_some();
232 let hue = parse_number_or_angle(context, arguments, true, allow_channel_keyword)?;
233
234 let is_legacy_syntax = origin_color.is_none()
237 && !hue.is_none()
238 && arguments.try_parse(|p| p.expect_comma()).is_ok();
239
240 let (saturation, lightness, alpha) = if is_legacy_syntax {
241 let saturation = parse_percentage(context, arguments, false, allow_channel_keyword)?;
242 arguments.expect_comma()?;
243 let lightness = parse_percentage(context, arguments, false, allow_channel_keyword)?;
244 let alpha = parse_legacy_alpha(context, arguments)?;
245 (saturation, lightness, alpha)
246 } else {
247 let saturation =
248 parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
249 let lightness =
250 parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
251 let alpha = parse_modern_alpha(context, arguments, allow_channel_keyword)?;
252 (saturation, lightness, alpha)
253 };
254
255 Ok(ColorFunction::Hsl(
256 origin_color.into(),
257 hue,
258 saturation,
259 lightness,
260 alpha,
261 ))
262}
263
264#[inline]
268fn parse_hwb<'i, 't>(
269 context: &ParserContext,
270 arguments: &mut Parser<'i, 't>,
271 origin_color: Option<SpecifiedColor>,
272) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
273 let allow_channel_keyword = origin_color.is_some();
274 let hue = parse_number_or_angle(context, arguments, true, allow_channel_keyword)?;
275 let whiteness = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
276 let blackness = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
277
278 let alpha = parse_modern_alpha(context, arguments, allow_channel_keyword)?;
279
280 Ok(ColorFunction::Hwb(
281 origin_color.into(),
282 hue,
283 whiteness,
284 blackness,
285 alpha,
286 ))
287}
288
289type IntoLabFn<Output> = fn(
290 origin: Optional<SpecifiedColor>,
291 l: ColorComponent<NumberOrPercentageComponent>,
292 a: ColorComponent<NumberOrPercentageComponent>,
293 b: ColorComponent<NumberOrPercentageComponent>,
294 alpha: ColorComponent<NumberOrPercentageComponent>,
295) -> Output;
296
297#[inline]
298fn parse_lab_like<'i, 't>(
299 context: &ParserContext,
300 arguments: &mut Parser<'i, 't>,
301 origin_color: Option<SpecifiedColor>,
302 into_color: IntoLabFn<ColorFunction<SpecifiedColor>>,
303) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
304 let allow_channel_keyword = origin_color.is_some();
305 let lightness = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
306 let a = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
307 let b = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
308
309 let alpha = parse_modern_alpha(context, arguments, allow_channel_keyword)?;
310
311 Ok(into_color(origin_color.into(), lightness, a, b, alpha))
312}
313
314type IntoLchFn<Output> = fn(
315 origin: Optional<SpecifiedColor>,
316 l: ColorComponent<NumberOrPercentageComponent>,
317 a: ColorComponent<NumberOrPercentageComponent>,
318 b: ColorComponent<NumberOrAngleComponent>,
319 alpha: ColorComponent<NumberOrPercentageComponent>,
320) -> Output;
321
322#[inline]
323fn parse_lch_like<'i, 't>(
324 context: &ParserContext,
325 arguments: &mut Parser<'i, 't>,
326 origin_color: Option<SpecifiedColor>,
327 into_color: IntoLchFn<ColorFunction<SpecifiedColor>>,
328) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
329 let allow_channel_keyword = origin_color.is_some();
330 let lightness = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
331 let chroma = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
332 let hue = parse_number_or_angle(context, arguments, true, allow_channel_keyword)?;
333
334 let alpha = parse_modern_alpha(context, arguments, allow_channel_keyword)?;
335
336 Ok(into_color(
337 origin_color.into(),
338 lightness,
339 chroma,
340 hue,
341 alpha,
342 ))
343}
344
345#[inline]
347fn parse_color_with_color_space<'i, 't>(
348 context: &ParserContext,
349 arguments: &mut Parser<'i, 't>,
350 origin_color: Option<SpecifiedColor>,
351) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
352 let allow_channel_keyword = origin_color.is_some();
353 let color_space = PredefinedColorSpace::parse(arguments)?;
354
355 let c1 = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
356 let c2 = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
357 let c3 = parse_number_or_percentage(context, arguments, true, allow_channel_keyword)?;
358
359 let alpha = parse_modern_alpha(context, arguments, allow_channel_keyword)?;
360
361 Ok(ColorFunction::Color(
362 origin_color.into(),
363 c1,
364 c2,
365 c3,
366 alpha,
367 color_space.into(),
368 ))
369}
370
371#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
373#[repr(u8)]
374pub enum NumberOrPercentageComponent {
375 Number(f32),
377 Percentage(f32),
380}
381
382impl NumberOrPercentageComponent {
383 pub fn to_number(&self, percentage_basis: f32) -> f32 {
386 match *self {
387 Self::Number(value) => value,
388 Self::Percentage(unit_value) => unit_value * percentage_basis,
389 }
390 }
391}
392
393impl ColorComponentType for NumberOrPercentageComponent {
394 fn from_value(value: f32) -> Self {
395 Self::Number(value)
396 }
397
398 fn units() -> CalcUnits {
399 CalcUnits::PERCENTAGE
400 }
401
402 fn try_from_token(token: &Token) -> Result<Self, ()> {
403 Ok(match *token {
404 Token::Number { value, .. } => Self::Number(value),
405 Token::Percentage { unit_value, .. } => Self::Percentage(unit_value),
406 _ => {
407 return Err(());
408 },
409 })
410 }
411
412 fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
413 Ok(match *leaf {
414 Leaf::Percentage(p) => Self::Percentage(p.get()),
415 Leaf::Number(n) => Self::Number(n.value()),
416 _ => return Err(()),
417 })
418 }
419}
420
421#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
423#[repr(u8)]
424pub enum NumberOrAngleComponent {
425 Number(f32),
427 Angle(f32),
430}
431
432impl NumberOrAngleComponent {
433 pub fn degrees(&self) -> f32 {
436 match *self {
437 Self::Number(value) => value,
438 Self::Angle(degrees) => degrees,
439 }
440 }
441}
442
443impl ColorComponentType for NumberOrAngleComponent {
444 fn from_value(value: f32) -> Self {
445 Self::Number(value)
446 }
447
448 fn units() -> CalcUnits {
449 CalcUnits::ANGLE
450 }
451
452 fn try_from_token(token: &Token) -> Result<Self, ()> {
453 Ok(match *token {
454 Token::Number { value, .. } => Self::Number(value),
455 Token::Dimension {
456 value, ref unit, ..
457 } => {
458 let degrees = NoCalcAngle::parse_dimension(value, unit)?.degrees();
459 NumberOrAngleComponent::Angle(degrees)
460 },
461 _ => {
462 return Err(());
463 },
464 })
465 }
466
467 fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
468 Ok(match *leaf {
469 Leaf::Angle(angle) => Self::Angle(angle.degrees()),
470 Leaf::Number(n) => Self::Number(n.value()),
471 _ => return Err(()),
472 })
473 }
474}
475
476impl ColorComponentType for f32 {
478 fn from_value(value: f32) -> Self {
479 value
480 }
481
482 fn units() -> CalcUnits {
483 CalcUnits::empty()
484 }
485
486 fn try_from_token(token: &Token) -> Result<Self, ()> {
487 if let Token::Number { value, .. } = *token {
488 Ok(value)
489 } else {
490 Err(())
491 }
492 }
493
494 fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
495 if let Leaf::Number(n) = *leaf {
496 Ok(n.value())
497 } else {
498 Err(())
499 }
500 }
501}
502
503fn parse_number_or_angle<'i, 't>(
505 context: &ParserContext,
506 input: &mut Parser<'i, 't>,
507 allow_none: bool,
508 allow_channel_keyword: bool,
509) -> Result<ColorComponent<NumberOrAngleComponent>, ParseError<'i>> {
510 ColorComponent::parse(context, input, allow_none, allow_channel_keyword)
511}
512
513fn parse_percentage<'i, 't>(
515 context: &ParserContext,
516 input: &mut Parser<'i, 't>,
517 allow_none: bool,
518 allow_channel_keyword: bool,
519) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
520 let location = input.current_source_location();
521
522 let value = ColorComponent::<NumberOrPercentageComponent>::parse(
523 context,
524 input,
525 allow_none,
526 allow_channel_keyword,
527 )?;
528 if !value.could_be_percentage() {
529 return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
530 }
531
532 Ok(value)
533}
534
535fn parse_number<'i, 't>(
537 context: &ParserContext,
538 input: &mut Parser<'i, 't>,
539 allow_none: bool,
540 allow_channel_keyword: bool,
541) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
542 let location = input.current_source_location();
543
544 let value = ColorComponent::<NumberOrPercentageComponent>::parse(
545 context,
546 input,
547 allow_none,
548 allow_channel_keyword,
549 )?;
550
551 if !value.could_be_number() {
552 return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
553 }
554
555 Ok(value)
556}
557
558fn parse_number_or_percentage<'i, 't>(
560 context: &ParserContext,
561 input: &mut Parser<'i, 't>,
562 allow_none: bool,
563 allow_channel_keyword: bool,
564) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
565 ColorComponent::parse(context, input, allow_none, allow_channel_keyword)
566}
567
568fn parse_legacy_alpha<'i, 't>(
569 context: &ParserContext,
570 arguments: &mut Parser<'i, 't>,
571) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
572 if !arguments.is_exhausted() {
573 arguments.expect_comma()?;
574 parse_number_or_percentage(context, arguments, false, false)
575 } else {
576 Ok(ColorComponent::AlphaOmitted)
577 }
578}
579
580fn parse_modern_alpha<'i, 't>(
581 context: &ParserContext,
582 arguments: &mut Parser<'i, 't>,
583 allow_channel_keyword: bool,
584) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
585 if !arguments.is_exhausted() {
586 arguments.expect_delim('/')?;
587 parse_number_or_percentage(context, arguments, true, allow_channel_keyword)
588 } else {
589 Ok(ColorComponent::AlphaOmitted)
590 }
591}
592
593impl ColorComponent<NumberOrPercentageComponent> {
594 fn could_be_number(&self) -> bool {
597 match self {
598 Self::None | Self::AlphaOmitted => true,
599 Self::Value(value) => matches!(value, NumberOrPercentageComponent::Number { .. }),
600 Self::ChannelKeyword(_) => {
601 true
603 },
604 Self::Calc(node) => {
605 if let Ok(unit) = node.unit() {
606 unit.is_empty()
607 } else {
608 false
609 }
610 },
611 }
612 }
613
614 fn could_be_percentage(&self) -> bool {
617 match self {
618 Self::None | Self::AlphaOmitted => true,
619 Self::Value(value) => matches!(value, NumberOrPercentageComponent::Percentage { .. }),
620 Self::ChannelKeyword(_) => {
621 false
623 },
624 Self::Calc(node) => {
625 if let Ok(unit) = node.unit() {
626 unit == CalcUnits::PERCENTAGE
627 } else {
628 false
629 }
630 },
631 }
632 }
633}