1use core::error::Error;
7use core::f64;
8use core::fmt;
9use core::str;
10use core::str::FromStr;
11
12use crate::{
13 AlphaColor, ColorSpace, ColorSpaceTag, DynamicColor, Flags, Missing, OpaqueColor, PremulColor,
14 Srgb,
15};
16
17#[derive(Clone, Debug, Eq, PartialEq)]
22#[non_exhaustive]
23pub enum ParseError {
24 UnclosedComment,
26 UnknownAngleDimension,
28 UnknownAngle,
30 UnknownColorComponent,
32 UnknownColorIdentifier,
34 UnknownColorSpace,
36 UnknownColorSyntax,
38 ExpectedArguments,
40 ExpectedClosingParenthesis,
42 ExpectedColorSpaceIdentifier,
44 ExpectedComma,
46 ExpectedEndOfString,
48 WrongNumberOfHexDigits,
50}
51
52impl Error for ParseError {}
53
54impl fmt::Display for ParseError {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 let msg = match *self {
57 Self::UnclosedComment => "unclosed comment",
58 Self::UnknownAngleDimension => "unknown angle dimension",
59 Self::UnknownAngle => "unknown angle",
60 Self::UnknownColorComponent => "unknown color component",
61 Self::UnknownColorIdentifier => "unknown color identifier",
62 Self::UnknownColorSpace => "unknown color space",
63 Self::UnknownColorSyntax => "unknown color syntax",
64 Self::ExpectedArguments => "expected arguments",
65 Self::ExpectedClosingParenthesis => "expected closing parenthesis",
66 Self::ExpectedColorSpaceIdentifier => "expected color space identifier",
67 Self::ExpectedComma => "expected comma",
68 Self::ExpectedEndOfString => "expected end of string",
69 Self::WrongNumberOfHexDigits => "wrong number of hex digits",
70 };
71 f.write_str(msg)
72 }
73}
74
75#[derive(Default)]
76struct Parser<'a> {
77 s: &'a str,
78 ix: usize,
79}
80
81#[derive(Debug, Clone)]
83enum Value<'a> {
84 Symbol(&'a str),
85 Number(f64),
86 Percent(f64),
87 Dimension(f64, &'a str),
88}
89
90#[derive(Clone, Copy, Debug, PartialEq)]
92enum Mode {
93 Legacy,
94 Modern,
95}
96
97impl Mode {
98 fn alpha_separator(self) -> u8 {
99 match self {
100 Self::Legacy => b',',
101 Self::Modern => b'/',
102 }
103 }
104}
105
106#[expect(
107 clippy::cast_possible_truncation,
108 reason = "deliberate choice of f32 for colors"
109)]
110fn color_from_components(components: [Option<f64>; 4], cs: ColorSpaceTag) -> DynamicColor {
111 let mut missing = Missing::default();
112 for (i, component) in components.iter().enumerate() {
113 if component.is_none() {
114 missing.insert(i);
115 }
116 }
117 DynamicColor {
118 cs,
119 flags: Flags::from_missing(missing),
120 components: components.map(|x| x.unwrap_or(0.0) as f32),
121 }
122}
123
124impl<'a> Parser<'a> {
125 fn new(s: &'a str) -> Self {
126 let ix = 0;
127 Parser { s, ix }
128 }
129
130 fn consume_comments(&mut self) -> Result<(), ParseError> {
132 while self.s[self.ix..].starts_with("/*") {
133 if let Some(i) = self.s[self.ix + 2..].find("*/") {
134 self.ix += i + 4;
135 } else {
136 return Err(ParseError::UnclosedComment);
137 }
138 }
139 Ok(())
140 }
141
142 fn number(&mut self) -> Option<f64> {
143 self.consume_comments().ok()?;
144 let tail = &self.s[self.ix..];
145 let mut i = 0;
146 let mut valid = false;
147 if matches!(tail.as_bytes().first(), Some(b'+' | b'-')) {
148 i += 1;
149 }
150 while let Some(c) = tail.as_bytes().get(i) {
151 if c.is_ascii_digit() {
152 valid = true;
153 i += 1;
154 } else {
155 break;
156 }
157 }
158 if let Some(b'.') = tail.as_bytes().get(i) {
159 if let Some(c) = tail.as_bytes().get(i + 1) {
160 if c.is_ascii_digit() {
161 valid = true;
162 i += 2;
163 while let Some(c2) = tail.as_bytes().get(i) {
164 if c2.is_ascii_digit() {
165 i += 1;
166 } else {
167 break;
168 }
169 }
170 }
171 }
172 }
173 if matches!(tail.as_bytes().get(i), Some(b'e' | b'E')) {
174 let mut j = i + 1;
175 if matches!(tail.as_bytes().get(j), Some(b'+' | b'-')) {
176 j += 1;
177 }
178 if let Some(c) = tail.as_bytes().get(j) {
179 if c.is_ascii_digit() {
180 i = j + 1;
181 while let Some(c2) = tail.as_bytes().get(i) {
182 if c2.is_ascii_digit() {
183 i += 1;
184 } else {
185 break;
186 }
187 }
188 }
189 }
190 }
191 if valid {
192 if let Ok(value) = tail[..i].parse() {
194 self.ix += i;
195 return Some(value);
196 }
197 }
198 None
199 }
200
201 fn ident(&mut self) -> Option<&'a str> {
206 let tail = &self.s[self.ix..];
208 let i_init = 0; let mut i = i_init;
210 while i < tail.len() {
211 let b = tail.as_bytes()[i];
212 if b.is_ascii_alphabetic()
213 || b == b'_'
214 || b == b'-'
215 || ((i >= 2 || i == 1 && tail.as_bytes()[i_init] != b'-') && b.is_ascii_digit())
216 {
217 i += 1;
218 } else {
219 break;
220 }
221 }
222 let mut j = i_init;
224 while j < i.min(i_init + 2) {
225 if tail.as_bytes()[j] == b'-' {
226 j += 1;
227 } else {
228 self.ix += i;
229 return Some(&tail[..i]);
230 }
231 }
232 None
233 }
234
235 fn ch(&mut self, ch: u8) -> bool {
236 if self.consume_comments().is_err() {
237 return false;
238 }
239 self.raw_ch(ch)
240 }
241
242 fn raw_ch(&mut self, ch: u8) -> bool {
246 debug_assert!(ch.is_ascii(), "`ch` must be an ASCII character");
247 if self.s.as_bytes().get(self.ix) == Some(&ch) {
248 self.ix += 1;
249 true
250 } else {
251 false
252 }
253 }
254
255 fn ws_one(&mut self) -> bool {
256 if self.consume_comments().is_err() {
257 return false;
258 }
259 let tail = &self.s[self.ix..];
260 let mut i = 0;
261 while let Some(&b) = tail.as_bytes().get(i) {
262 if !(b == b' ' || b == b'\t' || b == b'\r' || b == b'\n') {
263 break;
264 }
265 i += 1;
266 }
267 self.ix += i;
268 i > 0
269 }
270
271 fn ws(&mut self) -> bool {
272 if !self.ws_one() {
273 return false;
274 }
275 while self.consume_comments().is_ok() {
276 if !self.ws_one() {
277 break;
278 }
279 }
280 true
281 }
282
283 fn value(&mut self) -> Option<Value<'a>> {
284 if let Some(number) = self.number() {
285 if self.raw_ch(b'%') {
286 Some(Value::Percent(number))
287 } else if let Some(unit) = self.ident() {
288 Some(Value::Dimension(number, unit))
289 } else {
290 Some(Value::Number(number))
291 }
292 } else {
293 self.ident().map(Value::Symbol)
294 }
295 }
296
297 fn scaled_component(&mut self, scale: f64, pct_scale: f64) -> Result<Option<f64>, ParseError> {
299 self.ws();
300 let value = self.value();
301 match value {
302 Some(Value::Number(n)) => Ok(Some(n * scale)),
303 Some(Value::Percent(n)) => Ok(Some(n * pct_scale)),
304 Some(Value::Symbol(s)) if s.eq_ignore_ascii_case("none") => Ok(None),
305 _ => Err(ParseError::UnknownColorComponent),
306 }
307 }
308
309 fn angle(&mut self) -> Result<Option<f64>, ParseError> {
310 self.ws();
311 let value = self.value();
312 match value {
313 Some(Value::Number(n)) => Ok(Some(n)),
314 Some(Value::Symbol(s)) if s.eq_ignore_ascii_case("none") => Ok(None),
315 Some(Value::Dimension(n, dim)) => {
316 let mut buf = [0; LOWERCASE_BUF_SIZE];
317 let dim_lc = make_lowercase(dim, &mut buf);
318 let scale = match dim_lc {
319 "deg" => 1.0,
320 "rad" => 180.0 / f64::consts::PI,
321 "grad" => 0.9,
322 "turn" => 360.0,
323 _ => return Err(ParseError::UnknownAngleDimension),
324 };
325 Ok(Some(n * scale))
326 }
327 _ => Err(ParseError::UnknownAngle),
328 }
329 }
330
331 fn optional_comma(&mut self, comma: bool) -> Result<(), ParseError> {
332 self.ws();
333 if comma && !self.ch(b',') {
334 Err(ParseError::ExpectedComma)
335 } else {
336 Ok(())
337 }
338 }
339
340 fn rgb(&mut self) -> Result<DynamicColor, ParseError> {
341 if !self.raw_ch(b'(') {
342 return Err(ParseError::ExpectedArguments);
343 }
344 let r = self
347 .scaled_component(1. / 255., 0.01)?
348 .map(|x| x.clamp(0., 1.));
349 self.ws();
350 let comma = self.ch(b',');
351 let mode = if comma { Mode::Legacy } else { Mode::Modern };
352 let g = self
353 .scaled_component(1. / 255., 0.01)?
354 .map(|x| x.clamp(0., 1.));
355 self.optional_comma(comma)?;
356 let b = self
357 .scaled_component(1. / 255., 0.01)?
358 .map(|x| x.clamp(0., 1.));
359 let alpha = self.alpha(mode)?;
360 self.ws();
361 if !self.ch(b')') {
362 return Err(ParseError::ExpectedClosingParenthesis);
363 }
364 Ok(color_from_components([r, g, b, alpha], ColorSpaceTag::Srgb))
365 }
366
367 fn alpha(&mut self, mode: Mode) -> Result<Option<f64>, ParseError> {
381 self.ws();
382 if self.ch(mode.alpha_separator()) {
383 Ok(self.scaled_component(1., 0.01)?.map(|a| a.clamp(0., 1.)))
384 } else {
385 Ok(Some(1.0))
386 }
387 }
388
389 fn lab(&mut self, lmax: f64, c: f64, tag: ColorSpaceTag) -> Result<DynamicColor, ParseError> {
390 if !self.raw_ch(b'(') {
391 return Err(ParseError::ExpectedArguments);
392 }
393 let l = self
394 .scaled_component(1., 0.01 * lmax)?
395 .map(|x| x.clamp(0., lmax));
396 let a = self.scaled_component(1., c)?;
397 let b = self.scaled_component(1., c)?;
398 let alpha = self.alpha(Mode::Modern)?;
399 self.ws();
400 if !self.ch(b')') {
401 return Err(ParseError::ExpectedClosingParenthesis);
402 }
403 Ok(color_from_components([l, a, b, alpha], tag))
404 }
405
406 fn lch(&mut self, lmax: f64, c: f64, tag: ColorSpaceTag) -> Result<DynamicColor, ParseError> {
407 if !self.raw_ch(b'(') {
408 return Err(ParseError::ExpectedArguments);
409 }
410 let l = self
411 .scaled_component(1., 0.01 * lmax)?
412 .map(|x| x.clamp(0., lmax));
413 let c = self.scaled_component(1., c)?.map(|x| x.max(0.));
414 let h = self.angle()?;
415 let alpha = self.alpha(Mode::Modern)?;
416 self.ws();
417 if !self.ch(b')') {
418 return Err(ParseError::ExpectedClosingParenthesis);
419 }
420 Ok(color_from_components([l, c, h, alpha], tag))
421 }
422
423 fn hsl(&mut self) -> Result<DynamicColor, ParseError> {
424 if !self.raw_ch(b'(') {
425 return Err(ParseError::ExpectedArguments);
426 }
427 let h = self.angle()?;
428 let comma = self.ch(b',');
429 let mode = if comma { Mode::Legacy } else { Mode::Modern };
430 let s = self.scaled_component(1., 1.)?.map(|x| x.max(0.));
431 self.optional_comma(comma)?;
432 let l = self.scaled_component(1., 1.)?;
433 let alpha = self.alpha(mode)?;
434 self.ws();
435 if !self.ch(b')') {
436 return Err(ParseError::ExpectedClosingParenthesis);
437 }
438 Ok(color_from_components([h, s, l, alpha], ColorSpaceTag::Hsl))
439 }
440
441 fn hwb(&mut self) -> Result<DynamicColor, ParseError> {
442 if !self.raw_ch(b'(') {
443 return Err(ParseError::ExpectedArguments);
444 }
445 let h = self.angle()?;
446 let w = self.scaled_component(1., 1.)?;
447 let b = self.scaled_component(1., 1.)?;
448 let alpha = self.alpha(Mode::Modern)?;
449 self.ws();
450 if !self.ch(b')') {
451 return Err(ParseError::ExpectedClosingParenthesis);
452 }
453 Ok(color_from_components([h, w, b, alpha], ColorSpaceTag::Hwb))
454 }
455
456 fn color(&mut self) -> Result<DynamicColor, ParseError> {
457 if !self.raw_ch(b'(') {
458 return Err(ParseError::ExpectedArguments);
459 }
460 self.ws();
461 let Some(id) = self.ident() else {
462 return Err(ParseError::ExpectedColorSpaceIdentifier);
463 };
464 let mut buf = [0; LOWERCASE_BUF_SIZE];
465 let id_lc = make_lowercase(id, &mut buf);
466 let cs = match id_lc {
467 "srgb" => ColorSpaceTag::Srgb,
468 "srgb-linear" => ColorSpaceTag::LinearSrgb,
469 "display-p3" => ColorSpaceTag::DisplayP3,
470 "a98-rgb" => ColorSpaceTag::A98Rgb,
471 "prophoto-rgb" => ColorSpaceTag::ProphotoRgb,
472 "rec2020" => ColorSpaceTag::Rec2020,
473 "xyz-d50" => ColorSpaceTag::XyzD50,
474 "xyz" | "xyz-d65" => ColorSpaceTag::XyzD65,
475 _ => return Err(ParseError::UnknownColorSpace),
476 };
477 let r = self.scaled_component(1., 0.01)?;
478 let g = self.scaled_component(1., 0.01)?;
479 let b = self.scaled_component(1., 0.01)?;
480 let alpha = self.alpha(Mode::Modern)?;
481 self.ws();
482 if !self.ch(b')') {
483 return Err(ParseError::ExpectedClosingParenthesis);
484 }
485 Ok(color_from_components([r, g, b, alpha], cs))
486 }
487}
488
489pub fn parse_color_prefix(s: &str) -> Result<(usize, DynamicColor), ParseError> {
499 #[inline]
500 fn set_from_named_color_space(mut color: DynamicColor) -> DynamicColor {
501 color.flags.set_named_color_space();
502 color
503 }
504
505 if let Some(stripped) = s.strip_prefix('#') {
506 let (ix, channels) = get_4bit_hex_channels(stripped)?;
507 let color = color_from_4bit_hex(channels);
508 let mut color = DynamicColor::from_alpha_color(color);
511 color.flags.set_named_color_space();
512 return Ok((ix + 1, color));
513 }
514 let mut parser = Parser::new(s);
515 if let Some(id) = parser.ident() {
516 let mut buf = [0; LOWERCASE_BUF_SIZE];
517 let id_lc = make_lowercase(id, &mut buf);
518 let color = match id_lc {
519 "rgb" | "rgba" => parser.rgb().map(set_from_named_color_space),
520 "lab" => parser
521 .lab(100.0, 1.25, ColorSpaceTag::Lab)
522 .map(set_from_named_color_space),
523 "lch" => parser
524 .lch(100.0, 1.25, ColorSpaceTag::Lch)
525 .map(set_from_named_color_space),
526 "oklab" => parser
527 .lab(1.0, 0.004, ColorSpaceTag::Oklab)
528 .map(set_from_named_color_space),
529 "oklch" => parser
530 .lch(1.0, 0.004, ColorSpaceTag::Oklch)
531 .map(set_from_named_color_space),
532 "hsl" | "hsla" => parser.hsl().map(set_from_named_color_space),
533 "hwb" => parser.hwb().map(set_from_named_color_space),
534 "color" => parser.color(),
535 _ => {
536 if let Some(ix) = crate::x11_colors::lookup_palette_index(id_lc) {
537 let [r, g, b, a] = crate::x11_colors::COLORS[ix];
538 let mut color =
539 DynamicColor::from_alpha_color(AlphaColor::from_rgba8(r, g, b, a));
540 color.flags.set_named_color(ix);
541 Ok(color)
542 } else {
543 Err(ParseError::UnknownColorIdentifier)
544 }
545 }
546 }?;
547
548 Ok((parser.ix, color))
549 } else {
550 Err(ParseError::UnknownColorSyntax)
551 }
552}
553
554pub fn parse_color(s: &str) -> Result<DynamicColor, ParseError> {
565 let s = s.trim();
566 let (ix, color) = parse_color_prefix(s)?;
567
568 if ix == s.len() {
569 Ok(color)
570 } else {
571 Err(ParseError::ExpectedEndOfString)
572 }
573}
574
575impl FromStr for DynamicColor {
576 type Err = ParseError;
577
578 fn from_str(s: &str) -> Result<Self, Self::Err> {
579 parse_color(s)
580 }
581}
582
583impl<CS: ColorSpace> FromStr for AlphaColor<CS> {
584 type Err = ParseError;
585
586 fn from_str(s: &str) -> Result<Self, Self::Err> {
587 parse_color(s).map(DynamicColor::to_alpha_color)
588 }
589}
590
591impl<CS: ColorSpace> FromStr for OpaqueColor<CS> {
592 type Err = ParseError;
593
594 fn from_str(s: &str) -> Result<Self, Self::Err> {
595 parse_color(s)
596 .map(DynamicColor::to_alpha_color)
597 .map(AlphaColor::discard_alpha)
598 }
599}
600
601impl<CS: ColorSpace> FromStr for PremulColor<CS> {
602 type Err = ParseError;
603
604 fn from_str(s: &str) -> Result<Self, Self::Err> {
605 parse_color(s)
606 .map(DynamicColor::to_alpha_color)
607 .map(AlphaColor::premultiply)
608 }
609}
610
611const fn get_4bit_hex_channels(hex_str: &str) -> Result<(usize, [u8; 8]), ParseError> {
616 let mut hex = [0; 8];
617
618 let mut i = 0;
619 while i < 8 && i < hex_str.len() {
620 if let Ok(h) = hex_from_ascii_byte(hex_str.as_bytes()[i]) {
621 hex[i] = h;
622 i += 1;
623 } else {
624 break;
625 }
626 }
627
628 let four_bit_channels = match i {
629 3 => [hex[0], hex[0], hex[1], hex[1], hex[2], hex[2], 15, 15],
630 4 => [
631 hex[0], hex[0], hex[1], hex[1], hex[2], hex[2], hex[3], hex[3],
632 ],
633 6 => [hex[0], hex[1], hex[2], hex[3], hex[4], hex[5], 15, 15],
634 8 => hex,
635 _ => return Err(ParseError::WrongNumberOfHexDigits),
636 };
637
638 Ok((i, four_bit_channels))
639}
640
641const fn hex_from_ascii_byte(b: u8) -> Result<u8, ()> {
642 match b {
643 b'0'..=b'9' => Ok(b - b'0'),
644 b'A'..=b'F' => Ok(b - b'A' + 10),
645 b'a'..=b'f' => Ok(b - b'a' + 10),
646 _ => Err(()),
647 }
648}
649
650const fn color_from_4bit_hex(components: [u8; 8]) -> AlphaColor<Srgb> {
651 let [r0, r1, g0, g1, b0, b1, a0, a1] = components;
652 AlphaColor::from_rgba8(
653 (r0 << 4) | r1,
654 (g0 << 4) | g1,
655 (b0 << 4) | b1,
656 (a0 << 4) | a1,
657 )
658}
659
660impl FromStr for ColorSpaceTag {
661 type Err = ParseError;
662
663 fn from_str(s: &str) -> Result<Self, Self::Err> {
664 let mut buf = [0; LOWERCASE_BUF_SIZE];
665 match make_lowercase(s, &mut buf) {
666 "srgb" => Ok(Self::Srgb),
667 "srgb-linear" => Ok(Self::LinearSrgb),
668 "lab" => Ok(Self::Lab),
669 "lch" => Ok(Self::Lch),
670 "oklab" => Ok(Self::Oklab),
671 "oklch" => Ok(Self::Oklch),
672 "display-p3" => Ok(Self::DisplayP3),
673 "a98-rgb" => Ok(Self::A98Rgb),
674 "prophoto-rgb" => Ok(Self::ProphotoRgb),
675 "xyz-d50" => Ok(Self::XyzD50),
676 "xyz" | "xyz-d65" => Ok(Self::XyzD65),
677 _ => Err(ParseError::UnknownColorSpace),
678 }
679 }
680}
681
682const LOWERCASE_BUF_SIZE: usize = 32;
683
684fn make_lowercase<'a>(s: &'a str, buf: &'a mut [u8; LOWERCASE_BUF_SIZE]) -> &'a str {
690 let len = s.len();
691 if len <= LOWERCASE_BUF_SIZE && s.as_bytes().iter().any(|c| c.is_ascii_uppercase()) {
692 buf[..len].copy_from_slice(s.as_bytes());
693 if let Ok(s_copy) = str::from_utf8_mut(&mut buf[..len]) {
694 s_copy.make_ascii_lowercase();
695 s_copy
696 } else {
697 s
698 }
699 } else {
700 s
701 }
702}
703
704#[cfg(test)]
705mod tests {
706 use crate::DynamicColor;
707
708 use super::{parse_color, parse_color_prefix, Mode, ParseError, Parser};
709
710 fn assert_close_color(c1: DynamicColor, c2: DynamicColor) {
711 const EPSILON: f32 = 1e-4;
712 assert_eq!(c1.cs, c2.cs);
713 for i in 0..4 {
714 assert!((c1.components[i] - c2.components[i]).abs() < EPSILON);
715 }
716 }
717
718 fn assert_err(c: &str, err: ParseError) {
719 assert_eq!(parse_color(c).unwrap_err(), err);
720 }
721
722 #[test]
723 fn x11_color_names() {
724 let red = parse_color("red").unwrap();
725 assert_close_color(red, parse_color("rgb(255, 0, 0)").unwrap());
726 assert_close_color(red, parse_color("\n rgb(255, 0, 0)\t ").unwrap());
727 let lgy = parse_color("lightgoldenrodyellow").unwrap();
728 assert_close_color(lgy, parse_color("rgb(250, 250, 210)").unwrap());
729 let transparent = parse_color("transparent").unwrap();
730 assert_close_color(transparent, parse_color("rgba(0, 0, 0, 0)").unwrap());
731 }
732
733 #[test]
734 fn hex() {
735 let red = parse_color("red").unwrap();
736 assert_close_color(red, parse_color("#f00").unwrap());
737 assert_close_color(red, parse_color("#f00f").unwrap());
738 assert_close_color(red, parse_color("#ff0000ff").unwrap());
739 assert_eq!(
740 parse_color("#f00fa").unwrap_err(),
741 ParseError::WrongNumberOfHexDigits
742 );
743 }
744
745 #[test]
746 fn consume_string() {
747 assert_eq!(
748 parse_color("#ff0000ffa").unwrap_err(),
749 ParseError::ExpectedEndOfString
750 );
751 assert_eq!(
752 parse_color("rgba(255, 100, 0, 1)a").unwrap_err(),
753 ParseError::ExpectedEndOfString
754 );
755 }
756
757 #[test]
758 fn prefix() {
759 for (color, trailing) in [
760 ("color(rec2020 0.2 0.3 0.4 / 0.85)trailing", "trailing"),
761 ("color(rec2020 0.2 0.3 0.4 / 0.85) ", " "),
762 ("color(rec2020 0.2 0.3 0.4 / 0.85)", ""),
763 ("red\0", "\0"),
764 ("#ffftrailing", "trailing"),
765 ("#fffffftr", "tr"),
766 ] {
767 assert_eq!(&color[parse_color_prefix(color).unwrap().0..], trailing);
768 }
769 }
770
771 #[test]
772 fn consume_comments() {
773 for (s, remaining) in [
774 ("/* abc */ def", " def"),
775 ("/* *//* */abc", "abc"),
776 ("/* /* */abc", "abc"),
777 ] {
778 let mut parser = Parser::new(s);
779 assert!(parser.consume_comments().is_ok());
780 assert_eq!(&parser.s[parser.ix..], remaining);
781 }
782 }
783
784 #[test]
785 fn alpha() {
786 for (alpha, expected, mode) in [
787 (", 10%", Ok(Some(0.1)), Mode::Legacy),
788 ("/ 0.25", Ok(Some(0.25)), Mode::Modern),
789 ("/ -0.3", Ok(Some(0.)), Mode::Modern),
790 ("/ 110%", Ok(Some(1.)), Mode::Modern),
791 ("", Ok(Some(1.)), Mode::Legacy),
792 ("/ none", Ok(None), Mode::Modern),
793 ] {
794 let mut parser = Parser::new(alpha);
795 let result = parser.alpha(mode);
796 assert_eq!(result, expected,
797 "Failed parsing specified alpha `{alpha}`. Expected: `{expected:?}`. Got: `{result:?}`.");
798 }
799 }
800
801 #[test]
802 fn angles() {
803 for (angle, expected) in [
804 ("90deg", 90.),
805 ("1.5707963rad", 90.),
806 ("100grad", 90.),
807 ("0.25turn", 90.),
808 ] {
809 let mut parser = Parser::new(angle);
810 let result = parser.angle().unwrap().unwrap();
811 assert!((result - expected).abs() < 1e-4,
812 "Failed parsing specified angle `{angle}`. Expected: `{expected:?}`. Got: `{result:?}`.");
813 }
814
815 {
816 let mut parser = Parser::new("none");
817 assert_eq!(parser.angle().unwrap(), None);
818 }
819
820 assert_err(
821 "hwb(1turns 20% 30% / 50%)",
822 ParseError::UnknownAngleDimension,
823 );
824 }
825
826 #[test]
827 fn case_insensitive() {
828 for (c1, c2) in [
829 ("red", "ReD"),
830 ("lightgoldenrodyellow", "LightGoldenRodYellow"),
831 ("rgb(102, 51, 153)", "RGB(102, 51, 153)"),
832 (
833 "color(rec2020 0.2 0.3 0.4 / 0.85)",
834 "CoLoR(ReC2020 0.2 0.3 0.4 / 0.85)",
835 ),
836 ("hwb(120deg 30% 50%)", "HwB(120DeG 30% 50%)"),
837 ("hsl(none none none)", "HSL(NONE NONE NONE)"),
838 ] {
839 assert_close_color(parse_color(c1).unwrap(), parse_color(c2).unwrap());
840 }
841 }
842}