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