1use alloc::string::{String, ToString};
8
9use crate::{
10 props::{
11 basic::{
12 length::{parse_float_value, FloatValue},
13 pixel::{parse_pixel_value, CssPixelValueParseError, PixelValue},
14 },
15 formatter::PrintAsCssValue,
16 },
17 shape::CssShape,
18};
19#[allow(variant_size_differences)]
20#[derive(Debug, Clone, PartialEq)]
23#[repr(C, u8)]
24#[derive(Default)]
25pub enum ShapeOutside {
26 #[default]
27 None,
28 Shape(CssShape),
29}
30
31impl Eq for ShapeOutside {}
32impl core::hash::Hash for ShapeOutside {
33 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
34 core::mem::discriminant(self).hash(state);
35 if let Self::Shape(s) = self {
36 s.hash(state);
37 }
38 }
39}
40impl PartialOrd for ShapeOutside {
41 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
42 Some(self.cmp(other))
43 }
44}
45impl Ord for ShapeOutside {
46 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
47 match (self, other) {
48 (Self::None, Self::None) => core::cmp::Ordering::Equal,
49 (Self::None, Self::Shape(_)) => core::cmp::Ordering::Less,
50 (Self::Shape(_), Self::None) => core::cmp::Ordering::Greater,
51 (Self::Shape(a), Self::Shape(b)) => a.cmp(b),
52 }
53 }
54}
55
56impl PrintAsCssValue for ShapeOutside {
57 fn print_as_css_value(&self) -> String {
58 match self {
59 Self::None => "none".to_string(),
60 Self::Shape(shape) => shape.print_as_css_value(),
61 }
62 }
63}
64#[allow(variant_size_differences)]
65#[derive(Debug, Clone, PartialEq)]
68#[repr(C, u8)]
69#[derive(Default)]
70pub enum ShapeInside {
71 #[default]
72 None,
73 Shape(CssShape),
74}
75
76impl Eq for ShapeInside {}
77impl core::hash::Hash for ShapeInside {
78 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
79 core::mem::discriminant(self).hash(state);
80 if let Self::Shape(s) = self {
81 s.hash(state);
82 }
83 }
84}
85impl PartialOrd for ShapeInside {
86 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
87 Some(self.cmp(other))
88 }
89}
90impl Ord for ShapeInside {
91 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
92 match (self, other) {
93 (Self::None, Self::None) => core::cmp::Ordering::Equal,
94 (Self::None, Self::Shape(_)) => core::cmp::Ordering::Less,
95 (Self::Shape(_), Self::None) => core::cmp::Ordering::Greater,
96 (Self::Shape(a), Self::Shape(b)) => a.cmp(b),
97 }
98 }
99}
100
101impl PrintAsCssValue for ShapeInside {
102 fn print_as_css_value(&self) -> String {
103 match self {
104 Self::None => "none".to_string(),
105 Self::Shape(shape) => shape.print_as_css_value(),
106 }
107 }
108}
109#[allow(variant_size_differences)]
110#[derive(Debug, Clone, PartialEq)]
113#[repr(C, u8)]
114#[derive(Default)]
115pub enum ClipPath {
116 #[default]
117 None,
118 Shape(CssShape),
119}
120
121impl Eq for ClipPath {}
122impl core::hash::Hash for ClipPath {
123 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
124 core::mem::discriminant(self).hash(state);
125 if let Self::Shape(s) = self {
126 s.hash(state);
127 }
128 }
129}
130impl PartialOrd for ClipPath {
131 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
132 Some(self.cmp(other))
133 }
134}
135impl Ord for ClipPath {
136 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
137 match (self, other) {
138 (Self::None, Self::None) => core::cmp::Ordering::Equal,
139 (Self::None, Self::Shape(_)) => core::cmp::Ordering::Less,
140 (Self::Shape(_), Self::None) => core::cmp::Ordering::Greater,
141 (Self::Shape(a), Self::Shape(b)) => a.cmp(b),
142 }
143 }
144}
145
146impl PrintAsCssValue for ClipPath {
147 fn print_as_css_value(&self) -> String {
148 match self {
149 Self::None => "none".to_string(),
150 Self::Shape(shape) => shape.print_as_css_value(),
151 }
152 }
153}
154
155#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
157#[repr(C)]
158pub struct ShapeMargin {
159 pub inner: PixelValue,
160}
161
162impl Default for ShapeMargin {
163 fn default() -> Self {
164 Self {
165 inner: PixelValue::zero(),
166 }
167 }
168}
169
170impl PrintAsCssValue for ShapeMargin {
171 fn print_as_css_value(&self) -> String {
172 self.inner.print_as_css_value()
173 }
174}
175
176#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
178#[repr(C)]
179pub struct ShapeImageThreshold {
180 pub inner: FloatValue,
181}
182
183impl Default for ShapeImageThreshold {
184 fn default() -> Self {
185 Self {
186 inner: FloatValue::const_new(0),
187 }
188 }
189}
190
191impl PrintAsCssValue for ShapeImageThreshold {
192 fn print_as_css_value(&self) -> String {
193 self.inner.to_string()
194 }
195}
196
197impl crate::codegen::format::FormatAsRustCode for ShapeOutside {
199 fn format_as_rust_code(&self, _tabs: usize) -> String {
200 match self {
201 Self::None => String::from("ShapeOutside::None"),
202 Self::Shape(s) => {
203 let mut r = String::from("ShapeOutside::Shape(");
204 r.push_str(&s.format_as_rust_code());
205 r.push(')');
206 r
207 }
208 }
209 }
210}
211
212impl crate::codegen::format::FormatAsRustCode for ShapeInside {
213 fn format_as_rust_code(&self, _tabs: usize) -> String {
214 match self {
215 Self::None => String::from("ShapeInside::None"),
216 Self::Shape(s) => {
217 let mut r = String::from("ShapeInside::Shape(");
218 r.push_str(&s.format_as_rust_code());
219 r.push(')');
220 r
221 }
222 }
223 }
224}
225
226impl crate::codegen::format::FormatAsRustCode for ClipPath {
227 fn format_as_rust_code(&self, _tabs: usize) -> String {
228 match self {
229 Self::None => String::from("ClipPath::None"),
230 Self::Shape(s) => {
231 let mut r = String::from("ClipPath::Shape(");
232 r.push_str(&s.format_as_rust_code());
233 r.push(')');
234 r
235 }
236 }
237 }
238}
239
240impl crate::codegen::format::FormatAsRustCode for ShapeMargin {
241 fn format_as_rust_code(&self, _tabs: usize) -> String {
242 format!(
243 "ShapeMargin {{ inner: {} }}",
244 crate::codegen::format::format_pixel_value(&self.inner)
245 )
246 }
247}
248
249impl crate::codegen::format::FormatAsRustCode for ShapeImageThreshold {
250 fn format_as_rust_code(&self, _tabs: usize) -> String {
251 format!(
252 "ShapeImageThreshold {{ inner: {} }}",
253 crate::codegen::format::format_float_value(&self.inner)
254 )
255 }
256}
257
258#[cfg(feature = "parser")]
260pub mod parser {
261 use core::num::ParseFloatError;
262
263 #[allow(clippy::wildcard_imports)]
264 use super::*;
266 use crate::shape_parser::{parse_shape, ShapeParseError};
267
268 pub fn parse_shape_outside(input: &str) -> Result<ShapeOutside, ShapeParseError> {
273 let trimmed = input.trim();
274 if trimmed == "none" {
275 Ok(ShapeOutside::None)
276 } else {
277 let shape = parse_shape(trimmed)?;
278 Ok(ShapeOutside::Shape(shape))
279 }
280 }
281
282 pub fn parse_shape_inside(input: &str) -> Result<ShapeInside, ShapeParseError> {
287 let trimmed = input.trim();
288 if trimmed == "none" {
289 Ok(ShapeInside::None)
290 } else {
291 let shape = parse_shape(trimmed)?;
292 Ok(ShapeInside::Shape(shape))
293 }
294 }
295
296 pub fn parse_clip_path(input: &str) -> Result<ClipPath, ShapeParseError> {
301 let trimmed = input.trim();
302 if trimmed == "none" {
303 Ok(ClipPath::None)
304 } else {
305 let shape = parse_shape(trimmed)?;
306 Ok(ClipPath::Shape(shape))
307 }
308 }
309
310 pub fn parse_shape_margin(input: &str) -> Result<ShapeMargin, CssPixelValueParseError<'_>> {
315 Ok(ShapeMargin {
316 inner: parse_pixel_value(input)?,
317 })
318 }
319
320 pub fn parse_shape_image_threshold(
325 input: &str,
326 ) -> Result<ShapeImageThreshold, ParseFloatError> {
327 let val = parse_float_value(input)?;
328 let clamped = val.get().clamp(0.0, 1.0);
330 Ok(ShapeImageThreshold {
331 inner: FloatValue::new(clamped),
332 })
333 }
334}
335
336#[cfg(feature = "parser")]
337pub use parser::*;
338
339#[cfg(all(test, feature = "parser"))]
340mod tests {
341 #![allow(clippy::float_cmp)]
343 use super::*;
344
345 #[test]
346 fn test_parse_shape_properties() {
347 assert!(matches!(
349 parse_shape_outside("none").unwrap(),
350 ShapeOutside::None
351 ));
352 assert!(matches!(
353 parse_shape_outside("circle(50px)").unwrap(),
354 ShapeOutside::Shape(_)
355 ));
356
357 assert!(matches!(
359 parse_shape_inside("none").unwrap(),
360 ShapeInside::None
361 ));
362 assert!(matches!(
363 parse_shape_inside("circle(100px at 50px 50px)").unwrap(),
364 ShapeInside::Shape(_)
365 ));
366
367 assert!(matches!(parse_clip_path("none").unwrap(), ClipPath::None));
369 assert!(matches!(
370 parse_clip_path("polygon(0 0, 100px 0, 100px 100px, 0 100px)").unwrap(),
371 ClipPath::Shape(_)
372 ));
373
374 assert_eq!(
376 parse_shape_margin("10px").unwrap().inner,
377 PixelValue::px(10.0)
378 );
379 assert_eq!(parse_shape_image_threshold("0.5").unwrap().inner.get(), 0.5);
380 }
381}
382
383#[cfg(all(test, feature = "parser"))]
384mod autotest_generated {
385 #![allow(clippy::float_cmp, clippy::eq_op)]
397
398 use core::{cmp::Ordering, hash::Hash};
399
400 use super::*;
401 use crate::{
402 corety::OptionF32,
403 props::basic::length::SizeMetric,
404 shape::{ShapeCircle, ShapeEllipse, ShapeInset, ShapePath, ShapePolygon},
405 shape_parser::ShapeParseError,
406 };
407
408 const KNOWN_PANIC_INPUTS: &[&str] = &["path(\")", "path( \" )"];
414
415 fn is_known_panic(input: &str) -> bool {
416 KNOWN_PANIC_INPUTS.contains(&input)
417 }
418
419 fn nasty_corpus() -> Vec<String> {
423 let mut corpus: Vec<String> = [
424 "",
426 " ",
427 " ",
428 "\t",
429 "\n",
430 "\r\n",
431 "\t \n ",
432 "\u{a0}",
433 "none",
435 " none ",
436 "NONE",
437 "None",
438 "nonee",
439 "none none",
440 "none;",
441 "(",
443 ")",
444 "()",
445 ")(",
446 "((",
447 "))",
448 "(()",
449 "())",
450 "!@#$%^&*",
451 ";;;;",
452 ",,,,",
453 "\0",
454 "\0\0(\0)\0",
455 "\u{7f}",
456 "circle",
458 "circle(",
459 "circle)",
460 "circle()",
461 "circle( )",
462 "circle(50px",
463 "circle 50px)",
464 "circle(50px))",
465 "((circle(50px)))",
466 "unknown(50px)",
467 "square(1px)",
468 "(50px)",
469 " (50px) ",
470 "circle(;)",
471 "circle(,)",
472 "circle(50px);garbage",
474 "circle(50px)garbage",
475 "junk circle(50px)",
476 "circle(50px) circle(50px)",
477 "circle(0)",
479 "circle(-0)",
480 "circle(0px)",
481 "circle(-0px)",
482 "circle(-50px)",
483 "circle(50)",
484 "circle(50%)",
485 "circle(NaN)",
486 "circle(nan)",
487 "circle(inf)",
488 "circle(-inf)",
489 "circle(infpx)",
490 "circle(NaNpx)",
491 "circle(1e400px)",
492 "circle(-1e400)",
493 "circle(9223372036854775807px)",
494 "circle(-9223372036854775808)",
495 "circle(0x10px)",
496 "circle(1_000px)",
497 "circle(+5px)",
498 "circle(.5px)",
499 "circle(5.px)",
500 "circle(50px at)",
502 "circle(50px at 1px)",
503 "circle(50px at 1px 2px 3px)",
504 "circle(50px AT 1px 2px)",
505 "ellipse()",
506 "ellipse(1px)",
507 "ellipse(1px 2px)",
508 "ellipse(1px 2px at 3px 4px)",
509 "ellipse(a b)",
510 "polygon()",
511 "polygon(,)",
512 "polygon(0 0)",
513 "polygon(0 0, 1 1)",
514 "polygon(0 0, 1 1, 2 2)",
515 "polygon(0 0, 1 1, 2 2,)",
516 "polygon(nonzero,)",
517 "polygon(nonzero, 0 0, 1 1, 2 2)",
518 "polygon(evenodd,0 0,1 1,2 2)",
519 "polygon(nonzero 0 0, 1 1, 2 2)",
520 "polygon(0, 1, 2)",
521 "polygon(x y, x y, x y)",
522 "inset()",
523 "inset( )",
524 "inset(10px)",
525 "inset(1px 2px 3px 4px 5px)",
526 "inset(round)",
527 "inset(round 5px)",
528 "inset(10px round)",
529 "inset(10px round 5px)",
530 "inset(10px round 5px 6px)",
531 "inset(roundround)",
532 "path()",
533 "path(abc)",
534 "path(\"\")",
535 "path(\"M 0 0 Z\")",
536 "path(\"\"\")",
537 "path(\"🙂\")",
538 "🙂",
541 "🙂(1px)",
542 "circle(🙂)",
543 "circle(🙂px)",
544 "circle(1px at 🙂 🙂)",
545 "cïrcle(1px)",
546 "e\u{0301}(1px)",
547 "\u{202e}circle(1px)",
548 "circle(١٢٣px)",
549 "\u{1f600}\u{1f600}(\u{1f600})",
550 ]
551 .iter()
552 .map(|s| (*s).to_string())
553 .collect();
554
555 for panicking in KNOWN_PANIC_INPUTS {
556 corpus.push((*panicking).to_string());
557 }
558
559 corpus.push("a".repeat(1_000_000));
561 corpus.push("(".repeat(100_000));
562 corpus.push(format!("circle({}px)", "9".repeat(10_000)));
563 corpus.push("circle(".repeat(10_000) + &")".repeat(10_000));
564 corpus.push(format!("polygon({}1px 1px)", "1px 1px, ".repeat(10_000)));
565 corpus.push(format!("path(\"{}\")", "M 0 0 ".repeat(10_000)));
566
567 corpus
568 }
569
570 fn hash_of<T: Hash>(value: &T) -> u64 {
571 use core::hash::Hasher;
572 use std::collections::hash_map::DefaultHasher;
573
574 let mut hasher = DefaultHasher::new();
575 value.hash(&mut hasher);
576 hasher.finish()
577 }
578
579 fn clip_shape(input: &str) -> CssShape {
580 match parse_clip_path(input) {
581 Ok(ClipPath::Shape(shape)) => shape,
582 other => panic!("expected a shape for {input:?}, got {other:?}"),
583 }
584 }
585
586 #[test]
604 fn path_lone_quote_returns_err_not_panic() {
605 for input in KNOWN_PANIC_INPUTS {
606 let outcome =
607 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| parse_clip_path(input)));
608 match outcome {
609 Ok(res) => assert!(res.is_err(), "{input:?} must be rejected, got {res:?}"),
610 Err(_) => {
611 panic!("parse_clip_path({input:?}) still panics — the slice bug regressed")
612 }
613 }
614 }
615 }
616
617 #[test]
630 fn known_bug_vmin_unit_is_rejected_by_metric_table_order() {
631 assert_eq!(
633 parse_shape_margin("10vmin").unwrap().inner.metric,
634 SizeMetric::Vmin
635 );
636
637 assert_eq!(
640 parse_shape_margin("10vmax").unwrap().inner.metric,
641 SizeMetric::Vmax
642 );
643 assert_eq!(
644 parse_shape_margin("10vw").unwrap().inner.metric,
645 SizeMetric::Vw
646 );
647 assert_eq!(
648 parse_shape_margin("10vh").unwrap().inner.metric,
649 SizeMetric::Vh
650 );
651 }
652
653 #[test]
659 fn nan_shape_is_reflexive_and_consistent_across_eq_ord_hash() {
660 let a = parse_clip_path("circle(NaN)").expect("NaN is a preserved length");
661 let b = parse_clip_path("circle(NaN)").expect("NaN is a preserved length");
662 assert_eq!(a, a, "Eq must be reflexive for a NaN shape");
663 assert_eq!(a, b);
664 assert_eq!(a.cmp(&b), Ordering::Equal);
665 assert_eq!(a.partial_cmp(&b), Some(Ordering::Equal));
666 assert_eq!(hash_of(&a), hash_of(&b));
667 }
668
669 #[test]
674 fn floatvalue_encoding_makes_margin_and_threshold_nan_free() {
675 let threshold = parse_shape_image_threshold("NaN").expect("f32 parses NaN");
676 assert!(threshold.inner.get().is_finite());
677 assert_eq!(threshold.inner.get(), 0.0);
678 assert_eq!(threshold, threshold);
679 assert_eq!(hash_of(&threshold), hash_of(&threshold));
680
681 if let Ok(margin) = parse_shape_margin("NaNpx") {
684 assert!(
685 margin.inner.number.get().is_finite(),
686 "NaN leaked into a PixelValue"
687 );
688 assert_eq!(margin.inner.number.get(), 0.0);
689 assert_eq!(margin, margin);
690 } else { }
692 }
693
694 #[test]
703 fn shape_parsers_never_panic_on_hostile_input() {
704 let mut unexpected: Vec<String> = Vec::new();
705
706 for input in nasty_corpus() {
707 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
708 let _ = parse_shape_outside(&input);
709 let _ = parse_shape_inside(&input);
710 let _ = parse_clip_path(&input);
711 }));
712
713 if outcome.is_err() && !is_known_panic(&input) {
714 let preview: String = input.chars().take(48).collect();
715 unexpected.push(preview);
716 }
717 }
718
719 assert!(
720 unexpected.is_empty(),
721 "shape parsers panicked on input(s) outside the known-bug set: \
722 {unexpected:?}"
723 );
724 }
725
726 #[test]
729 fn numeric_parsers_never_panic_on_hostile_input() {
730 for input in nasty_corpus() {
731 let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
732 let _ = parse_shape_margin(&input);
733 let _ = parse_shape_image_threshold(&input);
734 }));
735 assert!(
736 outcome.is_ok(),
737 "numeric parser panicked on {:?}",
738 input.chars().take(48).collect::<String>()
739 );
740 }
741 }
742
743 #[test]
748 fn the_three_shape_properties_agree_on_every_input() {
749 for input in nasty_corpus() {
750 if is_known_panic(&input) {
751 continue;
752 }
753
754 let outside = parse_shape_outside(&input);
755 let inside = parse_shape_inside(&input);
756 let clip = parse_clip_path(&input);
757
758 assert_eq!(
759 outside.is_ok(),
760 inside.is_ok(),
761 "shape-outside and shape-inside disagree on {input:?}"
762 );
763 assert_eq!(
764 outside.is_ok(),
765 clip.is_ok(),
766 "shape-outside and clip-path disagree on {input:?}"
767 );
768
769 if let (Ok(ShapeOutside::Shape(a)), Ok(ShapeInside::Shape(b)), Ok(ClipPath::Shape(c))) =
770 (&outside, &inside, &clip)
771 {
772 assert_eq!(a.cmp(b), Ordering::Equal, "different shape for {input:?}");
773 assert_eq!(a.cmp(c), Ordering::Equal, "different shape for {input:?}");
774 }
775 }
776 }
777
778 #[test]
783 fn pathological_sizes_terminate_without_stack_overflow() {
784 let million = "a".repeat(1_000_000);
785 assert!(matches!(
786 parse_clip_path(&million),
787 Err(ShapeParseError::InvalidSyntax(_))
788 ));
789
790 let nested = "circle(".repeat(10_000) + &")".repeat(10_000);
791 assert!(parse_clip_path(&nested).is_err());
792
793 let unclosed = "(".repeat(100_000);
794 assert!(matches!(
795 parse_clip_path(&unclosed),
796 Err(ShapeParseError::InvalidSyntax(_))
797 ));
798
799 let big_polygon = format!("polygon({}1px 1px)", "1px 1px, ".repeat(10_000));
800 match clip_shape(&big_polygon) {
801 CssShape::Polygon(ShapePolygon { points }) => {
802 assert_eq!(points.as_ref().len(), 10_001);
803 }
804 other => panic!("expected Polygon, got {other:?}"),
805 }
806
807 let huge_radius = format!("circle({}px)", "9".repeat(10_000));
810 match clip_shape(&huge_radius) {
811 CssShape::Circle(ShapeCircle { radius, .. }) => {
812 assert!(radius.is_infinite() && radius.is_sign_positive());
813 }
814 other => panic!("expected Circle, got {other:?}"),
815 }
816 }
817
818 #[test]
823 fn empty_and_whitespace_only_input_is_empty_input_error() {
824 for input in ["", " ", " ", "\t", "\n", "\r\n", "\t \n ", "\u{a0}"] {
825 assert_eq!(
826 parse_shape_outside(input),
827 Err(ShapeParseError::EmptyInput),
828 "shape-outside {input:?}"
829 );
830 assert_eq!(
831 parse_shape_inside(input),
832 Err(ShapeParseError::EmptyInput),
833 "shape-inside {input:?}"
834 );
835 assert_eq!(
836 parse_clip_path(input),
837 Err(ShapeParseError::EmptyInput),
838 "clip-path {input:?}"
839 );
840 }
841 }
842
843 #[test]
845 fn none_keyword_parses_and_is_trimmed() {
846 for input in ["none", " none ", "\tnone\n", " none\r\n"] {
847 assert_eq!(parse_shape_outside(input), Ok(ShapeOutside::None));
848 assert_eq!(parse_shape_inside(input), Ok(ShapeInside::None));
849 assert_eq!(parse_clip_path(input), Ok(ClipPath::None));
850 }
851 }
852
853 #[test]
857 fn uppercase_none_never_yields_a_shape() {
858 for input in ["NONE", "None", "nOnE"] {
859 match parse_clip_path(input) {
860 Ok(ClipPath::None) | Err(_) => {}
861 Ok(other) => panic!("{input:?} parsed as a shape: {other:?}"),
862 }
863 }
864 assert!(parse_clip_path("NONE").is_err());
866 }
867
868 #[test]
869 fn garbage_and_broken_parens_are_rejected() {
870 for input in ["!@#$%^&*", ";;;;", ",,,,", "circle", "\u{7f}", "🙂"] {
872 assert!(
873 matches!(
874 parse_clip_path(input),
875 Err(ShapeParseError::InvalidSyntax(_))
876 ),
877 "expected InvalidSyntax for {input:?}"
878 );
879 }
880
881 assert!(matches!(
883 parse_clip_path("circle(50px"),
884 Err(ShapeParseError::InvalidSyntax(_))
885 ));
886 assert!(matches!(
888 parse_clip_path(")("),
889 Err(ShapeParseError::InvalidSyntax(_))
890 ));
891 assert!(matches!(
893 parse_clip_path("()"),
894 Err(ShapeParseError::UnknownFunction(_))
895 ));
896 assert_eq!(
898 parse_clip_path("square(1px)"),
899 Err(ShapeParseError::UnknownFunction("square".to_string()))
900 );
901 assert_eq!(
902 parse_clip_path("junk circle(50px)"),
903 Err(ShapeParseError::UnknownFunction("junk circle".to_string()))
904 );
905 }
906
907 #[test]
917 fn trailing_junk_is_ignored_but_never_changes_the_shape() {
918 for input in [
919 "circle(50px);garbage",
920 "circle(50px)garbage",
921 "circle(50px)🙂",
922 ] {
923 match parse_clip_path(input) {
924 Ok(ClipPath::Shape(CssShape::Circle(ShapeCircle { center, radius }))) => {
925 assert_eq!(radius, 50.0, "{input:?}");
926 assert_eq!(center.x, 0.0);
927 assert_eq!(center.y, 0.0);
928 }
929 Err(_) => { }
930 other => panic!("{input:?} produced an unexpected value: {other:?}"),
931 }
932 }
933 }
934
935 #[test]
939 fn multibyte_input_does_not_panic_and_is_rejected() {
940 for input in [
941 "🙂(1px)",
942 "circle(🙂)",
943 "circle(🙂px)",
944 "circle(1px at 🙂 🙂)",
945 "cïrcle(1px)",
946 "e\u{0301}(1px)",
947 "\u{202e}circle(1px)",
948 "circle(١٢٣px)",
949 "\u{1f600}\u{1f600}(\u{1f600})",
950 ] {
951 assert!(
952 parse_clip_path(input).is_err(),
953 "expected Err for {input:?}"
954 );
955 }
956
957 match clip_shape("path(\"🙂\")") {
959 CssShape::Path(ShapePath { data }) => assert_eq!(data.as_str(), "🙂"),
960 other => panic!("expected Path, got {other:?}"),
961 }
962 }
963
964 #[test]
965 fn circle_boundary_numbers() {
966 for input in ["circle(50px)", "circle(50)", "circle(50%)"] {
969 match clip_shape(input) {
970 CssShape::Circle(ShapeCircle { radius, .. }) => assert_eq!(radius, 50.0),
971 other => panic!("expected Circle, got {other:?}"),
972 }
973 }
974
975 for (input, expected) in [
978 ("circle(0px)", 0.0_f32),
979 ("circle(-0px)", -0.0_f32),
980 ("circle(-50px)", -50.0_f32),
981 ("circle(+5px)", 5.0_f32),
982 ("circle(.5px)", 0.5_f32),
983 ("circle(5.px)", 5.0_f32),
984 ] {
985 match clip_shape(input) {
986 CssShape::Circle(ShapeCircle { radius, .. }) => {
987 assert_eq!(radius, expected, "{input:?}");
988 }
989 other => panic!("expected Circle, got {other:?}"),
990 }
991 }
992
993 for input in ["circle(1e400px)", "circle(inf)", "circle(infpx)"] {
995 match clip_shape(input) {
996 CssShape::Circle(ShapeCircle { radius, .. }) => {
997 assert!(radius.is_infinite(), "{input:?} -> {radius}");
998 }
999 other => panic!("expected Circle, got {other:?}"),
1000 }
1001 }
1002
1003 match clip_shape("circle(9223372036854775807px)") {
1005 CssShape::Circle(ShapeCircle { radius, .. }) => {
1006 assert!(radius.is_finite() && radius > 9.0e18);
1007 }
1008 other => panic!("expected Circle, got {other:?}"),
1009 }
1010
1011 for input in ["circle(0x10px)", "circle(1_000px)"] {
1013 assert!(
1014 matches!(
1015 parse_clip_path(input),
1016 Err(ShapeParseError::InvalidNumber(_))
1017 ),
1018 "expected InvalidNumber for {input:?}"
1019 );
1020 }
1021 }
1022
1023 #[test]
1027 fn circle_at_clause_arity_falls_back_to_origin() {
1028 for input in [
1029 "circle(50px at)",
1030 "circle(50px at 1px)",
1031 "circle(50px AT 1px 2px)",
1032 ] {
1033 match clip_shape(input) {
1034 CssShape::Circle(ShapeCircle { center, radius }) => {
1035 assert_eq!(radius, 50.0);
1036 assert_eq!((center.x, center.y), (0.0, 0.0), "{input:?}");
1037 }
1038 other => panic!("expected Circle, got {other:?}"),
1039 }
1040 }
1041
1042 for input in ["circle(50px at 1px 2px)", "circle(50px at 1px 2px 3px)"] {
1044 match clip_shape(input) {
1045 CssShape::Circle(ShapeCircle { center, radius }) => {
1046 assert_eq!(radius, 50.0);
1047 assert_eq!((center.x, center.y), (1.0, 2.0), "{input:?}");
1048 }
1049 other => panic!("expected Circle, got {other:?}"),
1050 }
1051 }
1052
1053 assert!(matches!(
1054 parse_clip_path("circle()"),
1055 Err(ShapeParseError::MissingParameter(_))
1056 ));
1057 }
1058
1059 #[test]
1060 fn ellipse_requires_two_radii() {
1061 for input in ["ellipse()", "ellipse(1px)"] {
1062 assert!(
1063 matches!(
1064 parse_clip_path(input),
1065 Err(ShapeParseError::MissingParameter(_))
1066 ),
1067 "expected MissingParameter for {input:?}"
1068 );
1069 }
1070
1071 match clip_shape("ellipse(1px 2px)") {
1072 CssShape::Ellipse(ShapeEllipse {
1073 center,
1074 radius_x,
1075 radius_y,
1076 }) => {
1077 assert_eq!((radius_x, radius_y), (1.0, 2.0));
1078 assert_eq!((center.x, center.y), (0.0, 0.0));
1079 }
1080 other => panic!("expected Ellipse, got {other:?}"),
1081 }
1082
1083 match clip_shape("ellipse(1px 2px at 3px 4px)") {
1084 CssShape::Ellipse(ShapeEllipse {
1085 center,
1086 radius_x,
1087 radius_y,
1088 }) => {
1089 assert_eq!((radius_x, radius_y), (1.0, 2.0));
1090 assert_eq!((center.x, center.y), (3.0, 4.0));
1091 }
1092 other => panic!("expected Ellipse, got {other:?}"),
1093 }
1094
1095 assert!(matches!(
1096 parse_clip_path("ellipse(a b)"),
1097 Err(ShapeParseError::InvalidNumber(_))
1098 ));
1099 }
1100
1101 #[test]
1102 fn polygon_needs_three_complete_points() {
1103 for input in [
1105 "polygon()",
1106 "polygon(,)",
1107 "polygon(0 0)",
1108 "polygon(0 0, 1 1)",
1109 "polygon(0 0, 1 1, 2 2,)",
1110 "polygon(0, 1, 2)",
1111 "polygon(nonzero,)",
1112 ] {
1113 assert!(
1114 parse_clip_path(input).is_err(),
1115 "expected Err for {input:?}"
1116 );
1117 }
1118
1119 match clip_shape("polygon(0 0, 1 1, 2 2)") {
1120 CssShape::Polygon(ShapePolygon { points }) => {
1121 assert_eq!(points.as_ref().len(), 3);
1122 assert_eq!((points.as_ref()[2].x, points.as_ref()[2].y), (2.0, 2.0));
1123 }
1124 other => panic!("expected Polygon, got {other:?}"),
1125 }
1126
1127 for input in [
1130 "polygon(nonzero, 0 0, 1 1, 2 2)",
1131 "polygon(evenodd,0 0,1 1,2 2)",
1132 ] {
1133 match clip_shape(input) {
1134 CssShape::Polygon(ShapePolygon { points }) => {
1135 assert_eq!(points.as_ref().len(), 3, "{input:?}");
1136 }
1137 other => panic!("expected Polygon, got {other:?}"),
1138 }
1139 }
1140 assert!(matches!(
1141 parse_clip_path("polygon(nonzero 0 0, 1 1, 2 2)"),
1142 Err(ShapeParseError::InvalidNumber(_))
1143 ));
1144 assert!(matches!(
1145 parse_clip_path("polygon(x y, x y, x y)"),
1146 Err(ShapeParseError::InvalidNumber(_))
1147 ));
1148 }
1149
1150 #[test]
1151 fn inset_shorthand_and_round_keyword() {
1152 let cases: [(&str, [f32; 4]); 4] = [
1154 ("inset(10px)", [10.0, 10.0, 10.0, 10.0]),
1155 ("inset(1px 2px)", [1.0, 2.0, 1.0, 2.0]),
1156 ("inset(1px 2px 3px)", [1.0, 2.0, 3.0, 2.0]),
1157 ("inset(1px 2px 3px 4px)", [1.0, 2.0, 3.0, 4.0]),
1158 ];
1159 for (input, [top, right, bottom, left]) in cases {
1160 match clip_shape(input) {
1161 CssShape::Inset(ShapeInset {
1162 inset_top,
1163 inset_right,
1164 inset_bottom,
1165 inset_left,
1166 border_radius,
1167 }) => {
1168 assert_eq!(
1169 [inset_top, inset_right, inset_bottom, inset_left],
1170 [top, right, bottom, left],
1171 "{input:?}"
1172 );
1173 assert!(matches!(border_radius, OptionF32::None));
1174 }
1175 other => panic!("expected Inset, got {other:?}"),
1176 }
1177 }
1178
1179 assert!(matches!(
1181 parse_clip_path("inset(1px 2px 3px 4px 5px)"),
1182 Err(ShapeParseError::InvalidSyntax(_))
1183 ));
1184 assert!(matches!(
1185 parse_clip_path("inset()"),
1186 Err(ShapeParseError::MissingParameter(_))
1187 ));
1188 assert!(matches!(
1189 parse_clip_path("inset( )"),
1190 Err(ShapeParseError::MissingParameter(_))
1191 ));
1192
1193 for input in [
1196 "inset(round)",
1197 "inset(10px round)",
1198 "inset(roundround)",
1199 "inset(10px round 5px 6px)",
1200 ] {
1201 assert!(
1202 matches!(
1203 parse_clip_path(input),
1204 Err(ShapeParseError::InvalidNumber(_))
1205 ),
1206 "expected InvalidNumber for {input:?}"
1207 );
1208 }
1209
1210 match clip_shape("inset(10px round 5px)") {
1211 CssShape::Inset(ShapeInset { border_radius, .. }) => {
1212 assert!(matches!(border_radius, OptionF32::Some(r) if r == 5.0));
1213 }
1214 other => panic!("expected Inset, got {other:?}"),
1215 }
1216 }
1217
1218 #[test]
1219 fn path_data_must_be_quoted_and_is_stored_verbatim() {
1220 for input in ["path()", "path(abc)", "path(\"abc)", "path(abc\")"] {
1222 assert!(
1223 matches!(
1224 parse_clip_path(input),
1225 Err(ShapeParseError::InvalidSyntax(_))
1226 ),
1227 "expected InvalidSyntax for {input:?}"
1228 );
1229 }
1230
1231 match clip_shape("path(\"\")") {
1234 CssShape::Path(ShapePath { data }) => assert_eq!(data.as_str(), ""),
1235 other => panic!("expected Path, got {other:?}"),
1236 }
1237
1238 match clip_shape("path(\"M 0 0 (L) 1 1 Z\")") {
1241 CssShape::Path(ShapePath { data }) => {
1242 assert_eq!(data.as_str(), "M 0 0 (L) 1 1 Z");
1243 }
1244 other => panic!("expected Path, got {other:?}"),
1245 }
1246 }
1247
1248 #[test]
1253 fn shape_properties_round_trip_through_their_css_representation() {
1254 let inputs = [
1255 "none",
1256 "circle(50px)",
1257 "circle(50px at 10px 20px)",
1258 "circle(-1px at -2px -3px)",
1259 "ellipse(1px 2px)",
1260 "ellipse(1px 2px at 3px 4px)",
1261 "polygon(0px 0px, 100px 0px, 100px 100px)",
1262 "polygon(0px 0px, 1px 1px, 2px 2px, 3px 3px, 4px 4px)",
1263 "inset(1px 2px 3px 4px)",
1264 "inset(10px round 5px)",
1265 "path(\"M 0 0 L 1 1 Z\")",
1266 ];
1267
1268 for input in inputs {
1269 let outside = parse_shape_outside(input).expect(input);
1270 let printed = outside.print_as_css_value();
1271 assert_eq!(
1272 parse_shape_outside(&printed).as_ref(),
1273 Ok(&outside),
1274 "shape-outside round-trip changed the value: {input:?} -> {printed:?}"
1275 );
1276 assert_eq!(
1278 parse_shape_outside(&printed)
1279 .expect(input)
1280 .print_as_css_value(),
1281 printed
1282 );
1283
1284 let inside = parse_shape_inside(input).expect(input);
1285 let printed = inside.print_as_css_value();
1286 assert_eq!(
1287 parse_shape_inside(&printed).as_ref(),
1288 Ok(&inside),
1289 "{input:?}"
1290 );
1291
1292 let clip = parse_clip_path(input).expect(input);
1293 let printed = clip.print_as_css_value();
1294 assert_eq!(parse_clip_path(&printed).as_ref(), Ok(&clip), "{input:?}");
1295 }
1296 }
1297
1298 #[test]
1299 fn none_prints_as_the_none_keyword() {
1300 assert_eq!(ShapeOutside::None.print_as_css_value(), "none");
1301 assert_eq!(ShapeInside::None.print_as_css_value(), "none");
1302 assert_eq!(ClipPath::None.print_as_css_value(), "none");
1303 }
1304
1305 #[test]
1306 fn margin_and_threshold_round_trip() {
1307 for input in ["0px", "10px", "-5px", "1.5em", "50%", "2rem", "12pt", "1in"] {
1308 let margin = parse_shape_margin(input).expect(input);
1309 let printed = margin.print_as_css_value();
1310 assert_eq!(
1311 parse_shape_margin(&printed).expect(&printed),
1312 margin,
1313 "shape-margin round-trip changed the value: {input:?} -> {printed:?}"
1314 );
1315 }
1316
1317 for input in ["0", "0.5", "1", "0.001", "0.999"] {
1318 let threshold = parse_shape_image_threshold(input).expect(input);
1319 let printed = threshold.print_as_css_value();
1320 assert_eq!(
1321 parse_shape_image_threshold(&printed).expect(&printed),
1322 threshold,
1323 "shape-image-threshold round-trip changed the value: {input:?} -> {printed:?}"
1324 );
1325 }
1326 }
1327
1328 #[test]
1333 fn margin_empty_and_whitespace_only_is_empty_string_error() {
1334 for input in ["", " ", " ", "\t", "\n", "\r\n", "\u{a0}"] {
1335 assert!(
1336 matches!(
1337 parse_shape_margin(input),
1338 Err(CssPixelValueParseError::EmptyString)
1339 ),
1340 "expected EmptyString for {input:?}"
1341 );
1342 }
1343 }
1344
1345 #[test]
1346 fn margin_valid_units_map_to_the_right_metric() {
1347 let cases = [
1350 ("10px", SizeMetric::Px),
1351 ("10em", SizeMetric::Em),
1352 ("10rem", SizeMetric::Rem),
1353 ("10pt", SizeMetric::Pt),
1354 ("10in", SizeMetric::In),
1355 ("10cm", SizeMetric::Cm),
1356 ("10mm", SizeMetric::Mm),
1357 ("10%", SizeMetric::Percent),
1358 ("10vw", SizeMetric::Vw),
1359 ("10vh", SizeMetric::Vh),
1360 ("10vmax", SizeMetric::Vmax),
1361 ("10", SizeMetric::Px),
1363 ];
1364
1365 for (input, metric) in cases {
1366 let margin = parse_shape_margin(input).expect(input);
1367 assert_eq!(margin.inner.metric, metric, "{input:?}");
1368 assert_eq!(margin.inner.number.get(), 10.0, "{input:?}");
1369 }
1370
1371 assert_eq!(
1373 parse_shape_margin(" 10px ").expect("padded").inner,
1374 PixelValue::px(10.0)
1375 );
1376 assert_eq!(
1377 parse_shape_margin("10 px").expect("inner space").inner,
1378 PixelValue::px(10.0)
1379 );
1380 }
1381
1382 #[test]
1383 fn margin_rejects_malformed_and_junk_suffixed_values() {
1384 assert!(matches!(
1386 parse_shape_margin("px"),
1387 Err(CssPixelValueParseError::NoValueGiven(_, SizeMetric::Px))
1388 ));
1389 assert!(matches!(
1391 parse_shape_margin("abcpx"),
1392 Err(CssPixelValueParseError::ValueParseErr(_, "abc"))
1393 ));
1394 for input in [
1396 "10px;garbage",
1397 "garbage",
1398 "10 20px",
1399 "10px 20px",
1400 "10PX",
1401 "10Px",
1402 "10px🙂",
1403 "🙂px",
1404 "!@#$",
1405 ] {
1406 assert!(
1407 parse_shape_margin(input).is_err(),
1408 "expected Err for {input:?}"
1409 );
1410 }
1411 }
1412
1413 #[test]
1417 fn margin_uppercase_units_never_yield_a_wrong_value() {
1418 for input in ["10PX", "10Px", "10EM", "10%"] {
1419 if let Ok(margin) = parse_shape_margin(input) {
1420 assert_eq!(margin.inner.number.get(), 10.0, "{input:?}")
1421 } else { }
1423 }
1424 }
1425
1426 #[test]
1430 fn margin_never_produces_a_non_finite_value() {
1431 let extremes = [
1432 "0px",
1433 "-0px",
1434 "0",
1435 "-0",
1436 "NaNpx",
1437 "nanpx",
1438 "infpx",
1439 "-infpx",
1440 "inf",
1441 "-inf",
1442 "1e400px",
1443 "-1e400px",
1444 "1e38px",
1445 "-1e38px",
1446 "9223372036854775807px",
1447 "-9223372036854775808px",
1448 "340282350000000000000000000000000000000px",
1449 "0.0000000000000000001px",
1450 ];
1451
1452 for input in extremes {
1453 if let Ok(margin) = parse_shape_margin(input) {
1454 let value = margin.inner.number.get();
1455 assert!(
1456 value.is_finite(),
1457 "{input:?} produced a non-finite PixelValue: {value}"
1458 );
1459 }
1460 }
1461
1462 let saturated = parse_shape_margin("1e400px").expect("f32 overflow parses as inf");
1467 assert_eq!(saturated.inner.number.number(), isize::MAX);
1468 assert!(saturated.inner.number.get().is_finite());
1469
1470 let saturated_neg = parse_shape_margin("-1e400px").expect("parses as -inf");
1471 assert_eq!(saturated_neg.inner.number.number(), isize::MIN);
1472 assert!(saturated_neg.inner.number.get().is_finite());
1473
1474 assert_eq!(
1476 parse_shape_margin("NaNpx")
1477 .expect("NaN currently parses")
1478 .inner
1479 .number
1480 .number(),
1481 0
1482 );
1483 }
1484
1485 #[test]
1489 fn margin_quantizes_to_three_decimals_by_truncation() {
1490 assert_eq!(
1491 parse_shape_margin("0.001px")
1492 .expect("0.001")
1493 .inner
1494 .number
1495 .get(),
1496 0.001
1497 );
1498 assert_eq!(
1500 parse_shape_margin("0.0005px")
1501 .expect("0.0005")
1502 .inner
1503 .number
1504 .get(),
1505 0.0
1506 );
1507 assert_eq!(
1508 parse_shape_margin("0.0009px")
1509 .expect("0.0009")
1510 .inner
1511 .number
1512 .get(),
1513 0.0
1514 );
1515
1516 let truncated = parse_shape_margin("1.9999px")
1517 .expect("1.9999")
1518 .inner
1519 .number
1520 .get();
1521 assert!(
1522 (truncated - 1.999).abs() < 1.0e-6,
1523 "expected truncation to 1.999, got {truncated}"
1524 );
1525 }
1526
1527 #[test]
1532 fn threshold_empty_whitespace_and_garbage_are_errors() {
1533 for input in [
1534 "", " ", " ", "\t\n", "abc", "0.5px", "50%", "1,0", "0.5.5", "🙂", "--1",
1535 ] {
1536 assert!(
1537 parse_shape_image_threshold(input).is_err(),
1538 "expected Err for {input:?}"
1539 );
1540 }
1541 }
1542
1543 #[test]
1544 fn threshold_parses_and_trims_valid_values() {
1545 assert_eq!(
1546 parse_shape_image_threshold("0").expect("0").inner.get(),
1547 0.0
1548 );
1549 assert_eq!(
1550 parse_shape_image_threshold("1").expect("1").inner.get(),
1551 1.0
1552 );
1553 assert_eq!(
1554 parse_shape_image_threshold("0.5").expect("0.5").inner.get(),
1555 0.5
1556 );
1557 assert_eq!(
1558 parse_shape_image_threshold(" 0.5 ")
1559 .expect("padded")
1560 .inner
1561 .get(),
1562 0.5
1563 );
1564 assert_eq!(
1566 parse_shape_image_threshold("+0.5")
1567 .expect("+0.5")
1568 .inner
1569 .get(),
1570 0.5
1571 );
1572 assert_eq!(
1573 parse_shape_image_threshold("5e-1")
1574 .expect("5e-1")
1575 .inner
1576 .get(),
1577 0.5
1578 );
1579 assert_eq!(
1580 parse_shape_image_threshold(".5").expect(".5").inner.get(),
1581 0.5
1582 );
1583 }
1584
1585 #[test]
1589 fn threshold_is_always_clamped_to_zero_one_and_finite() {
1590 let extremes = [
1591 "0",
1592 "-0",
1593 "1",
1594 "-1",
1595 "2",
1596 "1.0001",
1597 "-0.0001",
1598 "100",
1599 "1e10",
1600 "-1e10",
1601 "1e38",
1602 "1e400",
1603 "-1e400",
1604 "inf",
1605 "-inf",
1606 "infinity",
1607 "-infinity",
1608 "NaN",
1609 "nan",
1610 "-NaN",
1611 "9223372036854775807",
1612 "-9223372036854775808",
1613 "1e-45",
1614 "-1e-45",
1615 "0.0000001",
1616 ];
1617
1618 for input in extremes {
1619 let Ok(threshold) = parse_shape_image_threshold(input) else {
1620 continue;
1621 };
1622 let value = threshold.inner.get();
1623 assert!(
1624 value.is_finite(),
1625 "{input:?} produced a non-finite threshold: {value}"
1626 );
1627 assert!(
1628 (0.0..=1.0).contains(&value),
1629 "{input:?} escaped the [0, 1] clamp: {value}"
1630 );
1631 }
1632
1633 assert_eq!(
1635 parse_shape_image_threshold("2").expect("2").inner.get(),
1636 1.0
1637 );
1638 assert_eq!(
1639 parse_shape_image_threshold("-1").expect("-1").inner.get(),
1640 0.0
1641 );
1642 assert_eq!(
1643 parse_shape_image_threshold("inf").expect("inf").inner.get(),
1644 1.0
1645 );
1646 assert_eq!(
1647 parse_shape_image_threshold("-inf")
1648 .expect("-inf")
1649 .inner
1650 .get(),
1651 0.0
1652 );
1653 assert_eq!(
1657 parse_shape_image_threshold("NaN").expect("NaN").inner.get(),
1658 0.0
1659 );
1660 }
1661
1662 #[test]
1665 fn threshold_quantizes_to_three_decimals() {
1666 assert_eq!(
1667 parse_shape_image_threshold("0.001")
1668 .expect("0.001")
1669 .inner
1670 .get(),
1671 0.001
1672 );
1673 assert_eq!(
1674 parse_shape_image_threshold("0.0005")
1675 .expect("0.0005")
1676 .inner
1677 .get(),
1678 0.0
1679 );
1680 let truncated = parse_shape_image_threshold("0.9999")
1681 .expect("0.9999")
1682 .inner
1683 .get();
1684 assert!(
1685 (truncated - 0.999).abs() < 1.0e-6,
1686 "expected truncation to 0.999, got {truncated}"
1687 );
1688 }
1689
1690 #[test]
1691 fn threshold_survives_a_ten_thousand_digit_number() {
1692 let huge = "9".repeat(10_000);
1693 assert_eq!(
1694 parse_shape_image_threshold(&huge)
1695 .expect("overflows to inf, clamps to 1")
1696 .inner
1697 .get(),
1698 1.0
1699 );
1700
1701 let tiny = format!("0.{}1", "0".repeat(10_000));
1702 assert_eq!(
1703 parse_shape_image_threshold(&tiny)
1704 .expect("underflows to 0")
1705 .inner
1706 .get(),
1707 0.0
1708 );
1709 }
1710
1711 #[test]
1716 fn defaults_are_none_and_zero() {
1717 assert_eq!(ShapeOutside::default(), ShapeOutside::None);
1718 assert_eq!(ShapeInside::default(), ShapeInside::None);
1719 assert_eq!(ClipPath::default(), ClipPath::None);
1720 assert_eq!(ShapeMargin::default().inner, PixelValue::zero());
1721 assert_eq!(ShapeMargin::default().inner.number.get(), 0.0);
1722 assert_eq!(ShapeImageThreshold::default().inner.get(), 0.0);
1723
1724 assert_eq!(parse_clip_path("none").expect("none"), ClipPath::default());
1726 assert_eq!(
1727 parse_shape_margin("0px").expect("0px"),
1728 ShapeMargin::default()
1729 );
1730 assert_eq!(
1731 parse_shape_image_threshold("0").expect("0"),
1732 ShapeImageThreshold::default()
1733 );
1734 }
1735
1736 #[test]
1739 fn none_sorts_before_shape_and_ord_agrees_with_partial_ord() {
1740 let shape_clip = parse_clip_path("circle(1px)").expect("circle");
1741 let shape_out = parse_shape_outside("circle(1px)").expect("circle");
1742 let shape_in = parse_shape_inside("circle(1px)").expect("circle");
1743
1744 assert_eq!(ClipPath::None.cmp(&shape_clip), Ordering::Less);
1745 assert_eq!(shape_clip.cmp(&ClipPath::None), Ordering::Greater);
1746 assert_eq!(ClipPath::None.cmp(&ClipPath::None), Ordering::Equal);
1747 assert_eq!(
1748 ClipPath::None.partial_cmp(&shape_clip),
1749 Some(ClipPath::None.cmp(&shape_clip))
1750 );
1751
1752 assert_eq!(ShapeOutside::None.cmp(&shape_out), Ordering::Less);
1753 assert_eq!(
1754 ShapeOutside::None.partial_cmp(&shape_out),
1755 Some(Ordering::Less)
1756 );
1757
1758 assert_eq!(ShapeInside::None.cmp(&shape_in), Ordering::Less);
1759 assert_eq!(
1760 ShapeInside::None.partial_cmp(&shape_in),
1761 Some(Ordering::Less)
1762 );
1763 }
1764
1765 #[test]
1770 fn css_shape_variant_ordering_is_antisymmetric() {
1771 let shapes = [
1772 parse_clip_path("circle(1px)").expect("circle"),
1773 parse_clip_path("ellipse(1px 2px)").expect("ellipse"),
1774 parse_clip_path("polygon(0 0, 1 1, 2 2)").expect("polygon"),
1775 parse_clip_path("inset(1px)").expect("inset"),
1776 parse_clip_path("path(\"Z\")").expect("path"),
1777 ];
1778
1779 for (i, a) in shapes.iter().enumerate() {
1780 assert_eq!(a.cmp(a), Ordering::Equal, "variant {i} is not self-equal");
1781
1782 for (j, b) in shapes.iter().enumerate() {
1783 let forward = a.cmp(b);
1784 let backward = b.cmp(a);
1785 assert_eq!(
1786 forward,
1787 backward.reverse(),
1788 "cmp is not antisymmetric for variants {i} and {j}"
1789 );
1790 if i < j {
1791 assert_eq!(
1792 forward,
1793 Ordering::Less,
1794 "variant {i} should sort before variant {j}"
1795 );
1796 }
1797 }
1798 }
1799 }
1800
1801 #[test]
1805 fn hash_agrees_with_equality() {
1806 let a = parse_clip_path("circle(50px at 1px 2px)").expect("circle");
1807 let b = parse_clip_path("circle(50px at 1px 2px)").expect("circle");
1808 assert_eq!(a, b);
1809 assert_eq!(hash_of(&a), hash_of(&b));
1810
1811 let different = parse_clip_path("circle(51px at 1px 2px)").expect("circle");
1812 assert_ne!(a, different);
1813 assert_ne!(hash_of(&a), hash_of(&different));
1814
1815 assert_ne!(hash_of(&ClipPath::None), hash_of(&a));
1818 assert_eq!(hash_of(&ClipPath::None), hash_of(&ClipPath::None));
1819
1820 let outside = parse_shape_outside("circle(50px at 1px 2px)").expect("circle");
1821 assert_ne!(hash_of(&ShapeOutside::None), hash_of(&outside));
1822
1823 let margin = parse_shape_margin("10px").expect("10px");
1824 assert_eq!(
1825 hash_of(&margin),
1826 hash_of(&ShapeMargin {
1827 inner: PixelValue::px(10.0)
1828 })
1829 );
1830 assert_ne!(
1831 hash_of(&margin),
1832 hash_of(&parse_shape_margin("10em").expect("10em"))
1833 );
1834 }
1835}