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