1use alloc::string::{String, ToString};
4use core::fmt;
5use crate::corety::AzString;
6
7use crate::props::{
8 basic::{
9 color::{parse_css_color, ColorU, CssColorParseError, CssColorParseErrorOwned},
10 pixel::{
11 parse_pixel_value_no_percent, CssPixelValueParseError, CssPixelValueParseErrorOwned,
12 PixelValueNoPercent,
13 },
14 },
15 formatter::PrintAsCssValue,
16};
17
18#[derive(Debug, Default, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)]
20#[repr(C)]
21pub enum BoxShadowClipMode {
22 #[default]
23 Outset,
24 Inset,
25}
26
27impl fmt::Display for BoxShadowClipMode {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self {
30 Self::Outset => Ok(()), Self::Inset => write!(f, "inset"),
32 }
33 }
34}
35
36#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
38#[repr(C)]
39pub struct StyleBoxShadow {
40 pub offset_x: PixelValueNoPercent,
41 pub offset_y: PixelValueNoPercent,
42 pub blur_radius: PixelValueNoPercent,
43 pub spread_radius: PixelValueNoPercent,
44 pub clip_mode: BoxShadowClipMode,
45 pub color: ColorU,
46}
47
48impl Default for StyleBoxShadow {
49 fn default() -> Self {
50 Self {
51 offset_x: PixelValueNoPercent::default(),
52 offset_y: PixelValueNoPercent::default(),
53 blur_radius: PixelValueNoPercent::default(),
54 spread_radius: PixelValueNoPercent::default(),
55 clip_mode: BoxShadowClipMode::default(),
56 color: ColorU::BLACK,
57 }
58 }
59}
60
61impl StyleBoxShadow {
62 pub fn scale_for_dpi(&mut self, scale_factor: f32) {
64 self.offset_x.scale_for_dpi(scale_factor);
65 self.offset_y.scale_for_dpi(scale_factor);
66 self.blur_radius.scale_for_dpi(scale_factor);
67 self.spread_radius.scale_for_dpi(scale_factor);
68 }
69}
70
71impl PrintAsCssValue for StyleBoxShadow {
72 fn print_as_css_value(&self) -> String {
73 let mut components = Vec::new();
74
75 if self.clip_mode == BoxShadowClipMode::Inset {
76 components.push("inset".to_string());
77 }
78 components.push(self.offset_x.to_string());
79 components.push(self.offset_y.to_string());
80
81 if self.blur_radius.inner.number.get() != 0.0
83 || self.spread_radius.inner.number.get() != 0.0
84 {
85 components.push(self.blur_radius.to_string());
86 }
87 if self.spread_radius.inner.number.get() != 0.0 {
88 components.push(self.spread_radius.to_string());
89 }
90 if self.color != ColorU::BLACK {
91 components.push(self.color.to_hash());
93 }
94
95 components.join(" ")
96 }
97}
98
99impl crate::codegen::format::FormatAsRustCode for StyleBoxShadow {
101 fn format_as_rust_code(&self, tabs: usize) -> String {
102 let t = String::from(" ").repeat(tabs);
103 format!(
104 "StyleBoxShadow {{\r\n{} offset_x: {},\r\n{} offset_y: {},\r\n{} color: \
105 {},\r\n{} blur_radius: {},\r\n{} spread_radius: {},\r\n{} clip_mode: \
106 BoxShadowClipMode::{:?},\r\n{}}}",
107 t,
108 crate::codegen::format::format_pixel_value_no_percent(&self.offset_x),
109 t,
110 crate::codegen::format::format_pixel_value_no_percent(&self.offset_y),
111 t,
112 crate::codegen::format::format_color_value(&self.color),
113 t,
114 crate::codegen::format::format_pixel_value_no_percent(&self.blur_radius),
115 t,
116 crate::codegen::format::format_pixel_value_no_percent(&self.spread_radius),
117 t,
118 self.clip_mode,
119 t
120 )
121 }
122}
123
124#[derive(Clone, PartialEq)]
128pub enum CssShadowParseError<'a> {
129 TooManyOrTooFewComponents(&'a str),
130 ValueParseErr(CssPixelValueParseError<'a>),
131 ColorParseError(CssColorParseError<'a>),
132}
133
134impl_debug_as_display!(CssShadowParseError<'a>);
135impl_display! { CssShadowParseError<'a>, {
136 TooManyOrTooFewComponents(e) => format!("Expected 2 to 4 length values for box-shadow, found an invalid number of components in: \"{}\"", e),
137 ValueParseErr(e) => format!("Invalid length value in box-shadow: {}", e),
138 ColorParseError(e) => format!("Invalid color value in box-shadow: {}", e),
139}}
140
141impl_from!(
142 CssPixelValueParseError<'a>,
143 CssShadowParseError::ValueParseErr
144);
145impl_from!(CssColorParseError<'a>, CssShadowParseError::ColorParseError);
146
147#[derive(Debug, Clone, PartialEq)]
149#[repr(C, u8)]
150pub enum CssShadowParseErrorOwned {
151 TooManyOrTooFewComponents(AzString),
152 ValueParseErr(CssPixelValueParseErrorOwned),
153 ColorParseError(CssColorParseErrorOwned),
154}
155
156impl CssShadowParseError<'_> {
157 #[must_use] pub fn to_contained(&self) -> CssShadowParseErrorOwned {
159 match self {
160 CssShadowParseError::TooManyOrTooFewComponents(s) => {
161 CssShadowParseErrorOwned::TooManyOrTooFewComponents((*s).to_string().into())
162 }
163 CssShadowParseError::ValueParseErr(e) => {
164 CssShadowParseErrorOwned::ValueParseErr(e.to_contained())
165 }
166 CssShadowParseError::ColorParseError(e) => {
167 CssShadowParseErrorOwned::ColorParseError(e.to_contained())
168 }
169 }
170 }
171}
172
173impl CssShadowParseErrorOwned {
174 #[must_use] pub fn to_shared(&self) -> CssShadowParseError<'_> {
176 match self {
177 Self::TooManyOrTooFewComponents(s) => {
178 CssShadowParseError::TooManyOrTooFewComponents(s.as_str())
179 }
180 Self::ValueParseErr(e) => {
181 CssShadowParseError::ValueParseErr(e.to_shared())
182 }
183 Self::ColorParseError(e) => {
184 CssShadowParseError::ColorParseError(e.to_shared())
185 }
186 }
187 }
188}
189
190#[cfg(feature = "parser")]
196pub fn parse_style_box_shadow(
200 input: &str,
201) -> Result<StyleBoxShadow, CssShadowParseError<'_>> {
202 let mut parts: Vec<&str> = input.split_whitespace().collect();
203 let mut shadow = StyleBoxShadow::default();
204
205 if let Some(pos) = parts.iter().position(|&p| p == "inset") {
207 shadow.clip_mode = BoxShadowClipMode::Inset;
208 parts.remove(pos);
209 }
210
211 if let Some((pos, color)) = parts
215 .iter()
216 .enumerate()
217 .rev()
218 .find_map(|(i, p)| parse_css_color(p).ok().map(|c| (i, c)))
219 {
220 shadow.color = color;
221 parts.remove(pos);
222 }
223
224 match parts.len() {
226 2..=4 => {
227 shadow.offset_x = parse_pixel_value_no_percent(parts[0])?;
228 shadow.offset_y = parse_pixel_value_no_percent(parts[1])?;
229 if parts.len() > 2 {
230 shadow.blur_radius = parse_pixel_value_no_percent(parts[2])?;
231 }
232 if parts.len() > 3 {
233 shadow.spread_radius = parse_pixel_value_no_percent(parts[3])?;
234 }
235 }
236 _ => return Err(CssShadowParseError::TooManyOrTooFewComponents(input)),
237 }
238
239 Ok(shadow)
240}
241
242#[cfg(all(test, feature = "parser"))]
243mod tests {
244 use super::*;
245 use crate::props::basic::pixel::PixelValue;
246
247 fn px_no_percent(val: f32) -> PixelValueNoPercent {
248 PixelValueNoPercent {
249 inner: PixelValue::px(val),
250 }
251 }
252
253 #[test]
254 fn test_parse_box_shadow_simple() {
255 let result = parse_style_box_shadow("10px 5px").unwrap();
256 assert_eq!(result.offset_x, px_no_percent(10.0));
257 assert_eq!(result.offset_y, px_no_percent(5.0));
258 assert_eq!(result.blur_radius, px_no_percent(0.0));
259 assert_eq!(result.spread_radius, px_no_percent(0.0));
260 assert_eq!(result.color, ColorU::BLACK);
261 assert_eq!(result.clip_mode, BoxShadowClipMode::Outset);
262 }
263
264 #[test]
265 fn test_parse_box_shadow_with_color() {
266 let result = parse_style_box_shadow("10px 5px #888").unwrap();
267 assert_eq!(result.offset_x, px_no_percent(10.0));
268 assert_eq!(result.offset_y, px_no_percent(5.0));
269 assert_eq!(result.color, ColorU::new_rgb(0x88, 0x88, 0x88));
270 }
271
272 #[test]
273 fn test_parse_box_shadow_with_blur() {
274 let result = parse_style_box_shadow("5px 10px 20px").unwrap();
275 assert_eq!(result.offset_x, px_no_percent(5.0));
276 assert_eq!(result.offset_y, px_no_percent(10.0));
277 assert_eq!(result.blur_radius, px_no_percent(20.0));
278 }
279
280 #[test]
281 fn test_parse_box_shadow_with_spread() {
282 let result = parse_style_box_shadow("2px 2px 2px 1px rgba(0,0,0,0.2)").unwrap();
283 assert_eq!(result.offset_x, px_no_percent(2.0));
284 assert_eq!(result.offset_y, px_no_percent(2.0));
285 assert_eq!(result.blur_radius, px_no_percent(2.0));
286 assert_eq!(result.spread_radius, px_no_percent(1.0));
287 assert_eq!(result.color, ColorU::new(0, 0, 0, 51));
288 }
289
290 #[test]
291 fn test_parse_box_shadow_inset() {
292 let result = parse_style_box_shadow("inset 0 0 10px #000").unwrap();
293 assert_eq!(result.clip_mode, BoxShadowClipMode::Inset);
294 assert_eq!(result.offset_x, px_no_percent(0.0));
295 assert_eq!(result.offset_y, px_no_percent(0.0));
296 assert_eq!(result.blur_radius, px_no_percent(10.0));
297 assert_eq!(result.color, ColorU::BLACK);
298 }
299
300 #[test]
301 fn test_parse_box_shadow_mixed_order() {
302 let result = parse_style_box_shadow("5px 1em red inset").unwrap();
303 assert_eq!(result.clip_mode, BoxShadowClipMode::Inset);
304 assert_eq!(result.offset_x, px_no_percent(5.0));
305 assert_eq!(
306 result.offset_y,
307 PixelValueNoPercent {
308 inner: PixelValue::em(1.0)
309 }
310 );
311 assert_eq!(result.color, ColorU::RED);
312 }
313
314 #[test]
315 fn test_parse_box_shadow_invalid() {
316 assert!(parse_style_box_shadow("10px").is_err());
317 assert!(parse_style_box_shadow("10px 5px 4px 3px 2px").is_err());
318 assert!(parse_style_box_shadow("10px 5px red blue").is_err());
321 assert!(parse_style_box_shadow("10% 5px").is_err()); }
323}
324
325#[cfg(all(test, feature = "parser"))]
326mod autotest_generated {
327 use super::*;
328 use crate::{
329 codegen::format::FormatAsRustCode,
330 props::basic::{
331 pixel::{CssPixelValueParseError, PixelValue},
332 SizeMetric,
333 },
334 };
335
336 fn px(val: f32) -> PixelValueNoPercent {
337 PixelValueNoPercent {
338 inner: PixelValue::px(val),
339 }
340 }
341
342 const fn shadow(
343 offset_x: PixelValueNoPercent,
344 offset_y: PixelValueNoPercent,
345 blur_radius: PixelValueNoPercent,
346 spread_radius: PixelValueNoPercent,
347 clip_mode: BoxShadowClipMode,
348 color: ColorU,
349 ) -> StyleBoxShadow {
350 StyleBoxShadow {
351 offset_x,
352 offset_y,
353 blur_radius,
354 spread_radius,
355 clip_mode,
356 color,
357 }
358 }
359
360 fn numbers(s: &StyleBoxShadow) -> [f32; 4] {
362 [
363 s.offset_x.inner.number.get(),
364 s.offset_y.inner.number.get(),
365 s.blur_radius.inner.number.get(),
366 s.spread_radius.inner.number.get(),
367 ]
368 }
369
370 fn round_trip_corpus() -> Vec<StyleBoxShadow> {
373 vec![
374 StyleBoxShadow::default(),
375 shadow(
376 px(1.0),
377 px(2.0),
378 px(0.0),
379 px(0.0),
380 BoxShadowClipMode::Outset,
381 ColorU::BLACK,
382 ),
383 shadow(
385 px(1.0),
386 px(2.0),
387 px(3.0),
388 px(0.0),
389 BoxShadowClipMode::Outset,
390 ColorU::BLACK,
391 ),
392 shadow(
394 px(1.0),
395 px(2.0),
396 px(0.0),
397 px(4.0),
398 BoxShadowClipMode::Outset,
399 ColorU::BLACK,
400 ),
401 shadow(
403 px(-5.5),
404 px(-7.25),
405 px(2.5),
406 px(1.125),
407 BoxShadowClipMode::Inset,
408 ColorU::RED,
409 ),
410 shadow(
412 PixelValueNoPercent {
413 inner: PixelValue::em(1.5),
414 },
415 PixelValueNoPercent {
416 inner: PixelValue::pt(2.0),
417 },
418 px(0.0),
419 px(0.0),
420 BoxShadowClipMode::Inset,
421 ColorU::BLACK,
422 ),
423 shadow(
425 px(0.0),
426 px(0.0),
427 px(0.0),
428 px(0.0),
429 BoxShadowClipMode::Outset,
430 ColorU::new(0, 0, 0, 0),
431 ),
432 shadow(
434 px(0.0),
435 px(0.0),
436 px(9.0),
437 px(0.0),
438 BoxShadowClipMode::Inset,
439 ColorU::new(1, 2, 3, 4),
440 ),
441 ]
442 }
443
444 #[test]
449 fn clip_mode_display_outset_is_empty_inset_is_keyword() {
450 assert_eq!(BoxShadowClipMode::Outset.to_string(), "");
452 assert_eq!(BoxShadowClipMode::Inset.to_string(), "inset");
453 }
454
455 #[test]
456 fn clip_mode_display_default_does_not_panic() {
457 let default: BoxShadowClipMode = Default::default();
458 assert_eq!(default, BoxShadowClipMode::Outset);
459 assert_eq!(default.to_string(), "");
460 assert_eq!(format!("{:?}", BoxShadowClipMode::Outset), "Outset");
462 assert_eq!(format!("{:?}", BoxShadowClipMode::Inset), "Inset");
463 }
464
465 #[test]
466 fn clip_mode_display_with_format_flags_does_not_panic() {
467 assert!(format!("{:>16}", BoxShadowClipMode::Inset).contains("inset"));
471 assert!(format!("{:*<16}", BoxShadowClipMode::Inset).contains("inset"));
472 assert!(format!("{:.2}", BoxShadowClipMode::Inset).contains("inset"));
473 assert_eq!(format!("{:>16}", BoxShadowClipMode::Outset), "");
475 }
476
477 #[test]
478 fn clip_mode_display_is_repeatable_and_ordered() {
479 for _ in 0..4 {
481 assert_eq!(BoxShadowClipMode::Inset.to_string(), "inset");
482 }
483 assert!(BoxShadowClipMode::Outset < BoxShadowClipMode::Inset);
485 }
486
487 fn scaled(mut s: StyleBoxShadow, factor: f32) -> StyleBoxShadow {
492 s.scale_for_dpi(factor);
493 s
494 }
495
496 fn all_ones() -> StyleBoxShadow {
497 shadow(
498 px(1.0),
499 px(2.0),
500 px(4.0),
501 px(8.0),
502 BoxShadowClipMode::Inset,
503 ColorU::RED,
504 )
505 }
506
507 #[test]
508 fn scale_for_dpi_zero_zeroes_every_length() {
509 let s = scaled(all_ones(), 0.0);
510 assert_eq!(numbers(&s), [0.0, 0.0, 0.0, 0.0]);
511 let neg = scaled(all_ones(), -0.0);
513 assert_eq!(numbers(&neg), [0.0, 0.0, 0.0, 0.0]);
514 assert_eq!(neg.offset_x.inner.number.number(), 0);
515 }
516
517 #[test]
518 fn scale_for_dpi_identity_and_double() {
519 let s = scaled(all_ones(), 1.0);
520 assert_eq!(numbers(&s), [1.0, 2.0, 4.0, 8.0]);
521 let d = scaled(all_ones(), 2.0);
522 assert_eq!(numbers(&d), [2.0, 4.0, 8.0, 16.0]);
523 let h = scaled(all_ones(), 0.5);
524 assert_eq!(numbers(&h), [0.5, 1.0, 2.0, 4.0]);
525 }
526
527 #[test]
528 fn scale_for_dpi_negative_flips_sign_deterministically() {
529 let s = scaled(all_ones(), -1.5);
530 assert_eq!(numbers(&s), [-1.5, -3.0, -6.0, -12.0]);
531 }
532
533 #[test]
534 fn scale_for_dpi_nan_yields_zero_never_nan() {
535 let s = scaled(all_ones(), f32::NAN);
538 for n in numbers(&s) {
539 assert!(!n.is_nan(), "NaN leaked into the fixed-point encoding");
540 assert_eq!(n, 0.0);
541 }
542 }
543
544 #[test]
545 fn scale_for_dpi_infinity_saturates_to_finite() {
546 let pos = scaled(all_ones(), f32::INFINITY);
547 for n in numbers(&pos) {
548 assert!(n.is_finite(), "+inf scale produced a non-finite value");
549 assert!(n > 0.0);
550 }
551
552 let neg = scaled(all_ones(), f32::NEG_INFINITY);
553 for n in numbers(&neg) {
554 assert!(n.is_finite(), "-inf scale produced a non-finite value");
555 assert!(n < 0.0);
556 }
557 }
558
559 #[test]
560 fn scale_for_dpi_float_extremes_do_not_panic() {
561 for n in numbers(&scaled(all_ones(), f32::MAX)) {
563 assert!(n.is_finite());
564 assert!(n > 0.0);
565 }
566 for n in numbers(&scaled(all_ones(), f32::MIN)) {
567 assert!(n.is_finite());
568 assert!(n < 0.0);
569 }
570 assert_eq!(numbers(&scaled(all_ones(), f32::MIN_POSITIVE)), [0.0; 4]);
572 assert_eq!(numbers(&scaled(all_ones(), f32::EPSILON)), [0.0; 4]);
573 }
574
575 #[test]
576 fn scale_for_dpi_saturation_is_stable_under_repetition() {
577 let mut s = all_ones();
580 for _ in 0..8 {
581 s.scale_for_dpi(1e30);
582 for n in numbers(&s) {
583 assert!(n.is_finite());
584 assert!(n > 0.0, "saturating scale wrapped to a negative value");
585 }
586 }
587 }
588
589 #[test]
590 fn scale_for_dpi_quantizes_below_the_fixed_point_precision() {
591 let s = scaled(all_ones(), 0.0001);
593 assert_eq!(numbers(&s), [0.0, 0.0, 0.0, 0.0]);
594 }
595
596 #[test]
597 fn scale_for_dpi_on_default_is_a_noop() {
598 for factor in [0.0, 1.0, 2.0, -3.0, f32::NAN, f32::INFINITY, f32::MAX] {
599 let s = scaled(StyleBoxShadow::default(), factor);
600 assert_eq!(
601 numbers(&s),
602 [0.0, 0.0, 0.0, 0.0],
603 "default shadow changed under scale {factor}"
604 );
605 }
606 }
607
608 #[test]
609 fn scale_for_dpi_preserves_metric_color_and_clip_mode() {
610 let mut s = shadow(
611 PixelValueNoPercent {
612 inner: PixelValue::em(2.0),
613 },
614 PixelValueNoPercent {
615 inner: PixelValue::pt(3.0),
616 },
617 PixelValueNoPercent {
618 inner: PixelValue::rem(4.0),
619 },
620 px(5.0),
621 BoxShadowClipMode::Inset,
622 ColorU::new(1, 2, 3, 4),
623 );
624 s.scale_for_dpi(2.0);
625
626 assert_eq!(s.offset_x.inner.metric, SizeMetric::Em);
628 assert_eq!(s.offset_y.inner.metric, SizeMetric::Pt);
629 assert_eq!(s.blur_radius.inner.metric, SizeMetric::Rem);
630 assert_eq!(s.spread_radius.inner.metric, SizeMetric::Px);
631 assert_eq!(numbers(&s), [4.0, 6.0, 8.0, 10.0]);
632
633 assert_eq!(s.clip_mode, BoxShadowClipMode::Inset);
635 assert_eq!(s.color, ColorU::new(1, 2, 3, 4));
636 }
637
638 fn error_corpus() -> Vec<CssShadowParseError<'static>> {
644 vec![
645 CssShadowParseError::TooManyOrTooFewComponents("1px"),
646 CssShadowParseError::TooManyOrTooFewComponents(""),
647 CssShadowParseError::TooManyOrTooFewComponents("\u{1F600}\u{0301} \u{202E}"),
648 CssShadowParseError::ValueParseErr(CssPixelValueParseError::EmptyString),
649 CssShadowParseError::ValueParseErr(CssPixelValueParseError::InvalidPixelValue("abc")),
650 CssShadowParseError::ValueParseErr(CssPixelValueParseError::NoValueGiven(
651 "px",
652 SizeMetric::Px,
653 )),
654 CssShadowParseError::ValueParseErr(CssPixelValueParseError::ValueParseErr(
655 "x".parse::<f32>().unwrap_err(),
656 "x",
657 )),
658 CssShadowParseError::ValueParseErr(CssPixelValueParseError::ValueParseErr(
659 "".parse::<f32>().unwrap_err(),
660 "",
661 )),
662 CssShadowParseError::ColorParseError(parse_css_color("notacolor").unwrap_err()),
663 CssShadowParseError::ColorParseError(parse_css_color("#gg").unwrap_err()),
664 CssShadowParseError::ColorParseError(parse_css_color("rgb(1,2").unwrap_err()),
665 ]
666 }
667
668 #[test]
669 fn shadow_error_to_contained_to_shared_is_lossless() {
670 for e in error_corpus() {
671 let owned = e.to_contained();
672 assert_eq!(owned.to_shared(), e, "borrow -> own -> borrow lost data");
673 }
674 }
675
676 #[test]
677 fn shadow_error_owned_to_shared_to_contained_is_lossless() {
678 for e in error_corpus() {
679 let owned = e.to_contained();
680 assert_eq!(
681 owned.to_shared().to_contained(),
682 owned,
683 "own -> borrow -> own lost data"
684 );
685 }
686 }
687
688 #[test]
689 fn shadow_error_to_contained_preserves_the_input_string_verbatim() {
690 for input in [
691 "",
692 " ",
693 "\u{1F600}",
694 "e\u{0301}\u{0301}\u{0301}",
695 "10px 5px 4px 3px 2px",
696 ] {
697 let owned = CssShadowParseError::TooManyOrTooFewComponents(input).to_contained();
698 match owned {
699 CssShadowParseErrorOwned::TooManyOrTooFewComponents(s) => {
700 assert_eq!(s.as_str(), input);
701 }
702 other => panic!("wrong variant: {other:?}"),
703 }
704 }
705 }
706
707 #[test]
708 fn shadow_error_to_contained_survives_a_very_long_input() {
709 let long = "x".repeat(200_000);
710 let owned = CssShadowParseError::TooManyOrTooFewComponents(&long).to_contained();
711 match owned.to_shared() {
712 CssShadowParseError::TooManyOrTooFewComponents(s) => {
713 assert_eq!(s.len(), 200_000);
714 }
715 other => panic!("wrong variant: {other:?}"),
716 }
717 }
718
719 #[test]
720 fn shadow_error_display_and_debug_are_non_empty() {
721 for e in error_corpus() {
722 assert!(!e.to_string().is_empty(), "empty Display for {e:?}");
723 assert_eq!(format!("{e:?}"), e.to_string());
725 assert!(!format!("{:?}", e.to_contained()).is_empty());
726 }
727 }
728
729 #[test]
730 fn shadow_error_to_contained_round_trips_real_parser_errors() {
731 for bad in ["", "1px", "abc 1px", "10% 5px", "1 2 3 4 5"] {
733 let e = parse_style_box_shadow(bad).unwrap_err();
734 assert_eq!(e.to_contained().to_shared(), e);
735 }
736 }
737
738 #[test]
743 fn parse_valid_minimal_positive_control() {
744 let s = parse_style_box_shadow("10px 5px").unwrap();
745 assert_eq!(
746 s,
747 shadow(
748 px(10.0),
749 px(5.0),
750 px(0.0),
751 px(0.0),
752 BoxShadowClipMode::Outset,
753 ColorU::BLACK,
754 )
755 );
756 }
757
758 #[test]
759 fn parse_empty_and_whitespace_only_input_is_err() {
760 for input in ["", " ", " ", "\t", "\n", "\t\n\r ", "\u{00a0}", "\u{3000}"] {
761 let e = parse_style_box_shadow(input).unwrap_err();
762 assert_eq!(
763 e,
764 CssShadowParseError::TooManyOrTooFewComponents(input),
765 "unexpected error for {input:?}"
766 );
767 }
768 }
769
770 #[test]
771 fn parse_garbage_is_err_and_never_panics() {
772 for input in [
773 "!!!",
774 "@#$%^&*",
775 "; drop table",
776 "{}{}{}",
777 "\\\\\\",
778 "\0\0",
779 "1px", "1px 2px 3px 4px 5px", "inset", "red", "inset red", "inset red 1px", "1px 2px red blue", "inset inset 1px 2px", "10% 5px", "1px 10%",
789 "1px 2px 3%",
790 "px px",
791 "in in",
792 "1px 2px 3px 4px 5px red inset",
793 ] {
794 assert!(
795 parse_style_box_shadow(input).is_err(),
796 "expected Err for {input:?}"
797 );
798 }
799 }
800
801 #[test]
802 fn parse_leading_trailing_junk_is_trimmed_or_rejected_deterministically() {
803 let padded = parse_style_box_shadow(" 10px 5px ").unwrap();
805 assert_eq!(padded, parse_style_box_shadow("10px 5px").unwrap());
806
807 for input in ["10px 5px;", "10px, 5px", "10px 5px !important", "10px 5px}"] {
809 let e = parse_style_box_shadow(input).unwrap_err();
810 assert!(
811 matches!(e, CssShadowParseError::ValueParseErr(_))
812 || matches!(e, CssShadowParseError::TooManyOrTooFewComponents(_)),
813 "unexpected error kind for {input:?}: {e:?}"
814 );
815 }
816 }
817
818 #[test]
819 fn parse_component_count_boundaries() {
820 assert!(parse_style_box_shadow("1px").is_err()); assert!(parse_style_box_shadow("1px 2px").is_ok()); assert!(parse_style_box_shadow("1px 2px 3px").is_ok()); assert!(parse_style_box_shadow("1px 2px 3px 4px").is_ok()); assert!(parse_style_box_shadow("1px 2px 3px 4px 5px").is_err()); assert!(parse_style_box_shadow("inset red 1px 2px 3px 4px").is_ok());
828 assert!(parse_style_box_shadow("inset red 1px 2px 3px 4px 5px").is_err());
830 }
831
832 #[test]
833 fn parse_is_insensitive_to_keyword_and_color_position() {
834 let expected = shadow(
835 px(1.0),
836 px(2.0),
837 px(0.0),
838 px(0.0),
839 BoxShadowClipMode::Inset,
840 ColorU::RED,
841 );
842 for input in [
843 "inset red 1px 2px",
844 "red inset 1px 2px",
845 "1px 2px red inset",
846 "1px 2px inset red",
847 "red 1px 2px inset",
848 "1px red 2px inset",
849 "inset 1px red 2px",
850 ] {
851 assert_eq!(
852 parse_style_box_shadow(input).unwrap(),
853 expected,
854 "position of inset/color changed the result for {input:?}"
855 );
856 }
857 }
858
859 #[test]
860 fn parse_accepts_every_color_syntax() {
861 for (input, expected) in [
862 ("1px 2px #888", ColorU::new_rgb(0x88, 0x88, 0x88)),
863 ("1px 2px #ff0000", ColorU::RED),
864 ("1px 2px #ff0000ff", ColorU::RED),
865 ("1px 2px #f00f", ColorU::RED),
866 ("1px 2px rgb(255,0,0)", ColorU::RED),
867 ("1px 2px rgba(255,0,0,1.0)", ColorU::RED),
868 ("1px 2px red", ColorU::RED),
869 ("1px 2px RED", ColorU::RED),
870 ("1px 2px transparent", ColorU::new(0, 0, 0, 0)),
871 ] {
872 let s = parse_style_box_shadow(input).unwrap();
873 assert_eq!(s.color, expected, "wrong color for {input:?}");
874 assert_eq!(s.offset_x, px(1.0));
875 assert_eq!(s.offset_y, px(2.0));
876 }
877 }
878
879 #[test]
880 fn parse_bare_zero_is_a_pixel_value_not_a_color() {
881 let s = parse_style_box_shadow("0 0").unwrap();
882 assert_eq!(s.offset_x, px(0.0));
883 assert_eq!(s.offset_y, px(0.0));
884 assert_eq!(s.offset_x.inner.metric, SizeMetric::Px);
885 assert_eq!(s.color, ColorU::BLACK, "a bare 0 was eaten by the color parser");
886 }
887
888 #[test]
889 fn parse_boundary_numbers_are_finite_and_defined() {
890 let zeroes = parse_style_box_shadow("-0 -0").unwrap();
892 assert_eq!(numbers(&zeroes)[0], 0.0);
893 assert_eq!(numbers(&zeroes)[1], 0.0);
894 assert!(!numbers(&zeroes)[0].is_sign_negative());
895
896 for input in [
898 "1e30px 1e30px",
899 "9223372036854775807px 9223372036854775807px", "340282350000000000000000000000000000000px 1px", ] {
902 let s = parse_style_box_shadow(input).unwrap();
903 let n = numbers(&s)[0];
904 assert!(n.is_finite(), "{input:?} produced a non-finite offset");
905 assert!(n > 0.0, "{input:?} saturated to the wrong sign");
906 }
907 let neg = parse_style_box_shadow("-1e30px 1px").unwrap();
908 assert!(numbers(&neg)[0].is_finite() && numbers(&neg)[0] < 0.0);
909
910 let tiny = parse_style_box_shadow("1e-30px 0.00001px").unwrap();
912 assert_eq!(numbers(&tiny)[0], 0.0);
913 assert_eq!(numbers(&tiny)[1], 0.0);
914 }
915
916 #[test]
917 fn parse_nan_and_infinity_tokens_are_defined_not_poisonous() {
918 let nan = parse_style_box_shadow("NaN NaN").unwrap();
921 for n in numbers(&nan) {
922 assert!(!n.is_nan(), "NaN survived into a parsed shadow");
923 assert_eq!(n, 0.0);
924 }
925 let nan_px = parse_style_box_shadow("NaNpx 1px").unwrap();
926 assert_eq!(numbers(&nan_px)[0], 0.0);
927
928 let inf = parse_style_box_shadow("inf 1px").unwrap();
930 assert!(numbers(&inf)[0].is_finite() && numbers(&inf)[0] > 0.0);
931 let neg_inf = parse_style_box_shadow("-inf 1px").unwrap();
932 assert!(numbers(&neg_inf)[0].is_finite() && numbers(&neg_inf)[0] < 0.0);
933 let infinity = parse_style_box_shadow("1px infinity").unwrap();
934 assert!(numbers(&infinity)[1].is_finite());
935
936 assert!(parse_style_box_shadow("in 1px").is_err());
938 }
939
940 #[test]
941 fn parse_never_yields_a_percent_metric() {
942 for input in [
943 "1px 2px",
944 "0 0",
945 "1em 2rem 3pt 4in",
946 "5vw 5vh 5vmin 5vmax",
947 "1cm 2mm",
948 "inset 1px 2px red",
949 ] {
950 let s = parse_style_box_shadow(input).unwrap();
951 for m in [
952 s.offset_x.inner.metric,
953 s.offset_y.inner.metric,
954 s.blur_radius.inner.metric,
955 s.spread_radius.inner.metric,
956 ] {
957 assert_ne!(m, SizeMetric::Percent, "percent metric leaked from {input:?}");
958 }
959 }
960 }
961
962 #[test]
963 fn parse_unicode_input_does_not_panic() {
964 for input in [
967 "\u{1F600}",
968 "\u{1F600} \u{1F600}",
969 "1px 2px \u{1F600}",
970 "1px 2px #\u{1F600}", "1px 2px #\u{e9}1", "1px 2px #\u{e9}\u{e9}\u{e9}\u{e9}", "e\u{0301}\u{0301} 1px",
974 "\u{202E}1px 2px",
975 "\u{0661}px \u{0662}px", "1px 2px", "1px 2px \u{fffd}",
978 ] {
979 let _ = parse_style_box_shadow(input); }
981
982 let nbsp = parse_style_box_shadow("10px\u{00a0}5px").unwrap();
984 assert_eq!(nbsp, parse_style_box_shadow("10px 5px").unwrap());
985 }
986
987 #[test]
988 fn parse_extremely_long_input_terminates() {
989 let many = "1px ".repeat(50_000);
991 assert!(matches!(
992 parse_style_box_shadow(&many),
993 Err(CssShadowParseError::TooManyOrTooFewComponents(_))
994 ));
995
996 let long_garbage = "a".repeat(1_000_000);
998 assert!(parse_style_box_shadow(&long_garbage).is_err());
999
1000 let huge = format!("{}px 1px", "9".repeat(50_000));
1002 let s = parse_style_box_shadow(&huge).unwrap();
1003 assert!(numbers(&s)[0].is_finite());
1004 assert!(numbers(&s)[0] > 0.0);
1005
1006 let padded = format!("{}1px 2px{}", " ".repeat(100_000), " ".repeat(100_000));
1008 assert_eq!(
1009 parse_style_box_shadow(&padded).unwrap(),
1010 parse_style_box_shadow("1px 2px").unwrap()
1011 );
1012 }
1013
1014 #[test]
1015 fn parse_deeply_nested_input_does_not_stack_overflow() {
1016 let open = "(".repeat(10_000);
1017 let nested = format!("{}{}", open, ")".repeat(10_000));
1018 assert!(parse_style_box_shadow(&nested).is_err());
1019 assert!(parse_style_box_shadow(&open).is_err());
1020 assert!(parse_style_box_shadow(&format!("1px 2px rgb{nested}")).is_err());
1021 assert!(parse_style_box_shadow(&format!("1px 2px {}", "[".repeat(10_000))).is_err());
1022 }
1023
1024 #[test]
1029 fn print_then_parse_is_the_identity() {
1030 for original in round_trip_corpus() {
1031 let printed = original.print_as_css_value();
1032 let reparsed = parse_style_box_shadow(&printed)
1033 .unwrap_or_else(|e| panic!("printed {printed:?} does not re-parse: {e:?}"));
1034 assert_eq!(reparsed, original, "round-trip changed {printed:?}");
1035 }
1036 }
1037
1038 #[test]
1039 fn parse_then_print_then_parse_is_idempotent() {
1040 for input in [
1041 "10px 5px",
1042 "5px 10px 20px",
1043 "2px 2px 2px 1px rgba(0,0,0,0.2)",
1044 "inset 0 0 10px #000",
1045 "5px 1em red inset",
1046 "1px 2px transparent",
1047 "-3px -4px 0 2px #12345678",
1048 ] {
1049 let first = parse_style_box_shadow(input).unwrap();
1050 let printed = first.print_as_css_value();
1051 let second = parse_style_box_shadow(&printed).unwrap();
1052 assert_eq!(first, second, "{input:?} -> {printed:?} was not idempotent");
1053 assert_eq!(printed, second.print_as_css_value());
1055 }
1056 }
1057
1058 #[test]
1059 fn print_omits_defaults_for_brevity() {
1060 let default = StyleBoxShadow::default();
1061 assert_eq!(default.print_as_css_value(), "0px 0px");
1062
1063 let black = shadow(
1065 px(1.0),
1066 px(2.0),
1067 px(3.0),
1068 px(0.0),
1069 BoxShadowClipMode::Outset,
1070 ColorU::BLACK,
1071 );
1072 assert_eq!(black.print_as_css_value(), "1px 2px 3px");
1073 assert!(!black.print_as_css_value().contains('#'));
1074
1075 assert!(!black.print_as_css_value().contains("inset"));
1077 }
1078
1079 #[test]
1080 fn print_writes_inset_first_and_color_last() {
1081 let s = shadow(
1082 px(1.0),
1083 px(2.0),
1084 px(0.0),
1085 px(0.0),
1086 BoxShadowClipMode::Inset,
1087 ColorU::RED,
1088 );
1089 assert_eq!(s.print_as_css_value(), "inset 1px 2px #ff0000ff");
1090 }
1091
1092 #[test]
1093 fn print_emits_a_placeholder_blur_when_only_the_spread_is_set() {
1094 let s = shadow(
1097 px(1.0),
1098 px(2.0),
1099 px(0.0),
1100 px(4.0),
1101 BoxShadowClipMode::Outset,
1102 ColorU::BLACK,
1103 );
1104 assert_eq!(s.print_as_css_value(), "1px 2px 0px 4px");
1105 let reparsed = parse_style_box_shadow(&s.print_as_css_value()).unwrap();
1106 assert_eq!(reparsed.blur_radius, px(0.0));
1107 assert_eq!(reparsed.spread_radius, px(4.0));
1108 }
1109
1110 #[test]
1111 fn print_of_extreme_values_does_not_panic() {
1112 for factor in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX, 1e30] {
1114 let s = scaled(all_ones(), factor);
1115 let printed = s.print_as_css_value();
1116 assert!(!printed.is_empty());
1117 assert!(!printed.contains("NaN"), "NaN reached the CSS output: {printed}");
1118 assert!(!printed.contains("inf"), "inf reached the CSS output: {printed}");
1119 }
1120 }
1121
1122 #[test]
1123 fn format_as_rust_code_is_well_formed_for_extremes() {
1124 for s in round_trip_corpus() {
1125 let code = s.format_as_rust_code(0);
1126 assert!(code.starts_with("StyleBoxShadow {"));
1127 assert!(code.contains("offset_x:"));
1128 assert!(code.contains("offset_y:"));
1129 assert!(code.contains("blur_radius:"));
1130 assert!(code.contains("spread_radius:"));
1131 assert!(code.contains("clip_mode: BoxShadowClipMode::"));
1132 assert!(code.contains("color:"));
1133 }
1134 let inset = scaled(all_ones(), f32::INFINITY);
1136 let code = inset.format_as_rust_code(3);
1137 assert!(code.contains("clip_mode: BoxShadowClipMode::Inset"));
1138 assert!(code.contains(" offset_x:"));
1139 }
1140
1141 #[test]
1146 fn default_shadow_is_an_opaque_black_outset_at_the_origin() {
1147 let d = StyleBoxShadow::default();
1148 assert_eq!(d.clip_mode, BoxShadowClipMode::Outset);
1149 assert_eq!(d.color, ColorU::BLACK);
1150 assert_eq!(numbers(&d), [0.0, 0.0, 0.0, 0.0]);
1151 assert_eq!(d.offset_x.inner.metric, SizeMetric::Px);
1152 assert_eq!(parse_style_box_shadow("0 0").unwrap(), d);
1154 }
1155
1156 #[test]
1157 fn equality_is_component_wise() {
1158 let base = all_ones();
1159 assert_eq!(base, base);
1160 assert_ne!(base, StyleBoxShadow::default());
1161
1162 let mut clip = base;
1163 clip.clip_mode = BoxShadowClipMode::Outset;
1164 assert_ne!(base, clip, "clip_mode is ignored by PartialEq");
1165
1166 let mut color = base;
1167 color.color = ColorU::BLACK;
1168 assert_ne!(base, color, "color is ignored by PartialEq");
1169
1170 let mut spread = base;
1171 spread.spread_radius = px(9.0);
1172 assert_ne!(base, spread, "spread_radius is ignored by PartialEq");
1173 }
1174}