parsing.rs (19987B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 #![deny(missing_docs)] 6 7 //! Parsing for CSS colors. 8 9 use super::{ 10 color_function::ColorFunction, 11 component::{ColorComponent, ColorComponentType}, 12 AbsoluteColor, 13 }; 14 use crate::derives::*; 15 use 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 }; 22 use cssparser::{ 23 color::{parse_hash_color, PredefinedColorSpace, OPAQUE}, 24 match_ignore_ascii_case, CowRcStr, Parser, Token, 25 }; 26 use style_traits::{ParseError, StyleParseErrorKind}; 27 28 /// Returns true if the relative color syntax pref is enabled. 29 #[inline] 30 pub fn rcs_enabled() -> bool { 31 static_prefs::pref!("layout.css.relative-color-syntax.enabled") 32 } 33 34 /// Represents a channel keyword inside a color. 35 #[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, PartialOrd, ToCss, ToShmem)] 36 #[repr(u8)] 37 pub enum ChannelKeyword { 38 /// alpha 39 Alpha, 40 /// a 41 A, 42 /// b, blackness, blue 43 B, 44 /// chroma 45 C, 46 /// green 47 G, 48 /// hue 49 H, 50 /// lightness 51 L, 52 /// red 53 R, 54 /// saturation 55 S, 56 /// whiteness 57 W, 58 /// x 59 X, 60 /// y 61 Y, 62 /// z 63 Z, 64 } 65 66 /// Return the named color with the given name. 67 /// 68 /// Matching is case-insensitive in the ASCII range. 69 /// CSS escaping (if relevant) should be resolved before calling this function. 70 /// (For example, the value of an `Ident` token is fine.) 71 #[inline] 72 pub 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 85 /// Parse a CSS color using the specified [`ColorParser`] and return a new color 86 /// value on success. 87 pub 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 // Preserve the color as it was parsed. 106 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 // This will only happen when the parsed color contains errors like calc units 111 // that cannot be resolved at parse time, but will fail when trying to resolve 112 // them, etc. This should be rare, but for now just failing the color value 113 // makes sense. 114 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 /// Parse one of the color functions: rgba(), lab(), color(), etc. 124 #[inline] 125 fn 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 // Validate the channels and calc expressions by trying to resolve them against 147 // transparent. 148 // FIXME(emilio, bug 1925572): This could avoid cloning, or be done earlier. 149 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 160 /// Parse the relative color syntax "from" syntax `from <color>`. 161 fn 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 // Not finding the from keyword is not an error, it just means we don't 170 // have an origin color. 171 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] 182 fn 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 // If the first component is not "none" and is followed by a comma, then we 190 // are parsing the legacy syntax. Legacy syntax also doesn't support an 191 // origin color. 192 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 /// Parses hsl syntax. 223 /// 224 /// <https://drafts.csswg.org/css-color/#the-hsl-notation> 225 #[inline] 226 fn 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 // If the hue is not "none" and is followed by a comma, then we are parsing 234 // the legacy syntax. Legacy syntax also doesn't support an origin color. 235 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 /// Parses hwb syntax. 262 /// 263 /// <https://drafts.csswg.org/css-color/#the-hbw-notation> 264 #[inline] 265 fn 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 285 type 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] 294 fn 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 309 type 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] 318 fn 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 /// Parse the color() function. 340 #[inline] 341 fn 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 /// Either a percentage or a number. 365 #[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)] 366 #[repr(u8)] 367 pub enum NumberOrPercentageComponent { 368 /// `<number>`. 369 Number(f32), 370 /// `<percentage>` 371 /// The value as a float, divided by 100 so that the nominal range is 0.0 to 1.0. 372 Percentage(f32), 373 } 374 375 impl NumberOrPercentageComponent { 376 /// Return the value as a number. Percentages will be adjusted to the range 377 /// [0..percent_basis]. 378 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 386 impl 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 /// Either an angle or a number. 415 #[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)] 416 #[repr(u8)] 417 pub enum NumberOrAngleComponent { 418 /// `<number>`. 419 Number(f32), 420 /// `<angle>` 421 /// The value as a number of degrees. 422 Angle(f32), 423 } 424 425 impl NumberOrAngleComponent { 426 /// Return the angle in degrees. `NumberOrAngle::Number` is returned as 427 /// degrees, because it is the canonical unit. 428 pub fn degrees(&self) -> f32 { 429 match *self { 430 Self::Number(value) => value, 431 Self::Angle(degrees) => degrees, 432 } 433 } 434 } 435 436 impl 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, /* from_calc = */ 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 472 /// The raw f32 here is for <number>. 473 impl 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 499 /// Parse an `<number>` or `<angle>` value. 500 fn 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 508 /// Parse a `<percentage>` value. 509 fn 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 524 /// Parse a `<number>` value. 525 fn 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 541 /// Parse a `<number>` or `<percentage>` value. 542 fn 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 550 fn 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 562 fn 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 574 impl ColorComponent<NumberOrPercentageComponent> { 575 /// Return true if the value contained inside is/can resolve to a number. 576 /// Also returns false if the node is invalid somehow. 577 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 // Channel keywords always resolve to numbers. 583 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 /// Return true if the value contained inside is/can resolve to a percentage. 596 /// Also returns false if the node is invalid somehow. 597 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 // Channel keywords always resolve to numbers. 603 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 }