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::Angle as SpecifiedAngle, 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() {
108 Ok(SpecifiedColor::from_absolute_color(resolved))
109 } else {
110 Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError))
115 }
116 });
117 },
118 _ => Err(()),
119 }
120 .map_err(|()| location.new_unexpected_token_error(token.clone()))
121}
122
123#[inline]
125fn parse_color_function<'i, 't>(
126 context: &ParserContext,
127 name: CowRcStr<'i>,
128 arguments: &mut Parser<'i, 't>,
129) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
130 let origin_color = parse_origin_color(context, arguments)?;
131 let has_origin_color = origin_color.is_some();
132
133 let color = match_ignore_ascii_case! { &name,
134 "rgb" | "rgba" => parse_rgb(context, arguments, origin_color),
135 "hsl" | "hsla" => parse_hsl(context, arguments, origin_color),
136 "hwb" => parse_hwb(context, arguments, origin_color),
137 "lab" => parse_lab_like(context, arguments, origin_color, ColorFunction::Lab),
138 "lch" => parse_lch_like(context, arguments, origin_color, ColorFunction::Lch),
139 "oklab" => parse_lab_like(context, arguments, origin_color, ColorFunction::Oklab),
140 "oklch" => parse_lch_like(context, arguments, origin_color, ColorFunction::Oklch),
141 "color" => parse_color_with_color_space(context, arguments, origin_color),
142 _ => return Err(arguments.new_unexpected_token_error(Token::Ident(name))),
143 }?;
144
145 if has_origin_color {
146 let abs = color.map_origin_color(|_| Some(AbsoluteColor::TRANSPARENT_BLACK));
150 if abs.resolve_to_absolute().is_err() {
151 return Err(arguments.new_custom_error(StyleParseErrorKind::UnspecifiedError));
152 }
153 }
154
155 arguments.expect_exhausted()?;
156
157 Ok(color)
158}
159
160fn parse_origin_color<'i, 't>(
162 context: &ParserContext,
163 arguments: &mut Parser<'i, 't>,
164) -> Result<Option<SpecifiedColor>, ParseError<'i>> {
165 if !rcs_enabled() {
166 return Ok(None);
167 }
168
169 if arguments
172 .try_parse(|p| p.expect_ident_matching("from"))
173 .is_err()
174 {
175 return Ok(None);
176 }
177
178 SpecifiedColor::parse(context, arguments).map(Option::Some)
179}
180
181#[inline]
182fn parse_rgb<'i, 't>(
183 context: &ParserContext,
184 arguments: &mut Parser<'i, 't>,
185 origin_color: Option<SpecifiedColor>,
186) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
187 let maybe_red = parse_number_or_percentage(context, arguments, true)?;
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)?;
199 arguments.expect_comma()?;
200 let blue = parse_percentage(context, arguments, false)?;
201 (green, blue)
202 } else {
203 let green = parse_number(context, arguments, false)?;
204 arguments.expect_comma()?;
205 let blue = parse_number(context, arguments, false)?;
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)?;
214 let blue = parse_number_or_percentage(context, arguments, true)?;
215
216 let alpha = parse_modern_alpha(context, arguments)?;
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 hue = parse_number_or_angle(context, arguments, true)?;
232
233 let is_legacy_syntax = origin_color.is_none()
236 && !hue.is_none()
237 && arguments.try_parse(|p| p.expect_comma()).is_ok();
238
239 let (saturation, lightness, alpha) = if is_legacy_syntax {
240 let saturation = parse_percentage(context, arguments, false)?;
241 arguments.expect_comma()?;
242 let lightness = parse_percentage(context, arguments, false)?;
243 let alpha = parse_legacy_alpha(context, arguments)?;
244 (saturation, lightness, alpha)
245 } else {
246 let saturation = parse_number_or_percentage(context, arguments, true)?;
247 let lightness = parse_number_or_percentage(context, arguments, true)?;
248 let alpha = parse_modern_alpha(context, arguments)?;
249 (saturation, lightness, alpha)
250 };
251
252 Ok(ColorFunction::Hsl(
253 origin_color.into(),
254 hue,
255 saturation,
256 lightness,
257 alpha,
258 ))
259}
260
261#[inline]
265fn parse_hwb<'i, 't>(
266 context: &ParserContext,
267 arguments: &mut Parser<'i, 't>,
268 origin_color: Option<SpecifiedColor>,
269) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
270 let hue = parse_number_or_angle(context, arguments, true)?;
271 let whiteness = parse_number_or_percentage(context, arguments, true)?;
272 let blackness = parse_number_or_percentage(context, arguments, true)?;
273
274 let alpha = parse_modern_alpha(context, arguments)?;
275
276 Ok(ColorFunction::Hwb(
277 origin_color.into(),
278 hue,
279 whiteness,
280 blackness,
281 alpha,
282 ))
283}
284
285type IntoLabFn<Output> = fn(
286 origin: Optional<SpecifiedColor>,
287 l: ColorComponent<NumberOrPercentageComponent>,
288 a: ColorComponent<NumberOrPercentageComponent>,
289 b: ColorComponent<NumberOrPercentageComponent>,
290 alpha: ColorComponent<NumberOrPercentageComponent>,
291) -> Output;
292
293#[inline]
294fn parse_lab_like<'i, 't>(
295 context: &ParserContext,
296 arguments: &mut Parser<'i, 't>,
297 origin_color: Option<SpecifiedColor>,
298 into_color: IntoLabFn<ColorFunction<SpecifiedColor>>,
299) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
300 let lightness = parse_number_or_percentage(context, arguments, true)?;
301 let a = parse_number_or_percentage(context, arguments, true)?;
302 let b = parse_number_or_percentage(context, arguments, true)?;
303
304 let alpha = parse_modern_alpha(context, arguments)?;
305
306 Ok(into_color(origin_color.into(), lightness, a, b, alpha))
307}
308
309type IntoLchFn<Output> = fn(
310 origin: Optional<SpecifiedColor>,
311 l: ColorComponent<NumberOrPercentageComponent>,
312 a: ColorComponent<NumberOrPercentageComponent>,
313 b: ColorComponent<NumberOrAngleComponent>,
314 alpha: ColorComponent<NumberOrPercentageComponent>,
315) -> Output;
316
317#[inline]
318fn parse_lch_like<'i, 't>(
319 context: &ParserContext,
320 arguments: &mut Parser<'i, 't>,
321 origin_color: Option<SpecifiedColor>,
322 into_color: IntoLchFn<ColorFunction<SpecifiedColor>>,
323) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
324 let lightness = parse_number_or_percentage(context, arguments, true)?;
325 let chroma = parse_number_or_percentage(context, arguments, true)?;
326 let hue = parse_number_or_angle(context, arguments, true)?;
327
328 let alpha = parse_modern_alpha(context, arguments)?;
329
330 Ok(into_color(
331 origin_color.into(),
332 lightness,
333 chroma,
334 hue,
335 alpha,
336 ))
337}
338
339#[inline]
341fn parse_color_with_color_space<'i, 't>(
342 context: &ParserContext,
343 arguments: &mut Parser<'i, 't>,
344 origin_color: Option<SpecifiedColor>,
345) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
346 let color_space = PredefinedColorSpace::parse(arguments)?;
347
348 let c1 = parse_number_or_percentage(context, arguments, true)?;
349 let c2 = parse_number_or_percentage(context, arguments, true)?;
350 let c3 = parse_number_or_percentage(context, arguments, true)?;
351
352 let alpha = parse_modern_alpha(context, arguments)?;
353
354 Ok(ColorFunction::Color(
355 origin_color.into(),
356 c1,
357 c2,
358 c3,
359 alpha,
360 color_space.into(),
361 ))
362}
363
364#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
366#[repr(u8)]
367pub enum NumberOrPercentageComponent {
368 Number(f32),
370 Percentage(f32),
373}
374
375impl NumberOrPercentageComponent {
376 pub fn to_number(&self, percentage_basis: f32) -> f32 {
379 match *self {
380 Self::Number(value) => value,
381 Self::Percentage(unit_value) => unit_value * percentage_basis,
382 }
383 }
384}
385
386impl ColorComponentType for NumberOrPercentageComponent {
387 fn from_value(value: f32) -> Self {
388 Self::Number(value)
389 }
390
391 fn units() -> CalcUnits {
392 CalcUnits::PERCENTAGE
393 }
394
395 fn try_from_token(token: &Token) -> Result<Self, ()> {
396 Ok(match *token {
397 Token::Number { value, .. } => Self::Number(value),
398 Token::Percentage { unit_value, .. } => Self::Percentage(unit_value),
399 _ => {
400 return Err(());
401 },
402 })
403 }
404
405 fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
406 Ok(match *leaf {
407 Leaf::Percentage(unit_value) => Self::Percentage(unit_value),
408 Leaf::Number(value) => Self::Number(value),
409 _ => return Err(()),
410 })
411 }
412}
413
414#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
416#[repr(u8)]
417pub enum NumberOrAngleComponent {
418 Number(f32),
420 Angle(f32),
423}
424
425impl NumberOrAngleComponent {
426 pub fn degrees(&self) -> f32 {
429 match *self {
430 Self::Number(value) => value,
431 Self::Angle(degrees) => degrees,
432 }
433 }
434}
435
436impl ColorComponentType for NumberOrAngleComponent {
437 fn from_value(value: f32) -> Self {
438 Self::Number(value)
439 }
440
441 fn units() -> CalcUnits {
442 CalcUnits::ANGLE
443 }
444
445 fn try_from_token(token: &Token) -> Result<Self, ()> {
446 Ok(match *token {
447 Token::Number { value, .. } => Self::Number(value),
448 Token::Dimension {
449 value, ref unit, ..
450 } => {
451 let degrees =
452 SpecifiedAngle::parse_dimension(value, unit, false)
453 .map(|angle| angle.degrees())?;
454
455 NumberOrAngleComponent::Angle(degrees)
456 },
457 _ => {
458 return Err(());
459 },
460 })
461 }
462
463 fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
464 Ok(match *leaf {
465 Leaf::Angle(angle) => Self::Angle(angle.degrees()),
466 Leaf::Number(value) => Self::Number(value),
467 _ => return Err(()),
468 })
469 }
470}
471
472impl ColorComponentType for f32 {
474 fn from_value(value: f32) -> Self {
475 value
476 }
477
478 fn units() -> CalcUnits {
479 CalcUnits::empty()
480 }
481
482 fn try_from_token(token: &Token) -> Result<Self, ()> {
483 if let Token::Number { value, .. } = *token {
484 Ok(value)
485 } else {
486 Err(())
487 }
488 }
489
490 fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
491 if let Leaf::Number(value) = *leaf {
492 Ok(value)
493 } else {
494 Err(())
495 }
496 }
497}
498
499fn parse_number_or_angle<'i, 't>(
501 context: &ParserContext,
502 input: &mut Parser<'i, 't>,
503 allow_none: bool,
504) -> Result<ColorComponent<NumberOrAngleComponent>, ParseError<'i>> {
505 ColorComponent::parse(context, input, allow_none)
506}
507
508fn parse_percentage<'i, 't>(
510 context: &ParserContext,
511 input: &mut Parser<'i, 't>,
512 allow_none: bool,
513) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
514 let location = input.current_source_location();
515
516 let value = ColorComponent::<NumberOrPercentageComponent>::parse(context, input, allow_none)?;
517 if !value.could_be_percentage() {
518 return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
519 }
520
521 Ok(value)
522}
523
524fn parse_number<'i, 't>(
526 context: &ParserContext,
527 input: &mut Parser<'i, 't>,
528 allow_none: bool,
529) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
530 let location = input.current_source_location();
531
532 let value = ColorComponent::<NumberOrPercentageComponent>::parse(context, input, allow_none)?;
533
534 if !value.could_be_number() {
535 return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
536 }
537
538 Ok(value)
539}
540
541fn parse_number_or_percentage<'i, 't>(
543 context: &ParserContext,
544 input: &mut Parser<'i, 't>,
545 allow_none: bool,
546) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
547 ColorComponent::parse(context, input, allow_none)
548}
549
550fn parse_legacy_alpha<'i, 't>(
551 context: &ParserContext,
552 arguments: &mut Parser<'i, 't>,
553) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
554 if !arguments.is_exhausted() {
555 arguments.expect_comma()?;
556 parse_number_or_percentage(context, arguments, false)
557 } else {
558 Ok(ColorComponent::AlphaOmitted)
559 }
560}
561
562fn parse_modern_alpha<'i, 't>(
563 context: &ParserContext,
564 arguments: &mut Parser<'i, 't>,
565) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
566 if !arguments.is_exhausted() {
567 arguments.expect_delim('/')?;
568 parse_number_or_percentage(context, arguments, true)
569 } else {
570 Ok(ColorComponent::AlphaOmitted)
571 }
572}
573
574impl ColorComponent<NumberOrPercentageComponent> {
575 fn could_be_number(&self) -> bool {
578 match self {
579 Self::None | Self::AlphaOmitted => true,
580 Self::Value(value) => matches!(value, NumberOrPercentageComponent::Number { .. }),
581 Self::ChannelKeyword(_) => {
582 true
584 },
585 Self::Calc(node) => {
586 if let Ok(unit) = node.unit() {
587 unit.is_empty()
588 } else {
589 false
590 }
591 },
592 }
593 }
594
595 fn could_be_percentage(&self) -> bool {
598 match self {
599 Self::None | Self::AlphaOmitted => true,
600 Self::Value(value) => matches!(value, NumberOrPercentageComponent::Percentage { .. }),
601 Self::ChannelKeyword(_) => {
602 false
604 },
605 Self::Calc(node) => {
606 if let Ok(unit) = node.unit() {
607 unit == CalcUnits::PERCENTAGE
608 } else {
609 false
610 }
611 },
612 }
613 }
614}