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
150 .map_origin_color(|_| Ok(AbsoluteColor::TRANSPARENT_BLACK))
151 .unwrap();
152 if abs.resolve_to_absolute().is_err() {
153 return Err(arguments.new_custom_error(StyleParseErrorKind::UnspecifiedError));
154 }
155 }
156
157 arguments.expect_exhausted()?;
158
159 Ok(color)
160}
161
162fn parse_origin_color<'i, 't>(
164 context: &ParserContext,
165 arguments: &mut Parser<'i, 't>,
166) -> Result<Option<SpecifiedColor>, ParseError<'i>> {
167 if !rcs_enabled() {
168 return Ok(None);
169 }
170
171 if arguments
174 .try_parse(|p| p.expect_ident_matching("from"))
175 .is_err()
176 {
177 return Ok(None);
178 }
179
180 SpecifiedColor::parse(context, arguments).map(Option::Some)
181}
182
183#[inline]
184fn parse_rgb<'i, 't>(
185 context: &ParserContext,
186 arguments: &mut Parser<'i, 't>,
187 origin_color: Option<SpecifiedColor>,
188) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
189 let maybe_red = parse_number_or_percentage(context, arguments, true)?;
190
191 let is_legacy_syntax = origin_color.is_none()
195 && !maybe_red.is_none()
196 && arguments.try_parse(|p| p.expect_comma()).is_ok();
197
198 Ok(if is_legacy_syntax {
199 let (green, blue) = if maybe_red.could_be_percentage() {
200 let green = parse_percentage(context, arguments, false)?;
201 arguments.expect_comma()?;
202 let blue = parse_percentage(context, arguments, false)?;
203 (green, blue)
204 } else {
205 let green = parse_number(context, arguments, false)?;
206 arguments.expect_comma()?;
207 let blue = parse_number(context, arguments, false)?;
208 (green, blue)
209 };
210
211 let alpha = parse_legacy_alpha(context, arguments)?;
212
213 ColorFunction::Rgb(origin_color.into(), maybe_red, green, blue, alpha)
214 } else {
215 let green = parse_number_or_percentage(context, arguments, true)?;
216 let blue = parse_number_or_percentage(context, arguments, true)?;
217
218 let alpha = parse_modern_alpha(context, arguments)?;
219
220 ColorFunction::Rgb(origin_color.into(), maybe_red, green, blue, alpha)
221 })
222}
223
224#[inline]
228fn parse_hsl<'i, 't>(
229 context: &ParserContext,
230 arguments: &mut Parser<'i, 't>,
231 origin_color: Option<SpecifiedColor>,
232) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
233 let hue = parse_number_or_angle(context, arguments, true)?;
234
235 let is_legacy_syntax = origin_color.is_none()
238 && !hue.is_none()
239 && arguments.try_parse(|p| p.expect_comma()).is_ok();
240
241 let (saturation, lightness, alpha) = if is_legacy_syntax {
242 let saturation = parse_percentage(context, arguments, false)?;
243 arguments.expect_comma()?;
244 let lightness = parse_percentage(context, arguments, false)?;
245 let alpha = parse_legacy_alpha(context, arguments)?;
246 (saturation, lightness, alpha)
247 } else {
248 let saturation = parse_number_or_percentage(context, arguments, true)?;
249 let lightness = parse_number_or_percentage(context, arguments, true)?;
250 let alpha = parse_modern_alpha(context, arguments)?;
251 (saturation, lightness, alpha)
252 };
253
254 Ok(ColorFunction::Hsl(
255 origin_color.into(),
256 hue,
257 saturation,
258 lightness,
259 alpha,
260 ))
261}
262
263#[inline]
267fn parse_hwb<'i, 't>(
268 context: &ParserContext,
269 arguments: &mut Parser<'i, 't>,
270 origin_color: Option<SpecifiedColor>,
271) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
272 let hue = parse_number_or_angle(context, arguments, true)?;
273 let whiteness = parse_number_or_percentage(context, arguments, true)?;
274 let blackness = parse_number_or_percentage(context, arguments, true)?;
275
276 let alpha = parse_modern_alpha(context, arguments)?;
277
278 Ok(ColorFunction::Hwb(
279 origin_color.into(),
280 hue,
281 whiteness,
282 blackness,
283 alpha,
284 ))
285}
286
287type IntoLabFn<Output> = fn(
288 origin: Optional<SpecifiedColor>,
289 l: ColorComponent<NumberOrPercentageComponent>,
290 a: ColorComponent<NumberOrPercentageComponent>,
291 b: ColorComponent<NumberOrPercentageComponent>,
292 alpha: ColorComponent<NumberOrPercentageComponent>,
293) -> Output;
294
295#[inline]
296fn parse_lab_like<'i, 't>(
297 context: &ParserContext,
298 arguments: &mut Parser<'i, 't>,
299 origin_color: Option<SpecifiedColor>,
300 into_color: IntoLabFn<ColorFunction<SpecifiedColor>>,
301) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
302 let lightness = parse_number_or_percentage(context, arguments, true)?;
303 let a = parse_number_or_percentage(context, arguments, true)?;
304 let b = parse_number_or_percentage(context, arguments, true)?;
305
306 let alpha = parse_modern_alpha(context, arguments)?;
307
308 Ok(into_color(origin_color.into(), lightness, a, b, alpha))
309}
310
311type IntoLchFn<Output> = fn(
312 origin: Optional<SpecifiedColor>,
313 l: ColorComponent<NumberOrPercentageComponent>,
314 a: ColorComponent<NumberOrPercentageComponent>,
315 b: ColorComponent<NumberOrAngleComponent>,
316 alpha: ColorComponent<NumberOrPercentageComponent>,
317) -> Output;
318
319#[inline]
320fn parse_lch_like<'i, 't>(
321 context: &ParserContext,
322 arguments: &mut Parser<'i, 't>,
323 origin_color: Option<SpecifiedColor>,
324 into_color: IntoLchFn<ColorFunction<SpecifiedColor>>,
325) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
326 let lightness = parse_number_or_percentage(context, arguments, true)?;
327 let chroma = parse_number_or_percentage(context, arguments, true)?;
328 let hue = parse_number_or_angle(context, arguments, true)?;
329
330 let alpha = parse_modern_alpha(context, arguments)?;
331
332 Ok(into_color(
333 origin_color.into(),
334 lightness,
335 chroma,
336 hue,
337 alpha,
338 ))
339}
340
341#[inline]
343fn parse_color_with_color_space<'i, 't>(
344 context: &ParserContext,
345 arguments: &mut Parser<'i, 't>,
346 origin_color: Option<SpecifiedColor>,
347) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
348 let color_space = PredefinedColorSpace::parse(arguments)?;
349
350 let c1 = parse_number_or_percentage(context, arguments, true)?;
351 let c2 = parse_number_or_percentage(context, arguments, true)?;
352 let c3 = parse_number_or_percentage(context, arguments, true)?;
353
354 let alpha = parse_modern_alpha(context, arguments)?;
355
356 Ok(ColorFunction::Color(
357 origin_color.into(),
358 c1,
359 c2,
360 c3,
361 alpha,
362 color_space.into(),
363 ))
364}
365
366#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
368#[repr(u8)]
369pub enum NumberOrPercentageComponent {
370 Number(f32),
372 Percentage(f32),
375}
376
377impl NumberOrPercentageComponent {
378 pub fn to_number(&self, percentage_basis: f32) -> f32 {
381 match *self {
382 Self::Number(value) => value,
383 Self::Percentage(unit_value) => unit_value * percentage_basis,
384 }
385 }
386}
387
388impl ColorComponentType for NumberOrPercentageComponent {
389 fn from_value(value: f32) -> Self {
390 Self::Number(value)
391 }
392
393 fn units() -> CalcUnits {
394 CalcUnits::PERCENTAGE
395 }
396
397 fn try_from_token(token: &Token) -> Result<Self, ()> {
398 Ok(match *token {
399 Token::Number { value, .. } => Self::Number(value),
400 Token::Percentage { unit_value, .. } => Self::Percentage(unit_value),
401 _ => {
402 return Err(());
403 },
404 })
405 }
406
407 fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
408 Ok(match *leaf {
409 Leaf::Percentage(unit_value) => Self::Percentage(unit_value),
410 Leaf::Number(value) => Self::Number(value),
411 _ => return Err(()),
412 })
413 }
414}
415
416#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
418#[repr(u8)]
419pub enum NumberOrAngleComponent {
420 Number(f32),
422 Angle(f32),
425}
426
427impl NumberOrAngleComponent {
428 pub fn degrees(&self) -> f32 {
431 match *self {
432 Self::Number(value) => value,
433 Self::Angle(degrees) => degrees,
434 }
435 }
436}
437
438impl ColorComponentType for NumberOrAngleComponent {
439 fn from_value(value: f32) -> Self {
440 Self::Number(value)
441 }
442
443 fn units() -> CalcUnits {
444 CalcUnits::ANGLE
445 }
446
447 fn try_from_token(token: &Token) -> Result<Self, ()> {
448 Ok(match *token {
449 Token::Number { value, .. } => Self::Number(value),
450 Token::Dimension {
451 value, ref unit, ..
452 } => {
453 let degrees =
454 SpecifiedAngle::parse_dimension(value, unit, false)
455 .map(|angle| angle.degrees())?;
456
457 NumberOrAngleComponent::Angle(degrees)
458 },
459 _ => {
460 return Err(());
461 },
462 })
463 }
464
465 fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
466 Ok(match *leaf {
467 Leaf::Angle(angle) => Self::Angle(angle.degrees()),
468 Leaf::Number(value) => Self::Number(value),
469 _ => return Err(()),
470 })
471 }
472}
473
474impl ColorComponentType for f32 {
476 fn from_value(value: f32) -> Self {
477 value
478 }
479
480 fn units() -> CalcUnits {
481 CalcUnits::empty()
482 }
483
484 fn try_from_token(token: &Token) -> Result<Self, ()> {
485 if let Token::Number { value, .. } = *token {
486 Ok(value)
487 } else {
488 Err(())
489 }
490 }
491
492 fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
493 if let Leaf::Number(value) = *leaf {
494 Ok(value)
495 } else {
496 Err(())
497 }
498 }
499}
500
501fn parse_number_or_angle<'i, 't>(
503 context: &ParserContext,
504 input: &mut Parser<'i, 't>,
505 allow_none: bool,
506) -> Result<ColorComponent<NumberOrAngleComponent>, ParseError<'i>> {
507 ColorComponent::parse(context, input, allow_none)
508}
509
510fn parse_percentage<'i, 't>(
512 context: &ParserContext,
513 input: &mut Parser<'i, 't>,
514 allow_none: bool,
515) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
516 let location = input.current_source_location();
517
518 let value = ColorComponent::<NumberOrPercentageComponent>::parse(context, input, allow_none)?;
519 if !value.could_be_percentage() {
520 return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
521 }
522
523 Ok(value)
524}
525
526fn parse_number<'i, 't>(
528 context: &ParserContext,
529 input: &mut Parser<'i, 't>,
530 allow_none: bool,
531) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
532 let location = input.current_source_location();
533
534 let value = ColorComponent::<NumberOrPercentageComponent>::parse(context, input, allow_none)?;
535
536 if !value.could_be_number() {
537 return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
538 }
539
540 Ok(value)
541}
542
543fn parse_number_or_percentage<'i, 't>(
545 context: &ParserContext,
546 input: &mut Parser<'i, 't>,
547 allow_none: bool,
548) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
549 ColorComponent::parse(context, input, allow_none)
550}
551
552fn parse_legacy_alpha<'i, 't>(
553 context: &ParserContext,
554 arguments: &mut Parser<'i, 't>,
555) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
556 if !arguments.is_exhausted() {
557 arguments.expect_comma()?;
558 parse_number_or_percentage(context, arguments, false)
559 } else {
560 Ok(ColorComponent::AlphaOmitted)
561 }
562}
563
564fn parse_modern_alpha<'i, 't>(
565 context: &ParserContext,
566 arguments: &mut Parser<'i, 't>,
567) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
568 if !arguments.is_exhausted() {
569 arguments.expect_delim('/')?;
570 parse_number_or_percentage(context, arguments, true)
571 } else {
572 Ok(ColorComponent::AlphaOmitted)
573 }
574}
575
576impl ColorComponent<NumberOrPercentageComponent> {
577 fn could_be_number(&self) -> bool {
580 match self {
581 Self::None | Self::AlphaOmitted => true,
582 Self::Value(value) => matches!(value, NumberOrPercentageComponent::Number { .. }),
583 Self::ChannelKeyword(_) => {
584 true
586 },
587 Self::Calc(node) => {
588 if let Ok(unit) = node.unit() {
589 unit.is_empty()
590 } else {
591 false
592 }
593 },
594 }
595 }
596
597 fn could_be_percentage(&self) -> bool {
600 match self {
601 Self::None | Self::AlphaOmitted => true,
602 Self::Value(value) => matches!(value, NumberOrPercentageComponent::Percentage { .. }),
603 Self::ChannelKeyword(_) => {
604 false
606 },
607 Self::Calc(node) => {
608 if let Ok(unit) = node.unit() {
609 unit == CalcUnits::PERCENTAGE
610 } else {
611 false
612 }
613 },
614 }
615 }
616}