1use crate::shape::{CssShape, ShapePoint};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ShapeParseError {
15 UnknownFunction(String),
17 MissingParameter(String),
19 InvalidNumber(String),
21 InvalidSyntax(String),
23 EmptyInput,
25}
26
27impl core::fmt::Display for ShapeParseError {
28 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
29 match self {
30 Self::UnknownFunction(func) => {
31 write!(f, "Unknown shape function: {func}")
32 }
33 Self::MissingParameter(param) => {
34 write!(f, "Missing required parameter: {param}")
35 }
36 Self::InvalidNumber(num) => {
37 write!(f, "Invalid numeric value: {num}")
38 }
39 Self::InvalidSyntax(msg) => {
40 write!(f, "Invalid syntax: {msg}")
41 }
42 Self::EmptyInput => {
43 write!(f, "Empty input")
44 }
45 }
46 }
47}
48
49pub fn parse_shape(input: &str) -> Result<CssShape, ShapeParseError> {
54 let input = input.trim();
55
56 if input.is_empty() {
57 return Err(ShapeParseError::EmptyInput);
58 }
59
60 let (func_name, args) = parse_function(input)?;
62
63 match func_name.as_str() {
64 "circle" => parse_circle(&args),
65 "ellipse" => parse_ellipse(&args),
66 "polygon" => parse_polygon(&args),
67 "inset" => parse_inset(&args),
68 "path" => parse_path(&args),
69 _ => Err(ShapeParseError::UnknownFunction(func_name)),
70 }
71}
72
73fn parse_function(
75 input: &str,
76) -> Result<(String, String), ShapeParseError> {
77 let open_paren = input
78 .find('(')
79 .ok_or_else(|| ShapeParseError::InvalidSyntax("Missing opening parenthesis".into()))?;
80
81 let close_paren = input
82 .rfind(')')
83 .ok_or_else(|| ShapeParseError::InvalidSyntax("Missing closing parenthesis".into()))?;
84
85 if close_paren <= open_paren {
86 return Err(ShapeParseError::InvalidSyntax("Invalid parentheses".into()));
87 }
88
89 let func_name = input[..open_paren].trim().to_string();
90 let args = input[open_paren + 1..close_paren].trim().to_string();
91
92 Ok((func_name, args))
93}
94
95fn parse_circle(args: &str) -> Result<CssShape, ShapeParseError> {
102 let parts: Vec<&str> = args.split_whitespace().collect();
103
104 if parts.is_empty() {
105 return Err(ShapeParseError::MissingParameter("radius".into()));
106 }
107
108 let radius = parse_length(parts[0])?;
109
110 let center = if parts.len() >= 4 && parts[1] == "at" {
111 let x = parse_length(parts[2])?;
112 let y = parse_length(parts[3])?;
113 ShapePoint::new(x, y)
114 } else {
115 ShapePoint::zero() };
117
118 Ok(CssShape::circle(center, radius))
119}
120
121fn parse_ellipse(args: &str) -> Result<CssShape, ShapeParseError> {
127 let parts: Vec<&str> = args.split_whitespace().collect();
128
129 if parts.len() < 2 {
130 return Err(ShapeParseError::MissingParameter(
131 "radius_x and radius_y".into(),
132 ));
133 }
134
135 let radius_x = parse_length(parts[0])?;
136 let radius_y = parse_length(parts[1])?;
137
138 let center = if parts.len() >= 5 && parts[2] == "at" {
139 let x = parse_length(parts[3])?;
140 let y = parse_length(parts[4])?;
141 ShapePoint::new(x, y)
142 } else {
143 ShapePoint::zero()
144 };
145
146 Ok(CssShape::ellipse(center, radius_x, radius_y))
147}
148
149fn parse_polygon(args: &str) -> Result<CssShape, ShapeParseError> {
159 let args = args.trim();
160
161 let point_str = if args.starts_with("nonzero,") || args.starts_with("evenodd,") {
163 let comma = args.find(',').unwrap();
165 &args[comma + 1..]
166 } else {
167 args
168 };
169
170 let pairs: Vec<&str> = point_str.split(',').map(str::trim).collect();
172
173 if pairs.is_empty() {
174 return Err(ShapeParseError::MissingParameter(
175 "at least one point".into(),
176 ));
177 }
178
179 let mut points = Vec::new();
180
181 for pair in pairs {
182 let coords: Vec<&str> = pair.split_whitespace().collect();
183
184 if coords.len() < 2 {
185 return Err(ShapeParseError::InvalidSyntax(format!(
186 "Expected x y pair, got: {pair}"
187 )));
188 }
189
190 let x = parse_length(coords[0])?;
191 let y = parse_length(coords[1])?;
192
193 points.push(ShapePoint::new(x, y));
194 }
195
196 if points.len() < 3 {
197 return Err(ShapeParseError::InvalidSyntax(
198 "Polygon must have at least 3 points".into(),
199 ));
200 }
201
202 Ok(CssShape::polygon(points.into()))
203}
204
205fn parse_inset(args: &str) -> Result<CssShape, ShapeParseError> {
214 let args = args.trim();
215
216 let (inset_str, border_radius) = if let Some(round_pos) = args.find("round") {
218 let insets = args[..round_pos].trim();
219 let radius_str = args[round_pos + 5..].trim();
220 let radius = parse_length(radius_str)?;
221 (insets, Some(radius))
222 } else {
223 (args, None)
224 };
225
226 let values: Vec<&str> = inset_str.split_whitespace().collect();
227
228 if values.is_empty() {
229 return Err(ShapeParseError::MissingParameter("inset values".into()));
230 }
231
232 let (top, right, bottom, left) = match values.len() {
234 1 => {
235 let all = parse_length(values[0])?;
236 (all, all, all, all)
237 }
238 2 => {
239 let vertical = parse_length(values[0])?;
240 let horizontal = parse_length(values[1])?;
241 (vertical, horizontal, vertical, horizontal)
242 }
243 3 => {
244 let top = parse_length(values[0])?;
245 let horizontal = parse_length(values[1])?;
246 let bottom = parse_length(values[2])?;
247 (top, horizontal, bottom, horizontal)
248 }
249 4 => {
250 let top = parse_length(values[0])?;
251 let right = parse_length(values[1])?;
252 let bottom = parse_length(values[2])?;
253 let left = parse_length(values[3])?;
254 (top, right, bottom, left)
255 }
256 _ => {
257 return Err(ShapeParseError::InvalidSyntax(
258 "Too many inset values (max 4)".into(),
259 ));
260 }
261 };
262
263 border_radius.map_or_else(
264 || Ok(CssShape::inset(top, right, bottom, left)),
265 |radius| Ok(CssShape::inset_rounded(top, right, bottom, left, radius)),
266 )
267}
268
269fn parse_path(args: &str) -> Result<CssShape, ShapeParseError> {
274 use crate::corety::AzString;
275
276 let args = args.trim();
277
278 if args.len() < 2 || !args.starts_with('"') || !args.ends_with('"') {
283 return Err(ShapeParseError::InvalidSyntax(
284 "Path data must be quoted".into(),
285 ));
286 }
287
288 let path_data = AzString::from(&args[1..args.len() - 1]);
289
290 Ok(CssShape::Path(crate::shape::ShapePath { data: path_data }))
291}
292
293fn parse_length(s: &str) -> Result<f32, ShapeParseError> {
298 let s = s.trim();
299
300 if let Some(num_str) = s.strip_suffix("px") {
301 num_str
302 .parse::<f32>()
303 .map_err(|_| ShapeParseError::InvalidNumber(s.to_string()))
304 } else if let Some(num_str) = s.strip_suffix('%') {
305 let percent = num_str
306 .parse::<f32>()
307 .map_err(|_| ShapeParseError::InvalidNumber(s.to_string()))?;
308 Ok(percent)
311 } else {
312 s.parse::<f32>()
314 .map_err(|_| ShapeParseError::InvalidNumber(s.to_string()))
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 #![allow(clippy::float_cmp)]
322 use super::*;
323 use crate::{
324 corety::OptionF32,
325 shape::{ShapeCircle, ShapeEllipse, ShapeInset, ShapePath, ShapePolygon},
326 };
327
328 #[test]
329 fn test_parse_circle() {
330 let shape = parse_shape("circle(50px at 100px 100px)").unwrap();
331 match shape {
332 CssShape::Circle(ShapeCircle { center, radius }) => {
333 assert_eq!(radius, 50.0);
334 assert_eq!(center.x, 100.0);
335 assert_eq!(center.y, 100.0);
336 }
337 _ => panic!("Expected Circle"),
338 }
339 }
340
341 #[test]
342 fn test_parse_circle_no_position() {
343 let shape = parse_shape("circle(50px)").unwrap();
344 match shape {
345 CssShape::Circle(ShapeCircle { center, radius }) => {
346 assert_eq!(radius, 50.0);
347 assert_eq!(center.x, 0.0);
348 assert_eq!(center.y, 0.0);
349 }
350 _ => panic!("Expected Circle"),
351 }
352 }
353
354 #[test]
355 fn test_parse_ellipse() {
356 let shape = parse_shape("ellipse(50px 75px at 100px 100px)").unwrap();
357 match shape {
358 CssShape::Ellipse(ShapeEllipse {
359 center,
360 radius_x,
361 radius_y,
362 }) => {
363 assert_eq!(radius_x, 50.0);
364 assert_eq!(radius_y, 75.0);
365 assert_eq!(center.x, 100.0);
366 assert_eq!(center.y, 100.0);
367 }
368 _ => panic!("Expected Ellipse"),
369 }
370 }
371
372 #[test]
373 fn test_parse_polygon_rectangle() {
374 let shape = parse_shape("polygon(0px 0px, 100px 0px, 100px 100px, 0px 100px)").unwrap();
375 match shape {
376 CssShape::Polygon(ShapePolygon { points }) => {
377 assert_eq!(points.as_ref().len(), 4);
378 assert_eq!(points.as_ref()[0].x, 0.0);
379 assert_eq!(points.as_ref()[0].y, 0.0);
380 assert_eq!(points.as_ref()[2].x, 100.0);
381 assert_eq!(points.as_ref()[2].y, 100.0);
382 }
383 _ => panic!("Expected Polygon"),
384 }
385 }
386
387 #[test]
388 fn test_parse_polygon_star() {
389 let shape = parse_shape(
391 "polygon(50px 0px, 61px 35px, 98px 35px, 68px 57px, 79px 91px, 50px 70px, 21px 91px, \
392 32px 57px, 2px 35px, 39px 35px)",
393 )
394 .unwrap();
395 match shape {
396 CssShape::Polygon(ShapePolygon { points }) => {
397 assert_eq!(points.as_ref().len(), 10); }
399 _ => panic!("Expected Polygon"),
400 }
401 }
402
403 #[test]
404 fn test_parse_inset() {
405 let shape = parse_shape("inset(10px 20px 30px 40px)").unwrap();
406 match shape {
407 CssShape::Inset(ShapeInset {
408 inset_top,
409 inset_right,
410 inset_bottom,
411 inset_left,
412 border_radius,
413 }) => {
414 assert_eq!(inset_top, 10.0);
415 assert_eq!(inset_right, 20.0);
416 assert_eq!(inset_bottom, 30.0);
417 assert_eq!(inset_left, 40.0);
418 assert!(matches!(border_radius, OptionF32::None));
419 }
420 _ => panic!("Expected Inset"),
421 }
422 }
423
424 #[test]
425 fn test_parse_inset_rounded() {
426 let shape = parse_shape("inset(10px round 5px)").unwrap();
427 match shape {
428 CssShape::Inset(ShapeInset {
429 inset_top,
430 inset_right,
431 inset_bottom,
432 inset_left,
433 border_radius,
434 }) => {
435 assert_eq!(inset_top, 10.0);
436 assert_eq!(inset_right, 10.0);
437 assert_eq!(inset_bottom, 10.0);
438 assert_eq!(inset_left, 10.0);
439 assert!(matches!(border_radius, OptionF32::Some(r) if r == 5.0));
440 }
441 _ => panic!("Expected Inset"),
442 }
443 }
444
445 #[test]
446 fn test_parse_path() {
447 let shape = parse_shape(r#"path("M 0 0 L 100 0 L 100 100 Z")"#).unwrap();
448 match shape {
449 CssShape::Path(ShapePath { data }) => {
450 assert_eq!(data.as_str(), "M 0 0 L 100 0 L 100 100 Z");
451 }
452 _ => panic!("Expected Path"),
453 }
454 }
455
456 #[test]
457 fn test_invalid_function() {
458 let result = parse_shape("unknown(50px)");
459 assert!(result.is_err());
460 }
461
462 #[test]
463 fn test_empty_input() {
464 let result = parse_shape("");
465 assert!(matches!(result, Err(ShapeParseError::EmptyInput)));
466 }
467}
468
469#[cfg(test)]
470#[allow(clippy::float_cmp, clippy::unreadable_literal)]
471mod autotest_generated {
472 use std::panic::{catch_unwind, AssertUnwindSafe};
482
483 use super::*;
484 use crate::{
485 corety::OptionF32,
486 shape::{ShapeCircle, ShapeEllipse, ShapeInset, ShapePath, ShapePolygon},
487 };
488
489 fn circle_of(shape: &CssShape) -> ShapeCircle {
492 match shape {
493 CssShape::Circle(c) => *c,
494 other => panic!("expected Circle, got {other:?}"),
495 }
496 }
497
498 fn ellipse_of(shape: &CssShape) -> ShapeEllipse {
499 match shape {
500 CssShape::Ellipse(e) => *e,
501 other => panic!("expected Ellipse, got {other:?}"),
502 }
503 }
504
505 fn inset_of(shape: &CssShape) -> ShapeInset {
506 match shape {
507 CssShape::Inset(i) => *i,
508 other => panic!("expected Inset, got {other:?}"),
509 }
510 }
511
512 fn polygon_points(shape: &CssShape) -> Vec<ShapePoint> {
513 match shape {
514 CssShape::Polygon(ShapePolygon { points }) => points.as_ref().to_vec(),
515 other => panic!("expected Polygon, got {other:?}"),
516 }
517 }
518
519 fn path_data(shape: &CssShape) -> String {
520 match shape {
521 CssShape::Path(ShapePath { data }) => data.as_str().to_string(),
522 other => panic!("expected Path, got {other:?}"),
523 }
524 }
525
526 fn radius_of(input: &str) -> f32 {
527 circle_of(&parse_shape(input).unwrap()).radius
528 }
529
530 #[test]
535 fn bug_parse_path_bare_quote_char_panics_on_reversed_slice() {
536 let parsed = catch_unwind(AssertUnwindSafe(|| parse_shape("path(\")")));
544 assert!(
545 parsed.is_ok(),
546 "parse_shape(r#\"path(\")\"#) panicked instead of returning Err: parse_path slices \
547 args[1..len-1] on a 1-char arg"
548 );
549 assert!(matches!(
550 parsed.unwrap(),
551 Err(ShapeParseError::InvalidSyntax(_))
552 ));
553 }
554
555 #[test]
556 fn bug_parse_path_direct_single_quote_arg_panics() {
557 let parsed = catch_unwind(AssertUnwindSafe(|| parse_path("\"")));
560 assert!(
561 parsed.is_ok(),
562 "parse_path(\"\\\"\") panicked instead of returning Err"
563 );
564 assert!(parsed.unwrap().is_err());
565 }
566
567 #[test]
572 fn shape_empty_and_whitespace_only_input_is_empty_input_err() {
573 assert!(matches!(parse_shape(""), Err(ShapeParseError::EmptyInput)));
574 assert!(matches!(
575 parse_shape(" "),
576 Err(ShapeParseError::EmptyInput)
577 ));
578 assert!(matches!(
579 parse_shape("\t\n\r \x0c"),
580 Err(ShapeParseError::EmptyInput)
581 ));
582 assert!(parse_shape("\u{00a0}\u{2003}").is_err());
584 }
585
586 #[test]
587 fn shape_garbage_input_is_rejected_without_panicking() {
588 for garbage in [
589 "!!!",
590 ";;;;",
591 "\0\0\0",
592 "\u{7}\u{1b}[0m",
593 "circle",
594 "circle 50px",
595 "()",
596 "(",
597 ")",
598 ")(",
599 ")circle(",
600 "((((",
601 "))))",
602 "-",
603 "50px",
604 "{}",
605 "circle{50px}",
606 "circle[50px]",
607 "<circle r=\"50\"/>",
608 ] {
609 let parsed = catch_unwind(AssertUnwindSafe(|| parse_shape(garbage)));
610 assert!(parsed.is_ok(), "parse_shape({garbage:?}) panicked");
611 assert!(
612 parsed.unwrap().is_err(),
613 "parse_shape({garbage:?}) unexpectedly succeeded"
614 );
615 }
616 }
617
618 #[test]
619 fn shape_missing_parens_report_which_one_is_missing() {
620 assert!(matches!(
621 parse_shape("circle 50px"),
622 Err(ShapeParseError::InvalidSyntax(ref m)) if m.contains("opening")
623 ));
624 assert!(matches!(
625 parse_shape("circle(50px"),
626 Err(ShapeParseError::InvalidSyntax(ref m)) if m.contains("closing")
627 ));
628 assert!(matches!(
630 parse_shape(")circle("),
631 Err(ShapeParseError::InvalidSyntax(ref m)) if m.contains("Invalid parentheses")
632 ));
633 }
634
635 #[test]
636 fn shape_unknown_function_names_are_reported_verbatim() {
637 assert!(matches!(
638 parse_shape("unknown(50px)"),
639 Err(ShapeParseError::UnknownFunction(ref f)) if f == "unknown"
640 ));
641 assert!(matches!(
643 parse_shape("()"),
644 Err(ShapeParseError::UnknownFunction(ref f)) if f.is_empty()
645 ));
646 }
647
648 #[test]
649 fn current_shape_function_names_are_case_sensitive() {
650 assert!(matches!(
654 parse_shape("CIRCLE(50px)"),
655 Err(ShapeParseError::UnknownFunction(ref f)) if f == "CIRCLE"
656 ));
657 assert!(parse_shape("Inset(10px)").is_err());
658 }
659
660 #[test]
661 fn current_shape_trailing_junk_after_the_last_paren_is_silently_dropped() {
662 assert_eq!(radius_of("circle(50px) garbage"), 50.0);
667 assert_eq!(radius_of("circle(50px);garbage"), 50.0);
668 assert_eq!(radius_of("circle(50px)!!!"), 50.0);
669 assert!(matches!(
671 parse_shape("junk circle(50px)"),
672 Err(ShapeParseError::UnknownFunction(ref f)) if f == "junk circle"
673 ));
674 }
675
676 #[test]
677 fn shape_surrounding_whitespace_is_trimmed_before_dispatch() {
678 assert_eq!(radius_of(" circle(50px) "), 50.0);
679 assert_eq!(radius_of("\n\tcircle( 50px )\n"), 50.0);
680 }
681
682 #[test]
683 fn shape_extra_closing_paren_inside_args_is_rejected_not_ignored() {
684 assert!(matches!(
686 parse_shape("circle(50px))"),
687 Err(ShapeParseError::InvalidNumber(ref n)) if n == "50px)"
688 ));
689 assert!(parse_shape("circle((50px)").is_err());
690 }
691
692 #[test]
693 fn shape_unicode_input_does_not_panic_or_split_a_codepoint() {
694 for input in [
695 "\u{1F600}",
696 "circle(\u{1F600})",
697 "\u{1F600}(50px)",
698 "cercle\u{301}(50px)", "circle(50px\u{200b})", "circle(\u{FF15}\u{FF10}px)", "円(50px)",
702 "\u{202e}circle(50px)", "polygon(\u{1F4A9} \u{1F4A9}, 0 0, 1 1)",
704 "path(\u{1F600})",
705 "inset(\u{1F600} round \u{1F600})",
706 "ellipse(\u{1F600} \u{1F600})",
707 ] {
708 let parsed = catch_unwind(AssertUnwindSafe(|| parse_shape(input)));
709 assert!(parsed.is_ok(), "parse_shape({input:?}) panicked");
710 assert!(
711 parsed.unwrap().is_err(),
712 "parse_shape({input:?}) unexpectedly succeeded"
713 );
714 }
715 }
716
717 #[test]
718 fn shape_multibyte_name_slices_on_a_char_boundary() {
719 assert!(matches!(
722 parse_shape("\u{1F600}(50px)"),
723 Err(ShapeParseError::UnknownFunction(ref f)) if f == "\u{1F600}"
724 ));
725 }
726
727 #[test]
728 fn shape_deeply_nested_parens_do_not_stack_overflow() {
729 let depth = 10_000;
732 let nested = format!("{}{}", "(".repeat(depth), ")".repeat(depth));
733 assert!(matches!(
734 parse_shape(&nested),
735 Err(ShapeParseError::UnknownFunction(ref f)) if f.is_empty()
736 ));
737
738 let nested_circle = format!("circle{}50px{}", "(".repeat(5_000), ")".repeat(5_000));
739 assert!(matches!(
740 parse_shape(&nested_circle),
741 Err(ShapeParseError::InvalidNumber(_))
742 ));
743 }
744
745 #[test]
746 fn shape_extremely_long_input_does_not_hang() {
747 let long_garbage = "x".repeat(1_000_000);
749 assert!(matches!(
750 parse_shape(&long_garbage),
751 Err(ShapeParseError::InvalidSyntax(_))
752 ));
753
754 let huge = format!("circle({}px)", "9".repeat(50_000));
756 let radius = radius_of(&huge);
757 assert!(radius.is_infinite() && radius.is_sign_positive());
758 }
759
760 #[test]
761 fn shape_parsing_is_deterministic() {
762 let input = "polygon(0px 0px, 100px 0px, 50px 100px)";
763 assert_eq!(parse_shape(input).unwrap(), parse_shape(input).unwrap());
764 assert_eq!(parse_shape("!!!").unwrap_err(), parse_shape("!!!").unwrap_err());
765 }
766
767 #[test]
768 fn shape_minimal_valid_input_per_function() {
769 assert_eq!(circle_of(&parse_shape("circle(1)").unwrap()).radius, 1.0);
770 assert_eq!(ellipse_of(&parse_shape("ellipse(1 2)").unwrap()).radius_y, 2.0);
771 assert_eq!(polygon_points(&parse_shape("polygon(0 0,1 0,0 1)").unwrap()).len(), 3);
772 assert_eq!(inset_of(&parse_shape("inset(0)").unwrap()).inset_top, 0.0);
773 assert_eq!(path_data(&parse_shape("path(\"\")").unwrap()), "");
774 }
775
776 #[test]
781 fn function_empty_and_whitespace_input_is_err() {
782 assert!(parse_function("").is_err());
783 assert!(parse_function(" \t\n").is_err());
784 }
785
786 #[test]
787 fn function_splits_and_trims_name_and_args() {
788 let (name, args) = parse_function(" circle ( 50px ) ").unwrap();
789 assert_eq!(name, "circle");
790 assert_eq!(args, "50px");
791
792 let (name, args) = parse_function("()").unwrap();
794 assert!(name.is_empty() && args.is_empty());
795 }
796
797 #[test]
798 fn function_uses_first_open_and_last_close_paren() {
799 let (name, args) = parse_function("a(b(c))").unwrap();
800 assert_eq!(name, "a");
801 assert_eq!(args, "b(c)", "rfind(')') must take the outermost close paren");
802 }
803
804 #[test]
805 fn function_rejects_reversed_and_missing_parens() {
806 assert!(matches!(
807 parse_function(")("),
808 Err(ShapeParseError::InvalidSyntax(ref m)) if m.contains("Invalid parentheses")
809 ));
810 assert!(parse_function("no parens here").is_err());
811 assert!(parse_function("circle(").is_err());
812 assert!(parse_function("circle)").is_err());
813 }
814
815 #[test]
816 fn function_handles_pathological_lengths_without_panicking() {
817 let long = "y".repeat(1_000_000);
818 assert!(parse_function(&long).is_err());
819
820 let long_args = format!("f({})", "z".repeat(1_000_000));
822 let (name, args) = parse_function(&long_args).unwrap();
823 assert_eq!(name, "f");
824 assert_eq!(args.len(), 1_000_000);
825 }
826
827 #[test]
828 fn function_multibyte_args_round_trip_intact() {
829 let (name, args) = parse_function("f(\u{1F600}\u{0301})").unwrap();
830 assert_eq!(name, "f");
831 assert_eq!(args, "\u{1F600}\u{0301}");
832 }
833
834 #[test]
839 fn circle_empty_or_whitespace_args_report_the_missing_radius() {
840 assert!(matches!(
841 parse_circle(""),
842 Err(ShapeParseError::MissingParameter(ref p)) if p == "radius"
843 ));
844 assert!(matches!(
845 parse_circle(" \t\n "),
846 Err(ShapeParseError::MissingParameter(_))
847 ));
848 }
849
850 #[test]
851 fn circle_garbage_radius_is_an_invalid_number() {
852 assert!(matches!(
853 parse_circle("abc"),
854 Err(ShapeParseError::InvalidNumber(ref n)) if n == "abc"
855 ));
856 assert!(parse_circle("at 10px 10px").is_err());
857 assert!(parse_circle("50px at 10px abc").is_err());
858 }
859
860 #[test]
861 fn current_circle_ignores_a_malformed_at_clause_instead_of_erroring() {
862 let truncated = circle_of(&parse_circle("50px at 100px").unwrap());
866 assert_eq!(truncated.center, ShapePoint::zero());
867
868 let wrong_case = circle_of(&parse_circle("50px AT 100px 100px").unwrap());
869 assert_eq!(wrong_case.center, ShapePoint::zero());
870
871 let bad_keyword = circle_of(&parse_circle("50px on 100px 100px").unwrap());
872 assert_eq!(bad_keyword.center, ShapePoint::zero());
873
874 let extra = circle_of(&parse_circle("50px at 1px 2px 3px 4px").unwrap());
876 assert_eq!(extra.center, ShapePoint::new(1.0, 2.0));
877 }
878
879 #[test]
880 fn current_circle_accepts_a_negative_radius() {
881 assert_eq!(radius_of("circle(-50px)"), -50.0);
883 let neg_zero = radius_of("circle(-0px)");
885 assert!(neg_zero == 0.0 && neg_zero.is_sign_negative());
886 }
887
888 #[test]
889 fn circle_non_finite_and_saturating_radii_do_not_panic() {
890 assert!(radius_of("circle(NaN)").is_nan());
892 assert!(radius_of("circle(NaNpx)").is_nan());
893 assert!(radius_of("circle(inf)").is_infinite());
894 assert!(radius_of("circle(infinitypx)").is_infinite());
895 assert!(radius_of("circle(-inf)").is_sign_negative());
896
897 assert!(radius_of("circle(1e39px)").is_infinite());
899 assert_eq!(radius_of("circle(1e-46px)"), 0.0);
900 assert_eq!(radius_of("circle(9223372036854775807px)"), 9223372036854775807.0_f32);
901 }
902
903 #[test]
904 fn circle_percentages_are_currently_kept_as_raw_numbers() {
905 assert_eq!(radius_of("circle(50%)"), 50.0);
908 assert_eq!(radius_of("circle(50%)"), radius_of("circle(50px)"));
909 }
910
911 #[test]
916 fn ellipse_requires_two_radii() {
917 assert!(matches!(
918 parse_ellipse(""),
919 Err(ShapeParseError::MissingParameter(ref p)) if p.contains("radius_x")
920 ));
921 assert!(matches!(
922 parse_ellipse("50px"),
923 Err(ShapeParseError::MissingParameter(_))
924 ));
925 assert!(parse_ellipse("50px abc").is_err());
926 }
927
928 #[test]
929 fn current_ellipse_ignores_a_truncated_at_clause() {
930 let truncated = ellipse_of(&parse_ellipse("50px 75px at 100px").unwrap());
932 assert_eq!(truncated.center, ShapePoint::zero());
933
934 let no_keyword = ellipse_of(&parse_ellipse("1px 2px 3px 4px 5px").unwrap());
936 assert_eq!(no_keyword.center, ShapePoint::zero());
937 assert_eq!((no_keyword.radius_x, no_keyword.radius_y), (1.0, 2.0));
938 }
939
940 #[test]
941 fn ellipse_valid_input_maps_radii_and_centre_in_order() {
942 let e = ellipse_of(&parse_shape("ellipse(50px 75px at 10px 20px)").unwrap());
943 assert_eq!(e.radius_x, 50.0);
944 assert_eq!(e.radius_y, 75.0);
945 assert_eq!(e.center, ShapePoint::new(10.0, 20.0));
946 }
947
948 #[test]
949 fn ellipse_non_finite_radii_do_not_panic() {
950 let e = ellipse_of(&parse_shape("ellipse(NaN inf)").unwrap());
951 assert!(e.radius_x.is_nan());
952 assert!(e.radius_y.is_infinite());
953 }
954
955 #[test]
960 fn polygon_empty_and_whitespace_args_are_rejected() {
961 assert!(matches!(
965 parse_polygon(""),
966 Err(ShapeParseError::InvalidSyntax(_))
967 ));
968 assert!(matches!(
969 parse_polygon(" \t "),
970 Err(ShapeParseError::InvalidSyntax(_))
971 ));
972 }
973
974 #[test]
975 fn polygon_needs_at_least_three_points() {
976 assert!(matches!(
977 parse_polygon("0 0"),
978 Err(ShapeParseError::InvalidSyntax(ref m)) if m.contains("at least 3 points")
979 ));
980 assert!(matches!(
981 parse_polygon("0 0, 1 1"),
982 Err(ShapeParseError::InvalidSyntax(ref m)) if m.contains("at least 3 points")
983 ));
984 assert_eq!(polygon_points(&parse_polygon("0 0, 1 1, 2 2").unwrap()).len(), 3);
985 }
986
987 #[test]
988 fn polygon_malformed_pairs_are_rejected() {
989 assert!(parse_polygon("0 0, 1 1, 2").is_err());
991 assert!(parse_polygon("0 0, 1 1, 2 2,").is_err());
992 assert!(parse_polygon("0 0,, 1 1, 2 2").is_err());
993 assert!(parse_polygon(",0 0, 1 1, 2 2").is_err());
994 assert!(parse_polygon("0 0, 1 1, abc def").is_err());
995 }
996
997 #[test]
998 fn polygon_fill_rule_prefix_is_stripped_only_when_comma_attached() {
999 let nonzero = polygon_points(&parse_polygon("nonzero, 0 0, 1 0, 1 1").unwrap());
1000 assert_eq!(nonzero.len(), 3);
1001 assert_eq!(nonzero[0], ShapePoint::zero());
1002
1003 let evenodd = polygon_points(&parse_polygon("evenodd, 0 0, 1 0, 1 1").unwrap());
1004 assert_eq!(evenodd.len(), 3);
1005
1006 assert!(matches!(
1009 parse_polygon("nonzero , 0 0, 1 0, 1 1"),
1010 Err(ShapeParseError::InvalidSyntax(_))
1011 ));
1012 assert!(matches!(
1014 parse_polygon("nonzero 0 0, 1 0, 1 1"),
1015 Err(ShapeParseError::InvalidNumber(ref n)) if n == "nonzero"
1016 ));
1017 assert!(parse_polygon("nonzero,").is_err());
1019 }
1020
1021 #[test]
1022 fn current_polygon_ignores_extra_coordinates_in_a_pair() {
1023 let points = polygon_points(&parse_polygon("0 0 999, 1 1 999, 2 2 999").unwrap());
1025 assert_eq!(points.len(), 3);
1026 assert_eq!(points[2], ShapePoint::new(2.0, 2.0));
1027 }
1028
1029 #[test]
1030 fn current_polygon_mixes_units_silently() {
1031 let points = polygon_points(&parse_polygon("0% 0px, 100 0%, 50px 100").unwrap());
1033 assert_eq!(points[1], ShapePoint::new(100.0, 0.0));
1034 assert_eq!(points[2], ShapePoint::new(50.0, 100.0));
1035 }
1036
1037 #[test]
1038 fn polygon_non_finite_coordinates_do_not_panic() {
1039 let points = polygon_points(&parse_polygon("NaN 0, inf 1, -inf 2").unwrap());
1040 assert!(points[0].x.is_nan());
1041 assert!(points[1].x.is_infinite() && points[1].x.is_sign_positive());
1042 assert!(points[2].x.is_infinite() && points[2].x.is_sign_negative());
1043 }
1044
1045 #[test]
1046 fn polygon_with_twenty_thousand_points_does_not_hang() {
1047 let mut args = String::with_capacity(20_000 * 10);
1048 for i in 0..20_000 {
1049 if i > 0 {
1050 args.push(',');
1051 }
1052 args.push_str("1px 2px");
1053 }
1054 let points = polygon_points(&parse_polygon(&args).unwrap());
1055 assert_eq!(points.len(), 20_000);
1056 assert_eq!(points[19_999], ShapePoint::new(1.0, 2.0));
1057 }
1058
1059 #[test]
1064 fn inset_empty_args_report_missing_values() {
1065 assert!(matches!(
1066 parse_inset(""),
1067 Err(ShapeParseError::MissingParameter(ref p)) if p.contains("inset values")
1068 ));
1069 assert!(matches!(
1070 parse_inset(" "),
1071 Err(ShapeParseError::MissingParameter(_))
1072 ));
1073 }
1074
1075 #[test]
1076 fn inset_shorthand_expansion_follows_the_margin_rules() {
1077 let one = inset_of(&parse_inset("10px").unwrap());
1078 assert_eq!(
1079 (one.inset_top, one.inset_right, one.inset_bottom, one.inset_left),
1080 (10.0, 10.0, 10.0, 10.0)
1081 );
1082
1083 let two = inset_of(&parse_inset("10px 20px").unwrap());
1084 assert_eq!(
1085 (two.inset_top, two.inset_right, two.inset_bottom, two.inset_left),
1086 (10.0, 20.0, 10.0, 20.0)
1087 );
1088
1089 let three = inset_of(&parse_inset("10px 20px 30px").unwrap());
1090 assert_eq!(
1091 (three.inset_top, three.inset_right, three.inset_bottom, three.inset_left),
1092 (10.0, 20.0, 30.0, 20.0)
1093 );
1094
1095 let four = inset_of(&parse_inset("10px 20px 30px 40px").unwrap());
1096 assert_eq!(
1097 (four.inset_top, four.inset_right, four.inset_bottom, four.inset_left),
1098 (10.0, 20.0, 30.0, 40.0)
1099 );
1100 }
1101
1102 #[test]
1103 fn inset_rejects_more_than_four_values() {
1104 assert!(matches!(
1105 parse_inset("1px 2px 3px 4px 5px"),
1106 Err(ShapeParseError::InvalidSyntax(ref m)) if m.contains("max 4")
1107 ));
1108 let many = ["1px"; 1_000].join(" ");
1110 assert!(matches!(
1111 parse_inset(&many),
1112 Err(ShapeParseError::InvalidSyntax(_))
1113 ));
1114 }
1115
1116 #[test]
1117 fn inset_round_keyword_splits_insets_from_the_radius() {
1118 let rounded = inset_of(&parse_inset("10px 20px round 5px").unwrap());
1119 assert_eq!(rounded.inset_top, 10.0);
1120 assert_eq!(rounded.inset_right, 20.0);
1121 assert!(matches!(rounded.border_radius, OptionF32::Some(r) if r == 5.0));
1122
1123 let plain = inset_of(&parse_inset("10px").unwrap());
1124 assert!(matches!(plain.border_radius, OptionF32::None));
1125 }
1126
1127 #[test]
1128 fn inset_malformed_round_clauses_are_rejected() {
1129 assert!(matches!(
1131 parse_inset("10px round"),
1132 Err(ShapeParseError::InvalidNumber(ref n)) if n.is_empty()
1133 ));
1134 assert!(matches!(
1137 parse_inset("round 5px"),
1138 Err(ShapeParseError::MissingParameter(_))
1139 ));
1140 assert!(parse_inset("10px round 5px round 6px").is_err());
1142 assert!(matches!(
1145 parse_inset("10px roundup"),
1146 Err(ShapeParseError::InvalidNumber(ref n)) if n == "up"
1147 ));
1148 }
1149
1150 #[test]
1151 fn inset_non_finite_and_negative_values_do_not_panic() {
1152 let nan = inset_of(&parse_inset("NaN").unwrap());
1153 assert!(nan.inset_top.is_nan() && nan.inset_left.is_nan());
1154
1155 let huge = inset_of(&parse_inset("1e39px round 1e39px").unwrap());
1156 assert!(huge.inset_top.is_infinite());
1157 assert!(matches!(huge.border_radius, OptionF32::Some(r) if r.is_infinite()));
1158
1159 let negative = inset_of(&parse_inset("-10px round -5px").unwrap());
1161 assert_eq!(negative.inset_top, -10.0);
1162 assert!(matches!(negative.border_radius, OptionF32::Some(r) if r == -5.0));
1163 }
1164
1165 #[test]
1170 fn path_requires_double_quotes_on_both_ends() {
1171 for unquoted in [
1172 "",
1173 " ",
1174 "M 0 0",
1175 "\"M 0 0",
1176 "M 0 0\"",
1177 "'M 0 0'", "`M 0 0`",
1179 ] {
1180 let parsed = catch_unwind(AssertUnwindSafe(|| parse_path(unquoted)));
1181 assert!(parsed.is_ok(), "parse_path({unquoted:?}) panicked");
1182 assert!(
1183 matches!(parsed.unwrap(), Err(ShapeParseError::InvalidSyntax(ref m)) if m.contains("quoted")),
1184 "parse_path({unquoted:?}) should be an unquoted-syntax error"
1185 );
1186 }
1187 }
1188
1189 #[test]
1190 fn path_strips_exactly_one_quote_from_each_end() {
1191 assert_eq!(path_data(&parse_path("\"\"").unwrap()), "");
1192 assert_eq!(path_data(&parse_path("\"M 0 0 Z\"").unwrap()), "M 0 0 Z");
1193 assert_eq!(path_data(&parse_path("\"a\"b\"").unwrap()), "a\"b");
1195 }
1196
1197 #[test]
1198 fn current_path_data_is_stored_without_validation() {
1199 assert_eq!(path_data(&parse_shape("path(\"not svg at all\")").unwrap()), "not svg at all");
1202 assert_eq!(path_data(&parse_shape("path(\"\u{1F600}\")").unwrap()), "\u{1F600}");
1203 assert_eq!(path_data(&parse_shape("path(\"M 0 0 L NaN inf\")").unwrap()), "M 0 0 L NaN inf");
1204 }
1205
1206 #[test]
1207 fn path_multibyte_content_slices_on_char_boundaries() {
1208 let data = path_data(&parse_path("\"\u{1F600}\u{0301}\u{1F600}\"").unwrap());
1211 assert_eq!(data, "\u{1F600}\u{0301}\u{1F600}");
1212 }
1213
1214 #[test]
1215 fn path_with_a_megabyte_of_data_does_not_hang() {
1216 let inner = "L 1 1 ".repeat(150_000);
1217 let arg = format!("\"{inner}\"");
1218 assert_eq!(path_data(&parse_path(&arg).unwrap()), inner);
1219 }
1220
1221 #[test]
1226 fn length_empty_and_whitespace_are_invalid_numbers() {
1227 assert!(matches!(
1228 parse_length(""),
1229 Err(ShapeParseError::InvalidNumber(ref n)) if n.is_empty()
1230 ));
1231 assert!(matches!(
1232 parse_length(" \t\n "),
1233 Err(ShapeParseError::InvalidNumber(ref n)) if n.is_empty()
1234 ));
1235 assert!(matches!(
1237 parse_length("px"),
1238 Err(ShapeParseError::InvalidNumber(ref n)) if n == "px"
1239 ));
1240 assert!(parse_length("%").is_err());
1241 }
1242
1243 #[test]
1244 fn length_accepts_px_percent_and_unitless() {
1245 assert_eq!(parse_length("50px").unwrap(), 50.0);
1246 assert_eq!(parse_length("50%").unwrap(), 50.0);
1247 assert_eq!(parse_length("50").unwrap(), 50.0);
1248 assert_eq!(parse_length(" 50px ").unwrap(), 50.0);
1249 assert_eq!(parse_length("+5px").unwrap(), 5.0);
1250 assert_eq!(parse_length(".5px").unwrap(), 0.5);
1251 assert_eq!(parse_length("5.px").unwrap(), 5.0);
1252 assert_eq!(parse_length("5e2px").unwrap(), 500.0);
1253 }
1254
1255 #[test]
1256 fn current_length_rejects_uppercase_units_and_other_css_units() {
1257 for unsupported in ["50PX", "50Px", "1em", "1rem", "1vh", "1vw", "1pt", "1cm", "1fr"] {
1260 assert!(
1261 parse_length(unsupported).is_err(),
1262 "parse_length({unsupported:?}) unexpectedly succeeded"
1263 );
1264 }
1265 }
1266
1267 #[test]
1268 fn length_rejects_non_css_numeric_syntax() {
1269 for bad in [
1270 "1_000px", "0x10px", "1,5px", "50px50px", "50%%", "50%px", "--5px", "5 0px", "50 px",
1271 "1e", "e5", "0b101", "١٠px", ] {
1273 let parsed = catch_unwind(AssertUnwindSafe(|| parse_length(bad)));
1274 assert!(parsed.is_ok(), "parse_length({bad:?}) panicked");
1275 assert!(
1276 parsed.unwrap().is_err(),
1277 "parse_length({bad:?}) unexpectedly succeeded"
1278 );
1279 }
1280 }
1281
1282 #[test]
1283 fn length_saturates_on_overflow_and_flushes_on_underflow() {
1284 assert!(parse_length("1e39").unwrap().is_infinite());
1285 assert!(parse_length("1e39px").unwrap().is_sign_positive());
1286 assert!(parse_length("-1e39px").unwrap().is_sign_negative());
1287 assert_eq!(parse_length("1e-46px").unwrap(), 0.0);
1288 assert_eq!(parse_length("-1e-46px").unwrap(), -0.0);
1289 assert_eq!(parse_length(&format!("{}px", f32::MAX)).unwrap(), f32::MAX);
1291 assert_eq!(parse_length(&format!("{}px", f32::MIN)).unwrap(), f32::MIN);
1292 assert_eq!(
1293 parse_length(&format!("{}px", f32::MIN_POSITIVE)).unwrap(),
1294 f32::MIN_POSITIVE
1295 );
1296 assert!(parse_length(&format!("{}px", f64::MAX)).unwrap().is_infinite());
1298 }
1299
1300 #[test]
1301 fn current_length_accepts_nan_and_infinity_spellings() {
1302 assert!(parse_length("NaN").unwrap().is_nan());
1306 assert!(parse_length("nan").unwrap().is_nan());
1307 assert!(parse_length("NaNpx").unwrap().is_nan());
1308 assert!(parse_length("-NaN%").unwrap().is_nan());
1309 assert!(parse_length("inf").unwrap().is_infinite());
1310 assert!(parse_length("infinity").unwrap().is_infinite());
1311 assert!(parse_length("INFpx").unwrap().is_infinite());
1312 assert!(parse_length("-inf").unwrap().is_sign_negative());
1313 }
1314
1315 #[test]
1316 fn length_preserves_the_sign_of_negative_zero() {
1317 let negative_zero = parse_length("-0px").unwrap();
1318 assert!(negative_zero == 0.0 && negative_zero.is_sign_negative());
1319 assert!(parse_length("0px").unwrap().is_sign_positive());
1320 }
1321
1322 #[test]
1323 fn length_handles_huge_digit_strings_without_hanging() {
1324 let huge = format!("{}px", "9".repeat(100_000));
1327 assert!(parse_length(&huge).unwrap().is_infinite());
1328
1329 let padded = format!("{}1px", "0".repeat(100_000));
1331 assert_eq!(parse_length(&padded).unwrap(), 1.0);
1332
1333 let garbage = "q".repeat(1_000_000);
1335 assert!(parse_length(&garbage).is_err());
1336 }
1337
1338 #[test]
1339 fn length_error_payload_is_the_trimmed_input() {
1340 assert!(matches!(
1341 parse_length(" bogus "),
1342 Err(ShapeParseError::InvalidNumber(ref n)) if n == "bogus"
1343 ));
1344 assert!(matches!(
1346 parse_length("bogus px"),
1347 Err(ShapeParseError::InvalidNumber(ref n)) if n == "bogus px"
1348 ));
1349 }
1350
1351 #[test]
1356 fn error_display_is_non_empty_and_names_the_variant() {
1357 let cases = [
1358 (ShapeParseError::UnknownFunction("blob".into()), "Unknown shape function", "blob"),
1359 (ShapeParseError::MissingParameter("radius".into()), "Missing required parameter", "radius"),
1360 (ShapeParseError::InvalidNumber("12abc".into()), "Invalid numeric value", "12abc"),
1361 (ShapeParseError::InvalidSyntax("bad parens".into()), "Invalid syntax", "bad parens"),
1362 ];
1363 for (err, prefix, payload) in cases {
1364 let rendered = err.to_string();
1365 assert!(rendered.starts_with(prefix), "{rendered:?} lacks prefix {prefix:?}");
1366 assert!(rendered.contains(payload), "{rendered:?} lost its payload {payload:?}");
1367 }
1368 assert_eq!(ShapeParseError::EmptyInput.to_string(), "Empty input");
1369 }
1370
1371 #[test]
1372 fn error_display_survives_hostile_payloads() {
1373 let payloads = [
1376 String::new(),
1377 "{}{0}{name}%s%n".to_string(),
1378 "\u{1F600}\u{0301}\u{202e}".to_string(),
1379 "\0\u{7}\n\t".to_string(),
1380 "x".repeat(100_000),
1381 ];
1382 for payload in payloads {
1383 for err in [
1384 ShapeParseError::UnknownFunction(payload.clone()),
1385 ShapeParseError::MissingParameter(payload.clone()),
1386 ShapeParseError::InvalidNumber(payload.clone()),
1387 ShapeParseError::InvalidSyntax(payload.clone()),
1388 ] {
1389 let rendered = catch_unwind(AssertUnwindSafe(|| err.to_string()));
1390 assert!(rendered.is_ok(), "Display panicked on payload {payload:?}");
1391 let rendered = rendered.unwrap();
1392 assert!(!rendered.is_empty());
1393 assert!(
1394 rendered.ends_with(&payload),
1395 "payload must be emitted literally, not re-formatted"
1396 );
1397 }
1398 }
1399 }
1400
1401 #[test]
1402 fn error_variants_render_distinctly_and_compare_by_value() {
1403 let same_payload = "x".to_string();
1404 let unknown = ShapeParseError::UnknownFunction(same_payload.clone());
1405 let missing = ShapeParseError::MissingParameter(same_payload.clone());
1406 assert_ne!(unknown, missing);
1407 assert_ne!(unknown.to_string(), missing.to_string());
1408 assert_eq!(unknown, ShapeParseError::UnknownFunction(same_payload));
1409 assert_eq!(unknown.clone(), unknown);
1410 assert!(!format!("{unknown:?}").is_empty());
1412 }
1413
1414 fn assert_round_trips(shape: &CssShape) {
1419 let printed = shape.print_as_css_value();
1420 let reparsed = parse_shape(&printed)
1421 .unwrap_or_else(|e| panic!("{printed:?} did not re-parse: {e}"));
1422 assert_eq!(
1423 &reparsed, shape,
1424 "round-trip changed the shape via {printed:?}"
1425 );
1426 assert_eq!(reparsed.print_as_css_value(), printed);
1428 }
1429
1430 #[test]
1431 fn round_trip_every_shape_variant() {
1432 assert_round_trips(&CssShape::circle(ShapePoint::new(10.0, 20.0), 50.0));
1433 assert_round_trips(&CssShape::ellipse(ShapePoint::new(1.5, -2.5), 3.25, 4.75));
1434 assert_round_trips(&CssShape::polygon(
1435 vec![
1436 ShapePoint::new(0.0, 0.0),
1437 ShapePoint::new(100.0, 0.0),
1438 ShapePoint::new(50.0, 100.0),
1439 ]
1440 .into(),
1441 ));
1442 assert_round_trips(&CssShape::inset(1.0, 2.0, 3.0, 4.0));
1443 assert_round_trips(&CssShape::inset_rounded(1.0, 2.0, 3.0, 4.0, 5.0));
1444 assert_round_trips(&CssShape::Path(ShapePath {
1445 data: crate::corety::AzString::from("M 0 0 L 100 0 Z"),
1446 }));
1447 }
1448
1449 #[test]
1450 fn round_trip_survives_extreme_and_negative_numbers() {
1451 assert_round_trips(&CssShape::circle(
1452 ShapePoint::new(f32::MIN, f32::MAX),
1453 f32::MIN_POSITIVE,
1454 ));
1455 assert_round_trips(&CssShape::inset(-0.0, -1.5, 1e-30, 1e30));
1456 assert_round_trips(&CssShape::ellipse(
1457 ShapePoint::new(-0.000_001, 123_456.79),
1458 0.1,
1459 0.2,
1460 ));
1461 }
1462
1463 #[test]
1464 fn round_trip_of_non_finite_values_preserves_them() {
1465 let printed = CssShape::circle(ShapePoint::zero(), f32::INFINITY).print_as_css_value();
1468 assert_eq!(printed, "circle(infpx at 0px 0px)");
1469 assert!(circle_of(&parse_shape(&printed).unwrap()).radius.is_infinite());
1470
1471 let printed = CssShape::circle(ShapePoint::zero(), f32::NAN).print_as_css_value();
1473 assert!(circle_of(&parse_shape(&printed).unwrap()).radius.is_nan());
1474 }
1475
1476 #[test]
1477 fn round_trip_of_a_path_containing_quotes_and_parens() {
1478 for data in ["M 0 0)", ")", "a\"b", "", "M 0 0 L 1 1 Z"] {
1481 assert_round_trips(&CssShape::Path(ShapePath {
1482 data: crate::corety::AzString::from(data),
1483 }));
1484 }
1485 }
1486
1487 #[test]
1488 fn current_round_trip_is_asymmetric_for_degenerate_polygons() {
1489 for count in 0..3 {
1492 let points: Vec<ShapePoint> =
1493 (0..count).map(|i| ShapePoint::new(i as f32, i as f32)).collect();
1494 let printed = CssShape::polygon(points.into()).print_as_css_value();
1495 assert!(
1496 parse_shape(&printed).is_err(),
1497 "{printed:?} should not re-parse (fewer than 3 points)"
1498 );
1499 }
1500 }
1501
1502 #[test]
1503 fn round_trip_from_the_css_source_side_is_a_fixed_point() {
1504 for source in [
1506 "circle(50px at 100px 100px)",
1507 "ellipse(50px 75px at 10px 20px)",
1508 "polygon(0px 0px, 100px 0px, 100px 100px, 0px 100px)",
1509 "inset(10px 20px 30px 40px)",
1510 "inset(10px round 5px)",
1511 "path(\"M 0 0 L 100 0 L 100 100 Z\")",
1512 ] {
1513 let first = parse_shape(source).unwrap();
1514 let second = parse_shape(&first.print_as_css_value()).unwrap();
1515 assert_eq!(first, second, "{source:?} is not a parse/print fixed point");
1516 }
1517 }
1518}