1use crate::corety::AzString;
4use alloc::string::{String, ToString};
5use core::fmt;
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]
159 pub fn to_contained(&self) -> CssShadowParseErrorOwned {
160 match self {
161 CssShadowParseError::TooManyOrTooFewComponents(s) => {
162 CssShadowParseErrorOwned::TooManyOrTooFewComponents((*s).to_string().into())
163 }
164 CssShadowParseError::ValueParseErr(e) => {
165 CssShadowParseErrorOwned::ValueParseErr(e.to_contained())
166 }
167 CssShadowParseError::ColorParseError(e) => {
168 CssShadowParseErrorOwned::ColorParseError(e.to_contained())
169 }
170 }
171 }
172}
173
174impl CssShadowParseErrorOwned {
175 #[must_use]
177 pub fn to_shared(&self) -> CssShadowParseError<'_> {
178 match self {
179 Self::TooManyOrTooFewComponents(s) => {
180 CssShadowParseError::TooManyOrTooFewComponents(s.as_str())
181 }
182 Self::ValueParseErr(e) => CssShadowParseError::ValueParseErr(e.to_shared()),
183 Self::ColorParseError(e) => CssShadowParseError::ColorParseError(e.to_shared()),
184 }
185 }
186}
187
188#[cfg(feature = "parser")]
194pub fn parse_style_box_shadow(input: &str) -> Result<StyleBoxShadow, CssShadowParseError<'_>> {
198 let mut parts: Vec<&str> = input.split_whitespace().collect();
199 let mut shadow = StyleBoxShadow::default();
200
201 if let Some(pos) = parts.iter().position(|&p| p == "inset") {
203 shadow.clip_mode = BoxShadowClipMode::Inset;
204 parts.remove(pos);
205 }
206
207 if let Some((pos, color)) = parts
211 .iter()
212 .enumerate()
213 .rev()
214 .find_map(|(i, p)| parse_css_color(p).ok().map(|c| (i, c)))
215 {
216 shadow.color = color;
217 parts.remove(pos);
218 }
219
220 match parts.len() {
222 2..=4 => {
223 shadow.offset_x = parse_pixel_value_no_percent(parts[0])?;
224 shadow.offset_y = parse_pixel_value_no_percent(parts[1])?;
225 if parts.len() > 2 {
226 shadow.blur_radius = parse_pixel_value_no_percent(parts[2])?;
227 }
228 if parts.len() > 3 {
229 shadow.spread_radius = parse_pixel_value_no_percent(parts[3])?;
230 }
231 }
232 _ => return Err(CssShadowParseError::TooManyOrTooFewComponents(input)),
233 }
234
235 Ok(shadow)
236}
237
238#[cfg(all(test, feature = "parser"))]
239mod tests {
240 use super::*;
241 use crate::props::basic::pixel::PixelValue;
242
243 fn px_no_percent(val: f32) -> PixelValueNoPercent {
244 PixelValueNoPercent {
245 inner: PixelValue::px(val),
246 }
247 }
248
249 #[test]
250 fn test_parse_box_shadow_simple() {
251 let result = parse_style_box_shadow("10px 5px").unwrap();
252 assert_eq!(result.offset_x, px_no_percent(10.0));
253 assert_eq!(result.offset_y, px_no_percent(5.0));
254 assert_eq!(result.blur_radius, px_no_percent(0.0));
255 assert_eq!(result.spread_radius, px_no_percent(0.0));
256 assert_eq!(result.color, ColorU::BLACK);
257 assert_eq!(result.clip_mode, BoxShadowClipMode::Outset);
258 }
259
260 #[test]
261 fn test_parse_box_shadow_with_color() {
262 let result = parse_style_box_shadow("10px 5px #888").unwrap();
263 assert_eq!(result.offset_x, px_no_percent(10.0));
264 assert_eq!(result.offset_y, px_no_percent(5.0));
265 assert_eq!(result.color, ColorU::new_rgb(0x88, 0x88, 0x88));
266 }
267
268 #[test]
269 fn test_parse_box_shadow_with_blur() {
270 let result = parse_style_box_shadow("5px 10px 20px").unwrap();
271 assert_eq!(result.offset_x, px_no_percent(5.0));
272 assert_eq!(result.offset_y, px_no_percent(10.0));
273 assert_eq!(result.blur_radius, px_no_percent(20.0));
274 }
275
276 #[test]
277 fn test_parse_box_shadow_with_spread() {
278 let result = parse_style_box_shadow("2px 2px 2px 1px rgba(0,0,0,0.2)").unwrap();
279 assert_eq!(result.offset_x, px_no_percent(2.0));
280 assert_eq!(result.offset_y, px_no_percent(2.0));
281 assert_eq!(result.blur_radius, px_no_percent(2.0));
282 assert_eq!(result.spread_radius, px_no_percent(1.0));
283 assert_eq!(result.color, ColorU::new(0, 0, 0, 51));
284 }
285
286 #[test]
287 fn test_parse_box_shadow_inset() {
288 let result = parse_style_box_shadow("inset 0 0 10px #000").unwrap();
289 assert_eq!(result.clip_mode, BoxShadowClipMode::Inset);
290 assert_eq!(result.offset_x, px_no_percent(0.0));
291 assert_eq!(result.offset_y, px_no_percent(0.0));
292 assert_eq!(result.blur_radius, px_no_percent(10.0));
293 assert_eq!(result.color, ColorU::BLACK);
294 }
295
296 #[test]
297 fn test_parse_box_shadow_mixed_order() {
298 let result = parse_style_box_shadow("5px 1em red inset").unwrap();
299 assert_eq!(result.clip_mode, BoxShadowClipMode::Inset);
300 assert_eq!(result.offset_x, px_no_percent(5.0));
301 assert_eq!(
302 result.offset_y,
303 PixelValueNoPercent {
304 inner: PixelValue::em(1.0)
305 }
306 );
307 assert_eq!(result.color, ColorU::RED);
308 }
309
310 #[test]
311 fn test_parse_box_shadow_invalid() {
312 assert!(parse_style_box_shadow("10px").is_err());
313 assert!(parse_style_box_shadow("10px 5px 4px 3px 2px").is_err());
314 assert!(parse_style_box_shadow("10px 5px red blue").is_err());
317 assert!(parse_style_box_shadow("10% 5px").is_err()); }
319}
320
321#[cfg(all(test, feature = "parser"))]
322mod autotest_generated {
323 use super::*;
324 use crate::{
325 codegen::format::FormatAsRustCode,
326 props::basic::{
327 pixel::{CssPixelValueParseError, PixelValue},
328 SizeMetric,
329 },
330 };
331
332 fn px(val: f32) -> PixelValueNoPercent {
333 PixelValueNoPercent {
334 inner: PixelValue::px(val),
335 }
336 }
337
338 const fn shadow(
339 offset_x: PixelValueNoPercent,
340 offset_y: PixelValueNoPercent,
341 blur_radius: PixelValueNoPercent,
342 spread_radius: PixelValueNoPercent,
343 clip_mode: BoxShadowClipMode,
344 color: ColorU,
345 ) -> StyleBoxShadow {
346 StyleBoxShadow {
347 offset_x,
348 offset_y,
349 blur_radius,
350 spread_radius,
351 clip_mode,
352 color,
353 }
354 }
355
356 fn numbers(s: &StyleBoxShadow) -> [f32; 4] {
358 [
359 s.offset_x.inner.number.get(),
360 s.offset_y.inner.number.get(),
361 s.blur_radius.inner.number.get(),
362 s.spread_radius.inner.number.get(),
363 ]
364 }
365
366 fn round_trip_corpus() -> Vec<StyleBoxShadow> {
369 vec![
370 StyleBoxShadow::default(),
371 shadow(
372 px(1.0),
373 px(2.0),
374 px(0.0),
375 px(0.0),
376 BoxShadowClipMode::Outset,
377 ColorU::BLACK,
378 ),
379 shadow(
381 px(1.0),
382 px(2.0),
383 px(3.0),
384 px(0.0),
385 BoxShadowClipMode::Outset,
386 ColorU::BLACK,
387 ),
388 shadow(
390 px(1.0),
391 px(2.0),
392 px(0.0),
393 px(4.0),
394 BoxShadowClipMode::Outset,
395 ColorU::BLACK,
396 ),
397 shadow(
399 px(-5.5),
400 px(-7.25),
401 px(2.5),
402 px(1.125),
403 BoxShadowClipMode::Inset,
404 ColorU::RED,
405 ),
406 shadow(
408 PixelValueNoPercent {
409 inner: PixelValue::em(1.5),
410 },
411 PixelValueNoPercent {
412 inner: PixelValue::pt(2.0),
413 },
414 px(0.0),
415 px(0.0),
416 BoxShadowClipMode::Inset,
417 ColorU::BLACK,
418 ),
419 shadow(
421 px(0.0),
422 px(0.0),
423 px(0.0),
424 px(0.0),
425 BoxShadowClipMode::Outset,
426 ColorU::new(0, 0, 0, 0),
427 ),
428 shadow(
430 px(0.0),
431 px(0.0),
432 px(9.0),
433 px(0.0),
434 BoxShadowClipMode::Inset,
435 ColorU::new(1, 2, 3, 4),
436 ),
437 ]
438 }
439
440 #[test]
445 fn clip_mode_display_outset_is_empty_inset_is_keyword() {
446 assert_eq!(BoxShadowClipMode::Outset.to_string(), "");
448 assert_eq!(BoxShadowClipMode::Inset.to_string(), "inset");
449 }
450
451 #[test]
452 fn clip_mode_display_default_does_not_panic() {
453 let default: BoxShadowClipMode = Default::default();
454 assert_eq!(default, BoxShadowClipMode::Outset);
455 assert_eq!(default.to_string(), "");
456 assert_eq!(format!("{:?}", BoxShadowClipMode::Outset), "Outset");
458 assert_eq!(format!("{:?}", BoxShadowClipMode::Inset), "Inset");
459 }
460
461 #[test]
462 fn clip_mode_display_with_format_flags_does_not_panic() {
463 assert!(format!("{:>16}", BoxShadowClipMode::Inset).contains("inset"));
467 assert!(format!("{:*<16}", BoxShadowClipMode::Inset).contains("inset"));
468 assert!(format!("{:.2}", BoxShadowClipMode::Inset).contains("inset"));
469 assert_eq!(format!("{:>16}", BoxShadowClipMode::Outset), "");
471 }
472
473 #[test]
474 fn clip_mode_display_is_repeatable_and_ordered() {
475 for _ in 0..4 {
477 assert_eq!(BoxShadowClipMode::Inset.to_string(), "inset");
478 }
479 assert!(BoxShadowClipMode::Outset < BoxShadowClipMode::Inset);
481 }
482
483 fn scaled(mut s: StyleBoxShadow, factor: f32) -> StyleBoxShadow {
488 s.scale_for_dpi(factor);
489 s
490 }
491
492 fn all_ones() -> StyleBoxShadow {
493 shadow(
494 px(1.0),
495 px(2.0),
496 px(4.0),
497 px(8.0),
498 BoxShadowClipMode::Inset,
499 ColorU::RED,
500 )
501 }
502
503 #[test]
504 fn scale_for_dpi_zero_zeroes_every_length() {
505 let s = scaled(all_ones(), 0.0);
506 assert_eq!(numbers(&s), [0.0, 0.0, 0.0, 0.0]);
507 let neg = scaled(all_ones(), -0.0);
509 assert_eq!(numbers(&neg), [0.0, 0.0, 0.0, 0.0]);
510 assert_eq!(neg.offset_x.inner.number.number(), 0);
511 }
512
513 #[test]
514 fn scale_for_dpi_identity_and_double() {
515 let s = scaled(all_ones(), 1.0);
516 assert_eq!(numbers(&s), [1.0, 2.0, 4.0, 8.0]);
517 let d = scaled(all_ones(), 2.0);
518 assert_eq!(numbers(&d), [2.0, 4.0, 8.0, 16.0]);
519 let h = scaled(all_ones(), 0.5);
520 assert_eq!(numbers(&h), [0.5, 1.0, 2.0, 4.0]);
521 }
522
523 #[test]
524 fn scale_for_dpi_negative_flips_sign_deterministically() {
525 let s = scaled(all_ones(), -1.5);
526 assert_eq!(numbers(&s), [-1.5, -3.0, -6.0, -12.0]);
527 }
528
529 #[test]
530 fn scale_for_dpi_nan_yields_zero_never_nan() {
531 let s = scaled(all_ones(), f32::NAN);
534 for n in numbers(&s) {
535 assert!(!n.is_nan(), "NaN leaked into the fixed-point encoding");
536 assert_eq!(n, 0.0);
537 }
538 }
539
540 #[test]
541 fn scale_for_dpi_infinity_saturates_to_finite() {
542 let pos = scaled(all_ones(), f32::INFINITY);
543 for n in numbers(&pos) {
544 assert!(n.is_finite(), "+inf scale produced a non-finite value");
545 assert!(n > 0.0);
546 }
547
548 let neg = scaled(all_ones(), f32::NEG_INFINITY);
549 for n in numbers(&neg) {
550 assert!(n.is_finite(), "-inf scale produced a non-finite value");
551 assert!(n < 0.0);
552 }
553 }
554
555 #[test]
556 fn scale_for_dpi_float_extremes_do_not_panic() {
557 for n in numbers(&scaled(all_ones(), f32::MAX)) {
559 assert!(n.is_finite());
560 assert!(n > 0.0);
561 }
562 for n in numbers(&scaled(all_ones(), f32::MIN)) {
563 assert!(n.is_finite());
564 assert!(n < 0.0);
565 }
566 assert_eq!(numbers(&scaled(all_ones(), f32::MIN_POSITIVE)), [0.0; 4]);
568 assert_eq!(numbers(&scaled(all_ones(), f32::EPSILON)), [0.0; 4]);
569 }
570
571 #[test]
572 fn scale_for_dpi_saturation_is_stable_under_repetition() {
573 let mut s = all_ones();
576 for _ in 0..8 {
577 s.scale_for_dpi(1e30);
578 for n in numbers(&s) {
579 assert!(n.is_finite());
580 assert!(n > 0.0, "saturating scale wrapped to a negative value");
581 }
582 }
583 }
584
585 #[test]
586 fn scale_for_dpi_quantizes_below_the_fixed_point_precision() {
587 let s = scaled(all_ones(), 0.0001);
589 assert_eq!(numbers(&s), [0.0, 0.0, 0.0, 0.0]);
590 }
591
592 #[test]
593 fn scale_for_dpi_on_default_is_a_noop() {
594 for factor in [0.0, 1.0, 2.0, -3.0, f32::NAN, f32::INFINITY, f32::MAX] {
595 let s = scaled(StyleBoxShadow::default(), factor);
596 assert_eq!(
597 numbers(&s),
598 [0.0, 0.0, 0.0, 0.0],
599 "default shadow changed under scale {factor}"
600 );
601 }
602 }
603
604 #[test]
605 fn scale_for_dpi_preserves_metric_color_and_clip_mode() {
606 let mut s = shadow(
607 PixelValueNoPercent {
608 inner: PixelValue::em(2.0),
609 },
610 PixelValueNoPercent {
611 inner: PixelValue::pt(3.0),
612 },
613 PixelValueNoPercent {
614 inner: PixelValue::rem(4.0),
615 },
616 px(5.0),
617 BoxShadowClipMode::Inset,
618 ColorU::new(1, 2, 3, 4),
619 );
620 s.scale_for_dpi(2.0);
621
622 assert_eq!(s.offset_x.inner.metric, SizeMetric::Em);
624 assert_eq!(s.offset_y.inner.metric, SizeMetric::Pt);
625 assert_eq!(s.blur_radius.inner.metric, SizeMetric::Rem);
626 assert_eq!(s.spread_radius.inner.metric, SizeMetric::Px);
627 assert_eq!(numbers(&s), [4.0, 6.0, 8.0, 10.0]);
628
629 assert_eq!(s.clip_mode, BoxShadowClipMode::Inset);
631 assert_eq!(s.color, ColorU::new(1, 2, 3, 4));
632 }
633
634 fn error_corpus() -> Vec<CssShadowParseError<'static>> {
640 vec![
641 CssShadowParseError::TooManyOrTooFewComponents("1px"),
642 CssShadowParseError::TooManyOrTooFewComponents(""),
643 CssShadowParseError::TooManyOrTooFewComponents("\u{1F600}\u{0301} \u{202E}"),
644 CssShadowParseError::ValueParseErr(CssPixelValueParseError::EmptyString),
645 CssShadowParseError::ValueParseErr(CssPixelValueParseError::InvalidPixelValue("abc")),
646 CssShadowParseError::ValueParseErr(CssPixelValueParseError::NoValueGiven(
647 "px",
648 SizeMetric::Px,
649 )),
650 CssShadowParseError::ValueParseErr(CssPixelValueParseError::ValueParseErr(
651 "x".parse::<f32>().unwrap_err(),
652 "x",
653 )),
654 CssShadowParseError::ValueParseErr(CssPixelValueParseError::ValueParseErr(
655 "".parse::<f32>().unwrap_err(),
656 "",
657 )),
658 CssShadowParseError::ColorParseError(parse_css_color("notacolor").unwrap_err()),
659 CssShadowParseError::ColorParseError(parse_css_color("#gg").unwrap_err()),
660 CssShadowParseError::ColorParseError(parse_css_color("rgb(1,2").unwrap_err()),
661 ]
662 }
663
664 #[test]
665 fn shadow_error_to_contained_to_shared_is_lossless() {
666 for e in error_corpus() {
667 let owned = e.to_contained();
668 assert_eq!(owned.to_shared(), e, "borrow -> own -> borrow lost data");
669 }
670 }
671
672 #[test]
673 fn shadow_error_owned_to_shared_to_contained_is_lossless() {
674 for e in error_corpus() {
675 let owned = e.to_contained();
676 assert_eq!(
677 owned.to_shared().to_contained(),
678 owned,
679 "own -> borrow -> own lost data"
680 );
681 }
682 }
683
684 #[test]
685 fn shadow_error_to_contained_preserves_the_input_string_verbatim() {
686 for input in [
687 "",
688 " ",
689 "\u{1F600}",
690 "e\u{0301}\u{0301}\u{0301}",
691 "10px 5px 4px 3px 2px",
692 ] {
693 let owned = CssShadowParseError::TooManyOrTooFewComponents(input).to_contained();
694 match owned {
695 CssShadowParseErrorOwned::TooManyOrTooFewComponents(s) => {
696 assert_eq!(s.as_str(), input);
697 }
698 other => panic!("wrong variant: {other:?}"),
699 }
700 }
701 }
702
703 #[test]
704 fn shadow_error_to_contained_survives_a_very_long_input() {
705 let long = "x".repeat(200_000);
706 let owned = CssShadowParseError::TooManyOrTooFewComponents(&long).to_contained();
707 match owned.to_shared() {
708 CssShadowParseError::TooManyOrTooFewComponents(s) => {
709 assert_eq!(s.len(), 200_000);
710 }
711 other => panic!("wrong variant: {other:?}"),
712 }
713 }
714
715 #[test]
716 fn shadow_error_display_and_debug_are_non_empty() {
717 for e in error_corpus() {
718 assert!(!e.to_string().is_empty(), "empty Display for {e:?}");
719 assert_eq!(format!("{e:?}"), e.to_string());
721 assert!(!format!("{:?}", e.to_contained()).is_empty());
722 }
723 }
724
725 #[test]
726 fn shadow_error_to_contained_round_trips_real_parser_errors() {
727 for bad in ["", "1px", "abc 1px", "10% 5px", "1 2 3 4 5"] {
729 let e = parse_style_box_shadow(bad).unwrap_err();
730 assert_eq!(e.to_contained().to_shared(), e);
731 }
732 }
733
734 #[test]
739 fn parse_valid_minimal_positive_control() {
740 let s = parse_style_box_shadow("10px 5px").unwrap();
741 assert_eq!(
742 s,
743 shadow(
744 px(10.0),
745 px(5.0),
746 px(0.0),
747 px(0.0),
748 BoxShadowClipMode::Outset,
749 ColorU::BLACK,
750 )
751 );
752 }
753
754 #[test]
755 fn parse_empty_and_whitespace_only_input_is_err() {
756 for input in [
757 "", " ", " ", "\t", "\n", "\t\n\r ", "\u{00a0}", "\u{3000}",
758 ] {
759 let e = parse_style_box_shadow(input).unwrap_err();
760 assert_eq!(
761 e,
762 CssShadowParseError::TooManyOrTooFewComponents(input),
763 "unexpected error for {input:?}"
764 );
765 }
766 }
767
768 #[test]
769 fn parse_garbage_is_err_and_never_panics() {
770 for input in [
771 "!!!",
772 "@#$%^&*",
773 "; drop table",
774 "{}{}{}",
775 "\\\\\\",
776 "\0\0",
777 "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%",
787 "1px 2px 3%",
788 "px px",
789 "in in",
790 "1px 2px 3px 4px 5px red inset",
791 ] {
792 assert!(
793 parse_style_box_shadow(input).is_err(),
794 "expected Err for {input:?}"
795 );
796 }
797 }
798
799 #[test]
800 fn parse_leading_trailing_junk_is_trimmed_or_rejected_deterministically() {
801 let padded = parse_style_box_shadow(" 10px 5px ").unwrap();
803 assert_eq!(padded, parse_style_box_shadow("10px 5px").unwrap());
804
805 for input in ["10px 5px;", "10px, 5px", "10px 5px !important", "10px 5px}"] {
807 let e = parse_style_box_shadow(input).unwrap_err();
808 assert!(
809 matches!(e, CssShadowParseError::ValueParseErr(_))
810 || matches!(e, CssShadowParseError::TooManyOrTooFewComponents(_)),
811 "unexpected error kind for {input:?}: {e:?}"
812 );
813 }
814 }
815
816 #[test]
817 fn parse_component_count_boundaries() {
818 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());
826 assert!(parse_style_box_shadow("inset red 1px 2px 3px 4px 5px").is_err());
828 }
829
830 #[test]
831 fn parse_is_insensitive_to_keyword_and_color_position() {
832 let expected = shadow(
833 px(1.0),
834 px(2.0),
835 px(0.0),
836 px(0.0),
837 BoxShadowClipMode::Inset,
838 ColorU::RED,
839 );
840 for input in [
841 "inset red 1px 2px",
842 "red inset 1px 2px",
843 "1px 2px red inset",
844 "1px 2px inset red",
845 "red 1px 2px inset",
846 "1px red 2px inset",
847 "inset 1px red 2px",
848 ] {
849 assert_eq!(
850 parse_style_box_shadow(input).unwrap(),
851 expected,
852 "position of inset/color changed the result for {input:?}"
853 );
854 }
855 }
856
857 #[test]
858 fn parse_accepts_every_color_syntax() {
859 for (input, expected) in [
860 ("1px 2px #888", ColorU::new_rgb(0x88, 0x88, 0x88)),
861 ("1px 2px #ff0000", ColorU::RED),
862 ("1px 2px #ff0000ff", ColorU::RED),
863 ("1px 2px #f00f", ColorU::RED),
864 ("1px 2px rgb(255,0,0)", ColorU::RED),
865 ("1px 2px rgba(255,0,0,1.0)", ColorU::RED),
866 ("1px 2px red", ColorU::RED),
867 ("1px 2px RED", ColorU::RED),
868 ("1px 2px transparent", ColorU::new(0, 0, 0, 0)),
869 ] {
870 let s = parse_style_box_shadow(input).unwrap();
871 assert_eq!(s.color, expected, "wrong color for {input:?}");
872 assert_eq!(s.offset_x, px(1.0));
873 assert_eq!(s.offset_y, px(2.0));
874 }
875 }
876
877 #[test]
878 fn parse_bare_zero_is_a_pixel_value_not_a_color() {
879 let s = parse_style_box_shadow("0 0").unwrap();
880 assert_eq!(s.offset_x, px(0.0));
881 assert_eq!(s.offset_y, px(0.0));
882 assert_eq!(s.offset_x.inner.metric, SizeMetric::Px);
883 assert_eq!(
884 s.color,
885 ColorU::BLACK,
886 "a bare 0 was eaten by the color parser"
887 );
888 }
889
890 #[test]
891 fn parse_boundary_numbers_are_finite_and_defined() {
892 let zeroes = parse_style_box_shadow("-0 -0").unwrap();
894 assert_eq!(numbers(&zeroes)[0], 0.0);
895 assert_eq!(numbers(&zeroes)[1], 0.0);
896 assert!(!numbers(&zeroes)[0].is_sign_negative());
897
898 for input in [
900 "1e30px 1e30px",
901 "9223372036854775807px 9223372036854775807px", "340282350000000000000000000000000000000px 1px", ] {
904 let s = parse_style_box_shadow(input).unwrap();
905 let n = numbers(&s)[0];
906 assert!(n.is_finite(), "{input:?} produced a non-finite offset");
907 assert!(n > 0.0, "{input:?} saturated to the wrong sign");
908 }
909 let neg = parse_style_box_shadow("-1e30px 1px").unwrap();
910 assert!(numbers(&neg)[0].is_finite() && numbers(&neg)[0] < 0.0);
911
912 let tiny = parse_style_box_shadow("1e-30px 0.00001px").unwrap();
914 assert_eq!(numbers(&tiny)[0], 0.0);
915 assert_eq!(numbers(&tiny)[1], 0.0);
916 }
917
918 #[test]
919 fn parse_nan_and_infinity_tokens_are_defined_not_poisonous() {
920 let nan = parse_style_box_shadow("NaN NaN").unwrap();
923 for n in numbers(&nan) {
924 assert!(!n.is_nan(), "NaN survived into a parsed shadow");
925 assert_eq!(n, 0.0);
926 }
927 let nan_px = parse_style_box_shadow("NaNpx 1px").unwrap();
928 assert_eq!(numbers(&nan_px)[0], 0.0);
929
930 let inf = parse_style_box_shadow("inf 1px").unwrap();
932 assert!(numbers(&inf)[0].is_finite() && numbers(&inf)[0] > 0.0);
933 let neg_inf = parse_style_box_shadow("-inf 1px").unwrap();
934 assert!(numbers(&neg_inf)[0].is_finite() && numbers(&neg_inf)[0] < 0.0);
935 let infinity = parse_style_box_shadow("1px infinity").unwrap();
936 assert!(numbers(&infinity)[1].is_finite());
937
938 assert!(parse_style_box_shadow("in 1px").is_err());
940 }
941
942 #[test]
943 fn parse_never_yields_a_percent_metric() {
944 for input in [
945 "1px 2px",
946 "0 0",
947 "1em 2rem 3pt 4in",
948 "5vw 5vh 5vmin 5vmax",
949 "1cm 2mm",
950 "inset 1px 2px red",
951 ] {
952 let s = parse_style_box_shadow(input).unwrap();
953 for m in [
954 s.offset_x.inner.metric,
955 s.offset_y.inner.metric,
956 s.blur_radius.inner.metric,
957 s.spread_radius.inner.metric,
958 ] {
959 assert_ne!(
960 m,
961 SizeMetric::Percent,
962 "percent metric leaked from {input:?}"
963 );
964 }
965 }
966 }
967
968 #[test]
969 fn parse_unicode_input_does_not_panic() {
970 for input in [
973 "\u{1F600}",
974 "\u{1F600} \u{1F600}",
975 "1px 2px \u{1F600}",
976 "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",
980 "\u{202E}1px 2px",
981 "\u{0661}px \u{0662}px", "1px 2px", "1px 2px \u{fffd}",
984 ] {
985 let _ = parse_style_box_shadow(input); }
987
988 let nbsp = parse_style_box_shadow("10px\u{00a0}5px").unwrap();
990 assert_eq!(nbsp, parse_style_box_shadow("10px 5px").unwrap());
991 }
992
993 #[test]
994 fn parse_extremely_long_input_terminates() {
995 let many = "1px ".repeat(50_000);
997 assert!(matches!(
998 parse_style_box_shadow(&many),
999 Err(CssShadowParseError::TooManyOrTooFewComponents(_))
1000 ));
1001
1002 let long_garbage = "a".repeat(1_000_000);
1004 assert!(parse_style_box_shadow(&long_garbage).is_err());
1005
1006 let huge = format!("{}px 1px", "9".repeat(50_000));
1008 let s = parse_style_box_shadow(&huge).unwrap();
1009 assert!(numbers(&s)[0].is_finite());
1010 assert!(numbers(&s)[0] > 0.0);
1011
1012 let padded = format!("{}1px 2px{}", " ".repeat(100_000), " ".repeat(100_000));
1014 assert_eq!(
1015 parse_style_box_shadow(&padded).unwrap(),
1016 parse_style_box_shadow("1px 2px").unwrap()
1017 );
1018 }
1019
1020 #[test]
1021 fn parse_deeply_nested_input_does_not_stack_overflow() {
1022 let open = "(".repeat(10_000);
1023 let nested = format!("{}{}", open, ")".repeat(10_000));
1024 assert!(parse_style_box_shadow(&nested).is_err());
1025 assert!(parse_style_box_shadow(&open).is_err());
1026 assert!(parse_style_box_shadow(&format!("1px 2px rgb{nested}")).is_err());
1027 assert!(parse_style_box_shadow(&format!("1px 2px {}", "[".repeat(10_000))).is_err());
1028 }
1029
1030 #[test]
1035 fn print_then_parse_is_the_identity() {
1036 for original in round_trip_corpus() {
1037 let printed = original.print_as_css_value();
1038 let reparsed = parse_style_box_shadow(&printed)
1039 .unwrap_or_else(|e| panic!("printed {printed:?} does not re-parse: {e:?}"));
1040 assert_eq!(reparsed, original, "round-trip changed {printed:?}");
1041 }
1042 }
1043
1044 #[test]
1045 fn parse_then_print_then_parse_is_idempotent() {
1046 for input in [
1047 "10px 5px",
1048 "5px 10px 20px",
1049 "2px 2px 2px 1px rgba(0,0,0,0.2)",
1050 "inset 0 0 10px #000",
1051 "5px 1em red inset",
1052 "1px 2px transparent",
1053 "-3px -4px 0 2px #12345678",
1054 ] {
1055 let first = parse_style_box_shadow(input).unwrap();
1056 let printed = first.print_as_css_value();
1057 let second = parse_style_box_shadow(&printed).unwrap();
1058 assert_eq!(first, second, "{input:?} -> {printed:?} was not idempotent");
1059 assert_eq!(printed, second.print_as_css_value());
1061 }
1062 }
1063
1064 #[test]
1065 fn print_omits_defaults_for_brevity() {
1066 let default = StyleBoxShadow::default();
1067 assert_eq!(default.print_as_css_value(), "0px 0px");
1068
1069 let black = shadow(
1071 px(1.0),
1072 px(2.0),
1073 px(3.0),
1074 px(0.0),
1075 BoxShadowClipMode::Outset,
1076 ColorU::BLACK,
1077 );
1078 assert_eq!(black.print_as_css_value(), "1px 2px 3px");
1079 assert!(!black.print_as_css_value().contains('#'));
1080
1081 assert!(!black.print_as_css_value().contains("inset"));
1083 }
1084
1085 #[test]
1086 fn print_writes_inset_first_and_color_last() {
1087 let s = shadow(
1088 px(1.0),
1089 px(2.0),
1090 px(0.0),
1091 px(0.0),
1092 BoxShadowClipMode::Inset,
1093 ColorU::RED,
1094 );
1095 assert_eq!(s.print_as_css_value(), "inset 1px 2px #ff0000ff");
1096 }
1097
1098 #[test]
1099 fn print_emits_a_placeholder_blur_when_only_the_spread_is_set() {
1100 let s = shadow(
1103 px(1.0),
1104 px(2.0),
1105 px(0.0),
1106 px(4.0),
1107 BoxShadowClipMode::Outset,
1108 ColorU::BLACK,
1109 );
1110 assert_eq!(s.print_as_css_value(), "1px 2px 0px 4px");
1111 let reparsed = parse_style_box_shadow(&s.print_as_css_value()).unwrap();
1112 assert_eq!(reparsed.blur_radius, px(0.0));
1113 assert_eq!(reparsed.spread_radius, px(4.0));
1114 }
1115
1116 #[test]
1117 fn print_of_extreme_values_does_not_panic() {
1118 for factor in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX, 1e30] {
1120 let s = scaled(all_ones(), factor);
1121 let printed = s.print_as_css_value();
1122 assert!(!printed.is_empty());
1123 assert!(
1124 !printed.contains("NaN"),
1125 "NaN reached the CSS output: {printed}"
1126 );
1127 assert!(
1128 !printed.contains("inf"),
1129 "inf reached the CSS output: {printed}"
1130 );
1131 }
1132 }
1133
1134 #[test]
1135 fn format_as_rust_code_is_well_formed_for_extremes() {
1136 for s in round_trip_corpus() {
1137 let code = s.format_as_rust_code(0);
1138 assert!(code.starts_with("StyleBoxShadow {"));
1139 assert!(code.contains("offset_x:"));
1140 assert!(code.contains("offset_y:"));
1141 assert!(code.contains("blur_radius:"));
1142 assert!(code.contains("spread_radius:"));
1143 assert!(code.contains("clip_mode: BoxShadowClipMode::"));
1144 assert!(code.contains("color:"));
1145 }
1146 let inset = scaled(all_ones(), f32::INFINITY);
1148 let code = inset.format_as_rust_code(3);
1149 assert!(code.contains("clip_mode: BoxShadowClipMode::Inset"));
1150 assert!(code.contains(" offset_x:"));
1151 }
1152
1153 #[test]
1158 fn default_shadow_is_an_opaque_black_outset_at_the_origin() {
1159 let d = StyleBoxShadow::default();
1160 assert_eq!(d.clip_mode, BoxShadowClipMode::Outset);
1161 assert_eq!(d.color, ColorU::BLACK);
1162 assert_eq!(numbers(&d), [0.0, 0.0, 0.0, 0.0]);
1163 assert_eq!(d.offset_x.inner.metric, SizeMetric::Px);
1164 assert_eq!(parse_style_box_shadow("0 0").unwrap(), d);
1166 }
1167
1168 #[test]
1169 fn equality_is_component_wise() {
1170 let base = all_ones();
1171 assert_eq!(base, base);
1172 assert_ne!(base, StyleBoxShadow::default());
1173
1174 let mut clip = base;
1175 clip.clip_mode = BoxShadowClipMode::Outset;
1176 assert_ne!(base, clip, "clip_mode is ignored by PartialEq");
1177
1178 let mut color = base;
1179 color.color = ColorU::BLACK;
1180 assert_ne!(base, color, "color is ignored by PartialEq");
1181
1182 let mut spread = base;
1183 spread.spread_radius = px(9.0);
1184 assert_ne!(base, spread, "spread_radius is ignored by PartialEq");
1185 }
1186}