1use alloc::string::{String, ToString};
6use crate::corety::AzString;
7
8use crate::{
9 props::{
10 basic::pixel::{
11 parse_pixel_value, CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue,
12 },
13 macros::PixelValueTaker,
14 },
15};
16
17macro_rules! define_border_radius_property {
20 ($struct_name:ident) => {
21 #[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22 #[repr(C)]
23 pub struct $struct_name {
24 pub inner: PixelValue,
25 }
26
27 impl ::core::fmt::Debug for $struct_name {
28 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
29 write!(f, "{}", self.inner)
30 }
31 }
32
33 impl PixelValueTaker for $struct_name {
34 fn from_pixel_value(inner: PixelValue) -> Self {
35 Self { inner }
36 }
37 }
38
39 impl_pixel_value!($struct_name);
40 };
41}
42
43define_border_radius_property!(StyleBorderTopLeftRadius);
45define_border_radius_property!(StyleBorderTopRightRadius);
47define_border_radius_property!(StyleBorderBottomLeftRadius);
49define_border_radius_property!(StyleBorderBottomRightRadius);
51
52#[cfg(feature = "parser")]
56#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
57pub struct StyleBorderRadius {
58 pub top_left: PixelValue,
59 pub top_right: PixelValue,
60 pub bottom_left: PixelValue,
61 pub bottom_right: PixelValue,
62}
63
64#[derive(Clone, PartialEq, Eq)]
68pub enum CssBorderRadiusParseError<'a> {
69 TooManyValues(&'a str),
71 PixelValue(CssPixelValueParseError<'a>),
73}
74
75impl_debug_as_display!(CssBorderRadiusParseError<'a>);
76impl_display! { CssBorderRadiusParseError<'a>, {
77 TooManyValues(val) => format!("Too many values for border-radius: \"{}\"", val),
78 PixelValue(e) => format!("{}", e),
79}}
80impl_from!(
81 CssPixelValueParseError<'a>,
82 CssBorderRadiusParseError::PixelValue
83);
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87#[repr(C, u8)]
88pub enum CssBorderRadiusParseErrorOwned {
89 TooManyValues(AzString),
90 PixelValue(CssPixelValueParseErrorOwned),
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95#[repr(C)]
96pub struct CssStyleBorderRadiusParseErrorOwned {
97 pub inner: CssBorderRadiusParseErrorOwned,
98}
99
100impl From<CssBorderRadiusParseErrorOwned> for CssStyleBorderRadiusParseErrorOwned {
101 fn from(v: CssBorderRadiusParseErrorOwned) -> Self {
102 Self { inner: v }
103 }
104}
105
106impl CssBorderRadiusParseError<'_> {
107 #[must_use] pub fn to_contained(&self) -> CssBorderRadiusParseErrorOwned {
108 match self {
109 CssBorderRadiusParseError::TooManyValues(s) => {
110 CssBorderRadiusParseErrorOwned::TooManyValues((*s).to_string().into())
111 }
112 CssBorderRadiusParseError::PixelValue(e) => {
113 CssBorderRadiusParseErrorOwned::PixelValue(e.to_contained())
114 }
115 }
116 }
117}
118
119impl CssBorderRadiusParseErrorOwned {
120 #[must_use] pub fn to_shared(&self) -> CssBorderRadiusParseError<'_> {
121 match self {
122 Self::TooManyValues(s) => {
123 CssBorderRadiusParseError::TooManyValues(s)
124 }
125 Self::PixelValue(e) => {
126 CssBorderRadiusParseError::PixelValue(e.to_shared())
127 }
128 }
129 }
130}
131
132macro_rules! define_border_radius_parse_error {
134 ($error_name:ident, $error_name_owned:ident) => {
135 #[derive(Clone, PartialEq, Eq)]
136 pub enum $error_name<'a> {
137 PixelValue(CssPixelValueParseError<'a>),
138 }
139
140 impl_debug_as_display!($error_name<'a>);
141 impl_display! { $error_name<'a>, {
142 PixelValue(e) => format!("{}", e),
143 }}
144
145 impl_from!(CssPixelValueParseError<'a>, $error_name::PixelValue);
146
147 #[derive(Debug, Clone, PartialEq, Eq)]
148 #[repr(C, u8)]
149 pub enum $error_name_owned {
150 PixelValue(CssPixelValueParseErrorOwned),
151 }
152
153 impl $error_name<'_> {
154 #[must_use] pub fn to_contained(&self) -> $error_name_owned {
155 match self {
156 $error_name::PixelValue(e) => $error_name_owned::PixelValue(e.to_contained()),
157 }
158 }
159 }
160
161 impl $error_name_owned {
162 #[must_use] pub fn to_shared(&self) -> $error_name<'_> {
163 match self {
164 $error_name_owned::PixelValue(e) => $error_name::PixelValue(e.to_shared()),
165 }
166 }
167 }
168 };
169}
170
171define_border_radius_parse_error!(
172 StyleBorderTopLeftRadiusParseError,
173 StyleBorderTopLeftRadiusParseErrorOwned
174);
175define_border_radius_parse_error!(
176 StyleBorderTopRightRadiusParseError,
177 StyleBorderTopRightRadiusParseErrorOwned
178);
179define_border_radius_parse_error!(
180 StyleBorderBottomLeftRadiusParseError,
181 StyleBorderBottomLeftRadiusParseErrorOwned
182);
183define_border_radius_parse_error!(
184 StyleBorderBottomRightRadiusParseError,
185 StyleBorderBottomRightRadiusParseErrorOwned
186);
187
188#[cfg(feature = "parser")]
192pub fn parse_style_border_radius(
196 input: &str,
197) -> Result<StyleBorderRadius, CssBorderRadiusParseError<'_>> {
198 let components: Vec<_> = input.split_whitespace().collect();
199 let mut values = Vec::with_capacity(components.len());
200 for comp in &components {
201 values.push(parse_pixel_value(comp)?);
202 }
203
204 match values.len() {
205 1 => Ok(StyleBorderRadius {
206 top_left: values[0],
207 top_right: values[0],
208 bottom_right: values[0],
209 bottom_left: values[0],
210 }),
211 2 => Ok(StyleBorderRadius {
212 top_left: values[0],
213 top_right: values[1],
214 bottom_right: values[0],
215 bottom_left: values[1],
216 }),
217 3 => Ok(StyleBorderRadius {
218 top_left: values[0],
219 top_right: values[1],
220 bottom_right: values[2],
221 bottom_left: values[1],
222 }),
223 4 => Ok(StyleBorderRadius {
224 top_left: values[0],
225 top_right: values[1],
226 bottom_right: values[2],
227 bottom_left: values[3],
228 }),
229 _ => Err(CssBorderRadiusParseError::TooManyValues(input)),
230 }
231}
232
233#[cfg(feature = "parser")]
235pub fn parse_style_border_top_left_radius(
239 input: &str,
240) -> Result<StyleBorderTopLeftRadius, StyleBorderTopLeftRadiusParseError<'_>> {
241 let pixel_value = parse_pixel_value(input)?;
242 Ok(StyleBorderTopLeftRadius { inner: pixel_value })
243}
244
245#[cfg(feature = "parser")]
247pub fn parse_style_border_top_right_radius(
251 input: &str,
252) -> Result<StyleBorderTopRightRadius, StyleBorderTopRightRadiusParseError<'_>> {
253 let pixel_value = parse_pixel_value(input)?;
254 Ok(StyleBorderTopRightRadius { inner: pixel_value })
255}
256
257#[cfg(feature = "parser")]
259pub fn parse_style_border_bottom_left_radius(
263 input: &str,
264) -> Result<StyleBorderBottomLeftRadius, StyleBorderBottomLeftRadiusParseError<'_>> {
265 let pixel_value = parse_pixel_value(input)?;
266 Ok(StyleBorderBottomLeftRadius { inner: pixel_value })
267}
268
269#[cfg(feature = "parser")]
271pub fn parse_style_border_bottom_right_radius(
275 input: &str,
276) -> Result<StyleBorderBottomRightRadius, StyleBorderBottomRightRadiusParseError<'_>> {
277 let pixel_value = parse_pixel_value(input)?;
278 Ok(StyleBorderBottomRightRadius { inner: pixel_value })
279}
280
281#[cfg(all(test, feature = "parser"))]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn test_parse_border_radius_shorthand() {
287 let result = parse_style_border_radius("10px").unwrap();
289 assert_eq!(result.top_left, PixelValue::px(10.0));
290 assert_eq!(result.top_right, PixelValue::px(10.0));
291 assert_eq!(result.bottom_right, PixelValue::px(10.0));
292 assert_eq!(result.bottom_left, PixelValue::px(10.0));
293
294 let result = parse_style_border_radius("10px 5%").unwrap();
296 assert_eq!(result.top_left, PixelValue::px(10.0));
297 assert_eq!(result.top_right, PixelValue::percent(5.0));
298 assert_eq!(result.bottom_right, PixelValue::px(10.0));
299 assert_eq!(result.bottom_left, PixelValue::percent(5.0));
300
301 let result = parse_style_border_radius("2px 4px 8px").unwrap();
303 assert_eq!(result.top_left, PixelValue::px(2.0));
304 assert_eq!(result.top_right, PixelValue::px(4.0));
305 assert_eq!(result.bottom_right, PixelValue::px(8.0));
306 assert_eq!(result.bottom_left, PixelValue::px(4.0));
307
308 let result = parse_style_border_radius("1px 0 3px 4px").unwrap();
310 assert_eq!(result.top_left, PixelValue::px(1.0));
311 assert_eq!(result.top_right, PixelValue::px(0.0));
312 assert_eq!(result.bottom_right, PixelValue::px(3.0));
313 assert_eq!(result.bottom_left, PixelValue::px(4.0));
314
315 let result = parse_style_border_radius(" 1em 2em ").unwrap();
317 assert_eq!(result.top_left, PixelValue::em(1.0));
318 assert_eq!(result.top_right, PixelValue::em(2.0));
319 }
320
321 #[test]
322 fn test_parse_border_radius_shorthand_errors() {
323 assert!(parse_style_border_radius("").is_err());
324 assert!(parse_style_border_radius("1px 2px 3px 4px 5px").is_err());
325 assert!(parse_style_border_radius("1px bad 3px").is_err());
326 }
327
328 #[test]
329 fn test_parse_longhand_radius() {
330 let result = parse_style_border_top_left_radius("25%").unwrap();
331 assert_eq!(result.inner, PixelValue::percent(25.0));
332 }
333}
334
335#[cfg(all(test, feature = "parser"))]
336#[allow(
337 clippy::float_cmp,
338 clippy::unreadable_literal,
339 clippy::too_many_lines,
340 clippy::cast_precision_loss
341)]
342mod autotest_generated {
343 use super::*;
344 use crate::{css::PrintAsCssValue, props::basic::length::SizeMetric};
345
346 const ROUNDTRIPPABLE_METRICS: [SizeMetric; 11] = [
349 SizeMetric::Px,
350 SizeMetric::Pt,
351 SizeMetric::Em,
352 SizeMetric::Rem,
353 SizeMetric::In,
354 SizeMetric::Cm,
355 SizeMetric::Mm,
356 SizeMetric::Percent,
357 SizeMetric::Vw,
358 SizeMetric::Vh,
359 SizeMetric::Vmax,
360 ];
361
362 const EXACT_VALUES: [f32; 6] = [0.0, 1.0, 12.5, -3.25, 0.125, 1000.5];
366
367 const GARBAGE: [&str; 14] = [
369 "bad",
370 "px",
371 "%",
372 "em",
373 "-",
374 "+",
375 ".",
376 "e",
377 "1..2px",
378 "1,2px",
379 "10px;",
380 "10px!important",
381 "\0",
382 "\u{7f}\u{1}",
383 ];
384
385 fn all_longhands(input: &str) -> [Result<PixelValue, ()>; 4] {
386 [
387 parse_style_border_top_left_radius(input)
388 .map(|v| v.inner)
389 .map_err(|_| ()),
390 parse_style_border_top_right_radius(input)
391 .map(|v| v.inner)
392 .map_err(|_| ()),
393 parse_style_border_bottom_left_radius(input)
394 .map(|v| v.inner)
395 .map_err(|_| ()),
396 parse_style_border_bottom_right_radius(input)
397 .map(|v| v.inner)
398 .map_err(|_| ()),
399 ]
400 }
401
402 #[test]
405 fn shorthand_rejects_empty_and_whitespace_only() {
406 for input in ["", " ", " ", "\t\n", "\r\n\t ", "\u{a0}"] {
410 let err = parse_style_border_radius(input)
411 .expect_err("whitespace-only border-radius must not parse");
412 assert!(
413 matches!(err, CssBorderRadiusParseError::TooManyValues(_)),
414 "unexpected error for {input:?}: {err}"
415 );
416 }
417 }
418
419 #[test]
420 fn shorthand_rejects_garbage_without_panicking() {
421 for input in GARBAGE {
422 assert!(
423 parse_style_border_radius(input).is_err(),
424 "garbage {input:?} was accepted"
425 );
426 }
427 }
428
429 #[test]
430 fn shorthand_rejects_more_than_four_values() {
431 let input = "1px 2px 3px 4px 5px";
432 let err = parse_style_border_radius(input).unwrap_err();
433 assert_eq!(err, CssBorderRadiusParseError::TooManyValues(input));
434 assert!(format!("{err}").contains(input));
436 }
437
438 #[test]
439 fn shorthand_reports_the_bad_component_before_counting_values() {
440 let err = parse_style_border_radius("1px 2px 3px 4px 5px bad").unwrap_err();
443 assert_eq!(
444 err,
445 CssBorderRadiusParseError::PixelValue(CssPixelValueParseError::InvalidPixelValue("bad"))
446 );
447 }
448
449 #[test]
450 fn shorthand_rejects_elliptical_slash_syntax() {
451 assert!(parse_style_border_radius("10px / 20px").is_err());
455 assert!(parse_style_border_radius("10px/20px").is_err());
456 assert!(parse_style_border_radius("1px 2px / 3px 4px").is_err());
457 }
458
459 #[test]
460 fn shorthand_rejects_split_number_and_unit() {
461 let err = parse_style_border_radius("10 px").unwrap_err();
464 assert_eq!(
465 err,
466 CssBorderRadiusParseError::PixelValue(CssPixelValueParseError::NoValueGiven(
467 "px",
468 SizeMetric::Px
469 ))
470 );
471 }
472
473 #[test]
476 fn shorthand_boundary_numbers_never_produce_nan_or_infinity() {
477 let inputs = [
480 "0",
481 "-0",
482 "+0",
483 "0px",
484 "-0px",
485 "0.0001px",
486 "1e-30px",
487 "1e30px",
488 "-1e30px",
489 "3.4028235e38px",
490 "-3.4028235e38px",
491 "9223372036854775807px",
492 "-9223372036854775808px",
493 "340282350000000000000000000000000000000%",
494 ];
495 for input in inputs {
496 let Ok(radius) = parse_style_border_radius(input) else {
497 continue; };
499 for corner in [
500 radius.top_left,
501 radius.top_right,
502 radius.bottom_left,
503 radius.bottom_right,
504 ] {
505 let n = corner.number.get();
506 assert!(n.is_finite(), "{input:?} produced non-finite {n}");
507 }
508 }
509 }
510
511 #[test]
512 fn shorthand_nan_input_is_flattened_to_zero() {
513 for input in ["NaN", "nan", "NaNpx", "-nan%"] {
518 let radius = parse_style_border_radius(input)
519 .unwrap_or_else(|e| panic!("{input:?} unexpectedly rejected: {e}"));
520 let n = radius.top_left.number.get();
521 assert!(!n.is_nan(), "{input:?} leaked a NaN");
522 assert_eq!(n, 0.0, "{input:?} should quantize to 0");
523 }
524 }
525
526 #[test]
527 fn shorthand_infinite_input_saturates_instead_of_overflowing() {
528 let pos = parse_style_border_radius("infpx").unwrap().top_left;
529 let neg = parse_style_border_radius("-infpx").unwrap().top_left;
530 assert!(pos.number.get().is_finite());
531 assert!(neg.number.get().is_finite());
532 assert!(pos.number.get() > 0.0);
533 assert!(neg.number.get() < 0.0);
534 assert_eq!(pos.number.number(), isize::MAX);
536 assert_eq!(neg.number.number(), isize::MIN);
537 }
538
539 #[test]
540 fn shorthand_sub_quantum_values_truncate_to_zero() {
541 let radius = parse_style_border_radius("0.0001px").unwrap();
544 assert_eq!(radius.top_left, PixelValue::px(0.0));
545 assert_eq!(radius.top_left.number.number(), 0);
546 }
547
548 #[test]
549 fn shorthand_negative_zero_equals_positive_zero() {
550 let neg = parse_style_border_radius("-0px").unwrap();
551 let pos = parse_style_border_radius("0px").unwrap();
552 assert_eq!(neg, pos);
553 assert_eq!(neg.top_left.number.number(), 0);
554 }
555
556 #[test]
559 fn shorthand_extremely_long_input_terminates() {
560 let many = "1px ".repeat(100_000);
562 assert!(matches!(
563 parse_style_border_radius(&many),
564 Err(CssBorderRadiusParseError::TooManyValues(_))
565 ));
566
567 let huge = format!("{}px", "1".repeat(100_000));
570 let radius = parse_style_border_radius(&huge).unwrap();
571 assert!(radius.top_left.number.get().is_finite());
572
573 let junk = "z".repeat(100_000);
575 assert!(parse_style_border_radius(&junk).is_err());
576 }
577
578 #[test]
579 fn shorthand_deeply_nested_brackets_do_not_stack_overflow() {
580 let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
581 assert!(parse_style_border_radius(&nested).is_err());
582
583 let calls = format!("{}1px{}", "calc(".repeat(10_000), ")".repeat(10_000));
584 assert!(parse_style_border_radius(&calls).is_err());
585 }
586
587 #[test]
588 fn shorthand_unicode_input_is_rejected_without_panicking() {
589 for input in [
591 "\u{1F600}",
592 "10px\u{1F600}",
593 "\u{1F600}px",
594 "\u{661}\u{660}px", "10\u{301}px", "10px\u{200b}", "10px", "10\u{202e}px", ] {
600 assert!(
601 parse_style_border_radius(input).is_err(),
602 "unicode {input:?} was accepted"
603 );
604 }
605 }
606
607 #[test]
608 fn shorthand_treats_non_breaking_space_as_a_value_separator() {
609 let radius = parse_style_border_radius("1px\u{a0}2px").unwrap();
614 assert_eq!(radius.top_left, PixelValue::px(1.0));
615 assert_eq!(radius.top_right, PixelValue::px(2.0));
616 }
617
618 #[test]
621 fn shorthand_one_value_fills_every_corner() {
622 let radius = parse_style_border_radius("7px").unwrap();
623 assert_eq!(radius.top_left, PixelValue::px(7.0));
624 assert_eq!(radius.top_right, PixelValue::px(7.0));
625 assert_eq!(radius.bottom_left, PixelValue::px(7.0));
626 assert_eq!(radius.bottom_right, PixelValue::px(7.0));
627 }
628
629 #[test]
630 fn shorthand_two_and_three_values_expand_along_the_diagonals() {
631 let two = parse_style_border_radius("1px 2px").unwrap();
633 assert_eq!(two.top_left, two.bottom_right);
634 assert_eq!(two.top_right, two.bottom_left);
635 assert_eq!(two.top_left, PixelValue::px(1.0));
636 assert_eq!(two.top_right, PixelValue::px(2.0));
637
638 let three = parse_style_border_radius("1px 2px 3px").unwrap();
640 assert_eq!(three.top_left, PixelValue::px(1.0));
641 assert_eq!(three.top_right, PixelValue::px(2.0));
642 assert_eq!(three.bottom_left, PixelValue::px(2.0));
643 assert_eq!(three.bottom_right, PixelValue::px(3.0));
644 }
645
646 #[test]
647 fn shorthand_four_values_map_clockwise_from_top_left() {
648 let four = parse_style_border_radius("1px 2px 3px 4px").unwrap();
649 assert_eq!(four.top_left, PixelValue::px(1.0));
650 assert_eq!(four.top_right, PixelValue::px(2.0));
651 assert_eq!(four.bottom_right, PixelValue::px(3.0));
652 assert_eq!(four.bottom_left, PixelValue::px(4.0));
653 assert_ne!(four.top_left, four.top_right);
656 assert_ne!(four.bottom_right, four.bottom_left);
657 }
658
659 #[test]
660 fn shorthand_preserves_per_corner_units() {
661 let radius = parse_style_border_radius("1px 2em 3% 4rem").unwrap();
662 assert_eq!(radius.top_left, PixelValue::px(1.0));
663 assert_eq!(radius.top_right, PixelValue::em(2.0));
664 assert_eq!(radius.bottom_right, PixelValue::percent(3.0));
665 assert_eq!(radius.bottom_left, PixelValue::rem(4.0));
666 }
667
668 #[test]
669 fn shorthand_valid_minimal_input() {
670 let radius = parse_style_border_radius("0").unwrap();
671 assert_eq!(radius.top_left, PixelValue::px(0.0));
672 }
673
674 #[test]
675 fn shorthand_tolerates_arbitrary_ascii_whitespace_runs() {
676 let radius = parse_style_border_radius("\n\t 1px \t 2px\r\n").unwrap();
677 assert_eq!(radius.top_left, PixelValue::px(1.0));
678 assert_eq!(radius.top_right, PixelValue::px(2.0));
679 }
680
681 #[test]
684 fn longhands_reject_empty_whitespace_and_garbage() {
685 for input in ["", " ", "\t\n"] {
686 for result in all_longhands(input) {
687 assert_eq!(result, Err(()), "{input:?} was accepted by a longhand");
688 }
689 }
690 for input in GARBAGE {
691 for result in all_longhands(input) {
692 assert_eq!(result, Err(()), "{input:?} was accepted by a longhand");
693 }
694 }
695 }
696
697 #[test]
698 fn longhands_reject_multiple_values() {
699 for input in ["1px 2px", "1px 2px 3px 4px", "1px,2px"] {
702 for result in all_longhands(input) {
703 assert_eq!(result, Err(()), "{input:?} was accepted by a longhand");
704 }
705 }
706 }
707
708 #[test]
709 fn all_four_longhands_agree_on_every_input() {
710 let inputs = [
713 "0", "10px", "-3.25em", "50%", "1.5rem", "2pt", "1in", "2.54cm", "10mm", "5vw", "5vh",
714 "5vmax", "bad", "", " ", "\u{1F600}", "1e30px", "NaNpx",
715 ];
716 for input in inputs {
717 let [tl, tr, bl, br] = all_longhands(input);
718 assert_eq!(tl, tr, "top-left vs top-right disagree on {input:?}");
719 assert_eq!(tl, bl, "top-left vs bottom-left disagree on {input:?}");
720 assert_eq!(tl, br, "top-left vs bottom-right disagree on {input:?}");
721 }
722 }
723
724 #[test]
725 fn longhands_agree_with_the_shorthand_on_single_values() {
726 for input in ["0", "10px", "-3.25em", "50%", "1.5rem", "2pt"] {
727 let shorthand = parse_style_border_radius(input).unwrap();
728 let [tl, tr, bl, br] = all_longhands(input);
729 assert_eq!(tl, Ok(shorthand.top_left), "{input:?}");
730 assert_eq!(tr, Ok(shorthand.top_right), "{input:?}");
731 assert_eq!(bl, Ok(shorthand.bottom_left), "{input:?}");
732 assert_eq!(br, Ok(shorthand.bottom_right), "{input:?}");
733 }
734 }
735
736 #[test]
737 fn longhands_accept_a_gap_between_number_and_unit_but_the_shorthand_does_not() {
738 for result in all_longhands("10 px") {
742 assert_eq!(result, Ok(PixelValue::px(10.0)));
743 }
744 assert!(parse_style_border_radius("10 px").is_err());
745 }
746
747 #[test]
748 fn longhands_survive_extremely_long_and_nested_input() {
749 let long = format!("{}px", "9".repeat(100_000));
750 for result in all_longhands(&long) {
751 assert!(result.unwrap().number.get().is_finite());
752 }
753 let nested = format!("{}1px{}", "(".repeat(10_000), ")".repeat(10_000));
754 for result in all_longhands(&nested) {
755 assert_eq!(result, Err(()));
756 }
757 }
758
759 #[test]
760 fn longhands_boundary_numbers_stay_finite() {
761 for input in [
762 "0",
763 "-0",
764 "1e-30px",
765 "1e30px",
766 "-1e30px",
767 "9223372036854775807px",
768 "NaNpx",
769 "infpx",
770 "-infpx",
771 ] {
772 for result in all_longhands(input) {
773 let value = result.unwrap_or_else(|()| panic!("{input:?} rejected"));
774 let n = value.number.get();
775 assert!(n.is_finite() && !n.is_nan(), "{input:?} produced {n}");
776 }
777 }
778 }
779
780 #[test]
783 fn print_as_css_value_round_trips_through_every_longhand_parser() {
784 for metric in ROUNDTRIPPABLE_METRICS {
785 for value in EXACT_VALUES {
786 let pixel = PixelValue::from_metric(metric, value);
787
788 let tl = StyleBorderTopLeftRadius { inner: pixel };
789 let reparsed = parse_style_border_top_left_radius(&tl.print_as_css_value())
790 .unwrap_or_else(|e| panic!("{:?} did not re-parse: {e}", tl.print_as_css_value()));
791 assert_eq!(reparsed, tl);
792
793 let tr = StyleBorderTopRightRadius { inner: pixel };
794 assert_eq!(
795 parse_style_border_top_right_radius(&tr.print_as_css_value()).unwrap(),
796 tr
797 );
798
799 let bl = StyleBorderBottomLeftRadius { inner: pixel };
800 assert_eq!(
801 parse_style_border_bottom_left_radius(&bl.print_as_css_value()).unwrap(),
802 bl
803 );
804
805 let br = StyleBorderBottomRightRadius { inner: pixel };
806 assert_eq!(
807 parse_style_border_bottom_right_radius(&br.print_as_css_value()).unwrap(),
808 br
809 );
810 }
811 }
812 }
813
814 #[test]
815 fn shorthand_round_trips_through_the_printed_corner_values() {
816 let original = parse_style_border_radius("1px 2em 3% 4rem").unwrap();
817 let printed = format!(
818 "{} {} {} {}",
819 StyleBorderTopLeftRadius {
820 inner: original.top_left
821 }
822 .print_as_css_value(),
823 StyleBorderTopRightRadius {
824 inner: original.top_right
825 }
826 .print_as_css_value(),
827 StyleBorderBottomRightRadius {
828 inner: original.bottom_right
829 }
830 .print_as_css_value(),
831 StyleBorderBottomLeftRadius {
832 inner: original.bottom_left
833 }
834 .print_as_css_value(),
835 );
836 assert_eq!(parse_style_border_radius(&printed).unwrap(), original);
837 }
838
839 #[test]
840 fn debug_output_matches_the_printed_css_value() {
841 let pixel = PixelValue::em(1.5);
844 let tl = StyleBorderTopLeftRadius { inner: pixel };
845 assert_eq!(format!("{tl:?}"), "1.5em");
846 assert_eq!(format!("{tl:?}"), tl.print_as_css_value());
847 }
848
849 #[test]
850 fn vmin_radius_parses() {
851 let printed = StyleBorderTopLeftRadius {
856 inner: PixelValue::from_metric(SizeMetric::Vmin, 3.0),
857 }
858 .print_as_css_value();
859 assert_eq!(printed, "3vmin");
860 assert_eq!(
861 parse_style_border_top_left_radius(&printed).unwrap().inner,
862 PixelValue::from_metric(SizeMetric::Vmin, 3.0)
863 );
864 }
865
866 #[test]
869 fn default_and_zero_agree_for_every_corner_type() {
870 assert_eq!(
871 StyleBorderTopLeftRadius::default(),
872 StyleBorderTopLeftRadius::zero()
873 );
874 assert_eq!(
875 StyleBorderTopRightRadius::default(),
876 StyleBorderTopRightRadius::zero()
877 );
878 assert_eq!(
879 StyleBorderBottomLeftRadius::default(),
880 StyleBorderBottomLeftRadius::zero()
881 );
882 assert_eq!(
883 StyleBorderBottomRightRadius::default(),
884 StyleBorderBottomRightRadius::zero()
885 );
886 assert_eq!(
887 StyleBorderTopLeftRadius::default().inner,
888 PixelValue::px(0.0)
889 );
890 }
891
892 #[test]
893 fn const_constructors_agree_with_the_float_constructors() {
894 assert_eq!(
895 StyleBorderTopLeftRadius::const_px(10),
896 StyleBorderTopLeftRadius::px(10.0)
897 );
898 assert_eq!(
899 StyleBorderTopLeftRadius::const_em(2),
900 StyleBorderTopLeftRadius::em(2.0)
901 );
902 assert_eq!(
903 StyleBorderTopLeftRadius::const_percent(50),
904 StyleBorderTopLeftRadius::percent(50.0)
905 );
906 assert_eq!(
907 StyleBorderTopLeftRadius::const_pt(-3),
908 StyleBorderTopLeftRadius::pt(-3.0)
909 );
910 assert_eq!(
911 StyleBorderTopLeftRadius::from_pixel_value(PixelValue::px(4.0)),
912 StyleBorderTopLeftRadius::px(4.0)
913 );
914 }
915
916 #[test]
917 fn interpolate_returns_the_endpoints_for_matching_metrics() {
918 let a = StyleBorderTopLeftRadius::px(0.0);
919 let b = StyleBorderTopLeftRadius::px(10.0);
920 assert_eq!(a.interpolate(&b, 0.0), a);
921 assert_eq!(a.interpolate(&b, 1.0), b);
922 assert_eq!(a.interpolate(&b, 0.5), StyleBorderTopLeftRadius::px(5.0));
923 }
924
925 #[test]
926 fn interpolate_stays_finite_for_hostile_t_values() {
927 let a = StyleBorderTopLeftRadius::px(0.0);
928 let b = StyleBorderTopLeftRadius::px(10.0);
929 for t in [
930 -1.0,
931 2.0,
932 1e30,
933 -1e30,
934 f32::INFINITY,
935 f32::NEG_INFINITY,
936 f32::NAN,
937 ] {
938 let n = a.interpolate(&b, t).inner.number.get();
939 assert!(n.is_finite(), "t={t} produced {n}");
940 assert!(!n.is_nan(), "t={t} produced NaN");
941 }
942 assert_eq!(
944 a.interpolate(&b, f32::NAN).inner.number.get(),
945 0.0
946 );
947 }
948
949 #[test]
950 fn interpolate_across_metrics_converts_to_px() {
951 let em = StyleBorderTopLeftRadius::em(1.0);
954 let px = StyleBorderTopLeftRadius::px(10.0);
955 let mid = em.interpolate(&px, 0.5);
956 assert_eq!(mid.inner.metric, SizeMetric::Px);
957 assert_eq!(mid.inner.number.get(), 13.0);
958 assert_eq!(em.interpolate(&px, 0.0), StyleBorderTopLeftRadius::px(16.0));
960 }
961
962 #[test]
965 fn shorthand_error_to_contained_round_trips_for_every_variant() {
966 let float_err = "x".parse::<f32>().unwrap_err();
967 let variants = [
968 CssBorderRadiusParseError::TooManyValues("1px 2px 3px 4px 5px"),
969 CssBorderRadiusParseError::PixelValue(CssPixelValueParseError::EmptyString),
970 CssBorderRadiusParseError::PixelValue(CssPixelValueParseError::NoValueGiven(
971 "px",
972 SizeMetric::Px,
973 )),
974 CssBorderRadiusParseError::PixelValue(CssPixelValueParseError::ValueParseErr(
975 float_err, "bad",
976 )),
977 CssBorderRadiusParseError::PixelValue(CssPixelValueParseError::InvalidPixelValue(
978 "nope",
979 )),
980 ];
981 for variant in variants {
982 let owned = variant.to_contained();
983 assert_eq!(owned.to_shared(), variant);
984 assert_eq!(format!("{}", owned.to_shared()), format!("{variant}"));
986 }
987 }
988
989 #[test]
990 fn shorthand_error_to_contained_handles_empty_unicode_and_huge_inputs() {
991 let huge = "1px ".repeat(50_000);
992 for input in ["", " ", "\u{1F600}\u{301}", "\0", huge.as_str()] {
993 let err = CssBorderRadiusParseError::TooManyValues(input);
994 let owned = err.to_contained();
995 assert_eq!(owned.to_shared(), err);
996 match owned {
997 CssBorderRadiusParseErrorOwned::TooManyValues(s) => {
998 assert_eq!(s.as_str(), input);
999 }
1000 CssBorderRadiusParseErrorOwned::PixelValue(_) => panic!("wrong variant"),
1001 }
1002 }
1003 }
1004
1005 #[test]
1006 fn shorthand_errors_from_the_parser_round_trip() {
1007 for input in ["", "1px 2px 3px 4px 5px", "1px bad 3px", "px", "\u{1F600}"] {
1010 let err = parse_style_border_radius(input).unwrap_err();
1011 let owned = err.to_contained();
1012 assert_eq!(owned.to_shared(), err);
1013 }
1014 }
1015
1016 #[test]
1017 fn error_debug_delegates_to_display() {
1018 let err = CssBorderRadiusParseError::TooManyValues("a b c d e");
1019 assert_eq!(format!("{err:?}"), format!("{err}"));
1020 assert!(format!("{err}").contains("a b c d e"));
1021
1022 let inner = CssBorderRadiusParseError::PixelValue(CssPixelValueParseError::EmptyString);
1023 assert_eq!(
1024 format!("{inner}"),
1025 format!("{}", CssPixelValueParseError::EmptyString)
1026 );
1027 }
1028
1029 #[test]
1030 fn longhand_error_types_round_trip() {
1031 let tl = parse_style_border_top_left_radius("bad").unwrap_err();
1034 assert_eq!(tl.to_contained().to_shared(), tl);
1035
1036 let tr = parse_style_border_top_right_radius("").unwrap_err();
1037 assert_eq!(tr.to_contained().to_shared(), tr);
1038
1039 let bl = parse_style_border_bottom_left_radius("px").unwrap_err();
1040 assert_eq!(bl.to_contained().to_shared(), bl);
1041
1042 let br = parse_style_border_bottom_right_radius("\u{1F600}").unwrap_err();
1043 assert_eq!(br.to_contained().to_shared(), br);
1044
1045 assert_eq!(
1047 tl.to_contained(),
1048 StyleBorderTopLeftRadiusParseErrorOwned::PixelValue(
1049 CssPixelValueParseError::InvalidPixelValue("bad").to_contained()
1050 )
1051 );
1052 }
1053
1054 #[test]
1055 fn owned_error_newtype_wrapper_preserves_the_inner_error() {
1056 let owned = CssBorderRadiusParseError::TooManyValues("1 2 3 4 5").to_contained();
1057 let wrapped = CssStyleBorderRadiusParseErrorOwned::from(owned.clone());
1058 assert_eq!(wrapped.inner, owned);
1059 assert_eq!(wrapped.inner.to_shared().to_contained(), owned);
1060 }
1061}