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(input: &str) -> Result<(String, String), ShapeParseError> {
75 let open_paren = input
76 .find('(')
77 .ok_or_else(|| ShapeParseError::InvalidSyntax("Missing opening parenthesis".into()))?;
78
79 let close_paren = input
80 .rfind(')')
81 .ok_or_else(|| ShapeParseError::InvalidSyntax("Missing closing parenthesis".into()))?;
82
83 if close_paren <= open_paren {
84 return Err(ShapeParseError::InvalidSyntax("Invalid parentheses".into()));
85 }
86
87 let func_name = input[..open_paren].trim().to_string();
88 let args = input[open_paren + 1..close_paren].trim().to_string();
89
90 Ok((func_name, args))
91}
92
93fn parse_circle(args: &str) -> Result<CssShape, ShapeParseError> {
100 let parts: Vec<&str> = args.split_whitespace().collect();
101
102 if parts.is_empty() {
103 return Err(ShapeParseError::MissingParameter("radius".into()));
104 }
105
106 let radius = parse_length(parts[0])?;
107
108 let center = if parts.len() >= 4 && parts[1] == "at" {
109 let x = parse_length(parts[2])?;
110 let y = parse_length(parts[3])?;
111 ShapePoint::new(x, y)
112 } else {
113 ShapePoint::zero() };
115
116 Ok(CssShape::circle(center, radius))
117}
118
119fn parse_ellipse(args: &str) -> Result<CssShape, ShapeParseError> {
125 let parts: Vec<&str> = args.split_whitespace().collect();
126
127 if parts.len() < 2 {
128 return Err(ShapeParseError::MissingParameter(
129 "radius_x and radius_y".into(),
130 ));
131 }
132
133 let radius_x = parse_length(parts[0])?;
134 let radius_y = parse_length(parts[1])?;
135
136 let center = if parts.len() >= 5 && parts[2] == "at" {
137 let x = parse_length(parts[3])?;
138 let y = parse_length(parts[4])?;
139 ShapePoint::new(x, y)
140 } else {
141 ShapePoint::zero()
142 };
143
144 Ok(CssShape::ellipse(center, radius_x, radius_y))
145}
146
147fn parse_polygon(args: &str) -> Result<CssShape, ShapeParseError> {
157 let args = args.trim();
158
159 let point_str = if args.starts_with("nonzero,") || args.starts_with("evenodd,") {
161 let comma = args.find(',').unwrap();
163 &args[comma + 1..]
164 } else {
165 args
166 };
167
168 let pairs: Vec<&str> = point_str.split(',').map(str::trim).collect();
170
171 if pairs.is_empty() {
172 return Err(ShapeParseError::MissingParameter(
173 "at least one point".into(),
174 ));
175 }
176
177 let mut points = Vec::new();
178
179 for pair in pairs {
180 let coords: Vec<&str> = pair.split_whitespace().collect();
181
182 if coords.len() < 2 {
183 return Err(ShapeParseError::InvalidSyntax(format!(
184 "Expected x y pair, got: {pair}"
185 )));
186 }
187
188 let x = parse_length(coords[0])?;
189 let y = parse_length(coords[1])?;
190
191 points.push(ShapePoint::new(x, y));
192 }
193
194 if points.len() < 3 {
195 return Err(ShapeParseError::InvalidSyntax(
196 "Polygon must have at least 3 points".into(),
197 ));
198 }
199
200 Ok(CssShape::polygon(points.into()))
201}
202
203fn parse_inset(args: &str) -> Result<CssShape, ShapeParseError> {
212 let args = args.trim();
213
214 let (inset_str, border_radius) = if let Some(round_pos) = args.find("round") {
216 let insets = args[..round_pos].trim();
217 let radius_str = args[round_pos + 5..].trim();
218 let radius = parse_length(radius_str)?;
219 (insets, Some(radius))
220 } else {
221 (args, None)
222 };
223
224 let values: Vec<&str> = inset_str.split_whitespace().collect();
225
226 if values.is_empty() {
227 return Err(ShapeParseError::MissingParameter("inset values".into()));
228 }
229
230 let (top, right, bottom, left) = match values.len() {
232 1 => {
233 let all = parse_length(values[0])?;
234 (all, all, all, all)
235 }
236 2 => {
237 let vertical = parse_length(values[0])?;
238 let horizontal = parse_length(values[1])?;
239 (vertical, horizontal, vertical, horizontal)
240 }
241 3 => {
242 let top = parse_length(values[0])?;
243 let horizontal = parse_length(values[1])?;
244 let bottom = parse_length(values[2])?;
245 (top, horizontal, bottom, horizontal)
246 }
247 4 => {
248 let top = parse_length(values[0])?;
249 let right = parse_length(values[1])?;
250 let bottom = parse_length(values[2])?;
251 let left = parse_length(values[3])?;
252 (top, right, bottom, left)
253 }
254 _ => {
255 return Err(ShapeParseError::InvalidSyntax(
256 "Too many inset values (max 4)".into(),
257 ));
258 }
259 };
260
261 border_radius.map_or_else(
262 || Ok(CssShape::inset(top, right, bottom, left)),
263 |radius| Ok(CssShape::inset_rounded(top, right, bottom, left, radius)),
264 )
265}
266
267fn parse_path(args: &str) -> Result<CssShape, ShapeParseError> {
272 use crate::corety::AzString;
273
274 let args = args.trim();
275
276 if args.len() < 2 || !args.starts_with('"') || !args.ends_with('"') {
281 return Err(ShapeParseError::InvalidSyntax(
282 "Path data must be quoted".into(),
283 ));
284 }
285
286 let path_data = AzString::from(&args[1..args.len() - 1]);
287
288 Ok(CssShape::Path(crate::shape::ShapePath { data: path_data }))
289}
290
291fn parse_length(s: &str) -> Result<f32, ShapeParseError> {
296 let s = s.trim();
297
298 if let Some(num_str) = s.strip_suffix("px") {
299 num_str
300 .parse::<f32>()
301 .map_err(|_| ShapeParseError::InvalidNumber(s.to_string()))
302 } else if let Some(num_str) = s.strip_suffix('%') {
303 let percent = num_str
304 .parse::<f32>()
305 .map_err(|_| ShapeParseError::InvalidNumber(s.to_string()))?;
306 Ok(percent)
309 } else {
310 s.parse::<f32>()
312 .map_err(|_| ShapeParseError::InvalidNumber(s.to_string()))
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 #![allow(clippy::float_cmp)]
320 use super::*;
321 use crate::{
322 corety::OptionF32,
323 shape::{ShapeCircle, ShapeEllipse, ShapeInset, ShapePath, ShapePolygon},
324 };
325
326 #[test]
327 fn test_parse_circle() {
328 let shape = parse_shape("circle(50px at 100px 100px)").unwrap();
329 match shape {
330 CssShape::Circle(ShapeCircle { center, radius }) => {
331 assert_eq!(radius, 50.0);
332 assert_eq!(center.x, 100.0);
333 assert_eq!(center.y, 100.0);
334 }
335 _ => panic!("Expected Circle"),
336 }
337 }
338
339 #[test]
340 fn test_parse_circle_no_position() {
341 let shape = parse_shape("circle(50px)").unwrap();
342 match shape {
343 CssShape::Circle(ShapeCircle { center, radius }) => {
344 assert_eq!(radius, 50.0);
345 assert_eq!(center.x, 0.0);
346 assert_eq!(center.y, 0.0);
347 }
348 _ => panic!("Expected Circle"),
349 }
350 }
351
352 #[test]
353 fn test_parse_ellipse() {
354 let shape = parse_shape("ellipse(50px 75px at 100px 100px)").unwrap();
355 match shape {
356 CssShape::Ellipse(ShapeEllipse {
357 center,
358 radius_x,
359 radius_y,
360 }) => {
361 assert_eq!(radius_x, 50.0);
362 assert_eq!(radius_y, 75.0);
363 assert_eq!(center.x, 100.0);
364 assert_eq!(center.y, 100.0);
365 }
366 _ => panic!("Expected Ellipse"),
367 }
368 }
369
370 #[test]
371 fn test_parse_polygon_rectangle() {
372 let shape = parse_shape("polygon(0px 0px, 100px 0px, 100px 100px, 0px 100px)").unwrap();
373 match shape {
374 CssShape::Polygon(ShapePolygon { points }) => {
375 assert_eq!(points.as_ref().len(), 4);
376 assert_eq!(points.as_ref()[0].x, 0.0);
377 assert_eq!(points.as_ref()[0].y, 0.0);
378 assert_eq!(points.as_ref()[2].x, 100.0);
379 assert_eq!(points.as_ref()[2].y, 100.0);
380 }
381 _ => panic!("Expected Polygon"),
382 }
383 }
384
385 #[test]
386 fn test_parse_polygon_star() {
387 let shape = parse_shape(
389 "polygon(50px 0px, 61px 35px, 98px 35px, 68px 57px, 79px 91px, 50px 70px, 21px 91px, \
390 32px 57px, 2px 35px, 39px 35px)",
391 )
392 .unwrap();
393 match shape {
394 CssShape::Polygon(ShapePolygon { points }) => {
395 assert_eq!(points.as_ref().len(), 10); }
397 _ => panic!("Expected Polygon"),
398 }
399 }
400
401 #[test]
402 fn test_parse_inset() {
403 let shape = parse_shape("inset(10px 20px 30px 40px)").unwrap();
404 match shape {
405 CssShape::Inset(ShapeInset {
406 inset_top,
407 inset_right,
408 inset_bottom,
409 inset_left,
410 border_radius,
411 }) => {
412 assert_eq!(inset_top, 10.0);
413 assert_eq!(inset_right, 20.0);
414 assert_eq!(inset_bottom, 30.0);
415 assert_eq!(inset_left, 40.0);
416 assert!(matches!(border_radius, OptionF32::None));
417 }
418 _ => panic!("Expected Inset"),
419 }
420 }
421
422 #[test]
423 fn test_parse_inset_rounded() {
424 let shape = parse_shape("inset(10px round 5px)").unwrap();
425 match shape {
426 CssShape::Inset(ShapeInset {
427 inset_top,
428 inset_right,
429 inset_bottom,
430 inset_left,
431 border_radius,
432 }) => {
433 assert_eq!(inset_top, 10.0);
434 assert_eq!(inset_right, 10.0);
435 assert_eq!(inset_bottom, 10.0);
436 assert_eq!(inset_left, 10.0);
437 assert!(matches!(border_radius, OptionF32::Some(r) if r == 5.0));
438 }
439 _ => panic!("Expected Inset"),
440 }
441 }
442
443 #[test]
444 fn test_parse_path() {
445 let shape = parse_shape(r#"path("M 0 0 L 100 0 L 100 100 Z")"#).unwrap();
446 match shape {
447 CssShape::Path(ShapePath { data }) => {
448 assert_eq!(data.as_str(), "M 0 0 L 100 0 L 100 100 Z");
449 }
450 _ => panic!("Expected Path"),
451 }
452 }
453
454 #[test]
455 fn test_invalid_function() {
456 let result = parse_shape("unknown(50px)");
457 assert!(result.is_err());
458 }
459
460 #[test]
461 fn test_empty_input() {
462 let result = parse_shape("");
463 assert!(matches!(result, Err(ShapeParseError::EmptyInput)));
464 }
465}
466
467#[cfg(test)]
468#[allow(clippy::float_cmp, clippy::unreadable_literal)]
469mod autotest_generated {
470 use std::panic::{catch_unwind, AssertUnwindSafe};
480
481 use super::*;
482 use crate::{
483 corety::OptionF32,
484 shape::{ShapeCircle, ShapeEllipse, ShapeInset, ShapePath, ShapePolygon},
485 };
486
487 fn circle_of(shape: &CssShape) -> ShapeCircle {
490 match shape {
491 CssShape::Circle(c) => *c,
492 other => panic!("expected Circle, got {other:?}"),
493 }
494 }
495
496 fn ellipse_of(shape: &CssShape) -> ShapeEllipse {
497 match shape {
498 CssShape::Ellipse(e) => *e,
499 other => panic!("expected Ellipse, got {other:?}"),
500 }
501 }
502
503 fn inset_of(shape: &CssShape) -> ShapeInset {
504 match shape {
505 CssShape::Inset(i) => *i,
506 other => panic!("expected Inset, got {other:?}"),
507 }
508 }
509
510 fn polygon_points(shape: &CssShape) -> Vec<ShapePoint> {
511 match shape {
512 CssShape::Polygon(ShapePolygon { points }) => points.as_ref().to_vec(),
513 other => panic!("expected Polygon, got {other:?}"),
514 }
515 }
516
517 fn path_data(shape: &CssShape) -> String {
518 match shape {
519 CssShape::Path(ShapePath { data }) => data.as_str().to_string(),
520 other => panic!("expected Path, got {other:?}"),
521 }
522 }
523
524 fn radius_of(input: &str) -> f32 {
525 circle_of(&parse_shape(input).unwrap()).radius
526 }
527
528 #[test]
533 fn bug_parse_path_bare_quote_char_panics_on_reversed_slice() {
534 let parsed = catch_unwind(AssertUnwindSafe(|| parse_shape("path(\")")));
542 assert!(
543 parsed.is_ok(),
544 "parse_shape(r#\"path(\")\"#) panicked instead of returning Err: parse_path slices \
545 args[1..len-1] on a 1-char arg"
546 );
547 assert!(matches!(
548 parsed.unwrap(),
549 Err(ShapeParseError::InvalidSyntax(_))
550 ));
551 }
552
553 #[test]
554 fn bug_parse_path_direct_single_quote_arg_panics() {
555 let parsed = catch_unwind(AssertUnwindSafe(|| parse_path("\"")));
558 assert!(
559 parsed.is_ok(),
560 "parse_path(\"\\\"\") panicked instead of returning Err"
561 );
562 assert!(parsed.unwrap().is_err());
563 }
564
565 #[test]
570 fn shape_empty_and_whitespace_only_input_is_empty_input_err() {
571 assert!(matches!(parse_shape(""), Err(ShapeParseError::EmptyInput)));
572 assert!(matches!(
573 parse_shape(" "),
574 Err(ShapeParseError::EmptyInput)
575 ));
576 assert!(matches!(
577 parse_shape("\t\n\r \x0c"),
578 Err(ShapeParseError::EmptyInput)
579 ));
580 assert!(parse_shape("\u{00a0}\u{2003}").is_err());
582 }
583
584 #[test]
585 fn shape_garbage_input_is_rejected_without_panicking() {
586 for garbage in [
587 "!!!",
588 ";;;;",
589 "\0\0\0",
590 "\u{7}\u{1b}[0m",
591 "circle",
592 "circle 50px",
593 "()",
594 "(",
595 ")",
596 ")(",
597 ")circle(",
598 "((((",
599 "))))",
600 "-",
601 "50px",
602 "{}",
603 "circle{50px}",
604 "circle[50px]",
605 "<circle r=\"50\"/>",
606 ] {
607 let parsed = catch_unwind(AssertUnwindSafe(|| parse_shape(garbage)));
608 assert!(parsed.is_ok(), "parse_shape({garbage:?}) panicked");
609 assert!(
610 parsed.unwrap().is_err(),
611 "parse_shape({garbage:?}) unexpectedly succeeded"
612 );
613 }
614 }
615
616 #[test]
617 fn shape_missing_parens_report_which_one_is_missing() {
618 assert!(matches!(
619 parse_shape("circle 50px"),
620 Err(ShapeParseError::InvalidSyntax(ref m)) if m.contains("opening")
621 ));
622 assert!(matches!(
623 parse_shape("circle(50px"),
624 Err(ShapeParseError::InvalidSyntax(ref m)) if m.contains("closing")
625 ));
626 assert!(matches!(
628 parse_shape(")circle("),
629 Err(ShapeParseError::InvalidSyntax(ref m)) if m.contains("Invalid parentheses")
630 ));
631 }
632
633 #[test]
634 fn shape_unknown_function_names_are_reported_verbatim() {
635 assert!(matches!(
636 parse_shape("unknown(50px)"),
637 Err(ShapeParseError::UnknownFunction(ref f)) if f == "unknown"
638 ));
639 assert!(matches!(
641 parse_shape("()"),
642 Err(ShapeParseError::UnknownFunction(ref f)) if f.is_empty()
643 ));
644 }
645
646 #[test]
647 fn current_shape_function_names_are_case_sensitive() {
648 assert!(matches!(
652 parse_shape("CIRCLE(50px)"),
653 Err(ShapeParseError::UnknownFunction(ref f)) if f == "CIRCLE"
654 ));
655 assert!(parse_shape("Inset(10px)").is_err());
656 }
657
658 #[test]
659 fn current_shape_trailing_junk_after_the_last_paren_is_silently_dropped() {
660 assert_eq!(radius_of("circle(50px) garbage"), 50.0);
665 assert_eq!(radius_of("circle(50px);garbage"), 50.0);
666 assert_eq!(radius_of("circle(50px)!!!"), 50.0);
667 assert!(matches!(
669 parse_shape("junk circle(50px)"),
670 Err(ShapeParseError::UnknownFunction(ref f)) if f == "junk circle"
671 ));
672 }
673
674 #[test]
675 fn shape_surrounding_whitespace_is_trimmed_before_dispatch() {
676 assert_eq!(radius_of(" circle(50px) "), 50.0);
677 assert_eq!(radius_of("\n\tcircle( 50px )\n"), 50.0);
678 }
679
680 #[test]
681 fn shape_extra_closing_paren_inside_args_is_rejected_not_ignored() {
682 assert!(matches!(
684 parse_shape("circle(50px))"),
685 Err(ShapeParseError::InvalidNumber(ref n)) if n == "50px)"
686 ));
687 assert!(parse_shape("circle((50px)").is_err());
688 }
689
690 #[test]
691 fn shape_unicode_input_does_not_panic_or_split_a_codepoint() {
692 for input in [
693 "\u{1F600}",
694 "circle(\u{1F600})",
695 "\u{1F600}(50px)",
696 "cercle\u{301}(50px)", "circle(50px\u{200b})", "circle(\u{FF15}\u{FF10}px)", "円(50px)",
700 "\u{202e}circle(50px)", "polygon(\u{1F4A9} \u{1F4A9}, 0 0, 1 1)",
702 "path(\u{1F600})",
703 "inset(\u{1F600} round \u{1F600})",
704 "ellipse(\u{1F600} \u{1F600})",
705 ] {
706 let parsed = catch_unwind(AssertUnwindSafe(|| parse_shape(input)));
707 assert!(parsed.is_ok(), "parse_shape({input:?}) panicked");
708 assert!(
709 parsed.unwrap().is_err(),
710 "parse_shape({input:?}) unexpectedly succeeded"
711 );
712 }
713 }
714
715 #[test]
716 fn shape_multibyte_name_slices_on_a_char_boundary() {
717 assert!(matches!(
720 parse_shape("\u{1F600}(50px)"),
721 Err(ShapeParseError::UnknownFunction(ref f)) if f == "\u{1F600}"
722 ));
723 }
724
725 #[test]
726 fn shape_deeply_nested_parens_do_not_stack_overflow() {
727 let depth = 10_000;
730 let nested = format!("{}{}", "(".repeat(depth), ")".repeat(depth));
731 assert!(matches!(
732 parse_shape(&nested),
733 Err(ShapeParseError::UnknownFunction(ref f)) if f.is_empty()
734 ));
735
736 let nested_circle = format!("circle{}50px{}", "(".repeat(5_000), ")".repeat(5_000));
737 assert!(matches!(
738 parse_shape(&nested_circle),
739 Err(ShapeParseError::InvalidNumber(_))
740 ));
741 }
742
743 #[test]
744 fn shape_extremely_long_input_does_not_hang() {
745 let long_garbage = "x".repeat(1_000_000);
747 assert!(matches!(
748 parse_shape(&long_garbage),
749 Err(ShapeParseError::InvalidSyntax(_))
750 ));
751
752 let huge = format!("circle({}px)", "9".repeat(50_000));
754 let radius = radius_of(&huge);
755 assert!(radius.is_infinite() && radius.is_sign_positive());
756 }
757
758 #[test]
759 fn shape_parsing_is_deterministic() {
760 let input = "polygon(0px 0px, 100px 0px, 50px 100px)";
761 assert_eq!(parse_shape(input).unwrap(), parse_shape(input).unwrap());
762 assert_eq!(
763 parse_shape("!!!").unwrap_err(),
764 parse_shape("!!!").unwrap_err()
765 );
766 }
767
768 #[test]
769 fn shape_minimal_valid_input_per_function() {
770 assert_eq!(circle_of(&parse_shape("circle(1)").unwrap()).radius, 1.0);
771 assert_eq!(
772 ellipse_of(&parse_shape("ellipse(1 2)").unwrap()).radius_y,
773 2.0
774 );
775 assert_eq!(
776 polygon_points(&parse_shape("polygon(0 0,1 0,0 1)").unwrap()).len(),
777 3
778 );
779 assert_eq!(inset_of(&parse_shape("inset(0)").unwrap()).inset_top, 0.0);
780 assert_eq!(path_data(&parse_shape("path(\"\")").unwrap()), "");
781 }
782
783 #[test]
788 fn function_empty_and_whitespace_input_is_err() {
789 assert!(parse_function("").is_err());
790 assert!(parse_function(" \t\n").is_err());
791 }
792
793 #[test]
794 fn function_splits_and_trims_name_and_args() {
795 let (name, args) = parse_function(" circle ( 50px ) ").unwrap();
796 assert_eq!(name, "circle");
797 assert_eq!(args, "50px");
798
799 let (name, args) = parse_function("()").unwrap();
801 assert!(name.is_empty() && args.is_empty());
802 }
803
804 #[test]
805 fn function_uses_first_open_and_last_close_paren() {
806 let (name, args) = parse_function("a(b(c))").unwrap();
807 assert_eq!(name, "a");
808 assert_eq!(
809 args, "b(c)",
810 "rfind(')') must take the outermost close paren"
811 );
812 }
813
814 #[test]
815 fn function_rejects_reversed_and_missing_parens() {
816 assert!(matches!(
817 parse_function(")("),
818 Err(ShapeParseError::InvalidSyntax(ref m)) if m.contains("Invalid parentheses")
819 ));
820 assert!(parse_function("no parens here").is_err());
821 assert!(parse_function("circle(").is_err());
822 assert!(parse_function("circle)").is_err());
823 }
824
825 #[test]
826 fn function_handles_pathological_lengths_without_panicking() {
827 let long = "y".repeat(1_000_000);
828 assert!(parse_function(&long).is_err());
829
830 let long_args = format!("f({})", "z".repeat(1_000_000));
832 let (name, args) = parse_function(&long_args).unwrap();
833 assert_eq!(name, "f");
834 assert_eq!(args.len(), 1_000_000);
835 }
836
837 #[test]
838 fn function_multibyte_args_round_trip_intact() {
839 let (name, args) = parse_function("f(\u{1F600}\u{0301})").unwrap();
840 assert_eq!(name, "f");
841 assert_eq!(args, "\u{1F600}\u{0301}");
842 }
843
844 #[test]
849 fn circle_empty_or_whitespace_args_report_the_missing_radius() {
850 assert!(matches!(
851 parse_circle(""),
852 Err(ShapeParseError::MissingParameter(ref p)) if p == "radius"
853 ));
854 assert!(matches!(
855 parse_circle(" \t\n "),
856 Err(ShapeParseError::MissingParameter(_))
857 ));
858 }
859
860 #[test]
861 fn circle_garbage_radius_is_an_invalid_number() {
862 assert!(matches!(
863 parse_circle("abc"),
864 Err(ShapeParseError::InvalidNumber(ref n)) if n == "abc"
865 ));
866 assert!(parse_circle("at 10px 10px").is_err());
867 assert!(parse_circle("50px at 10px abc").is_err());
868 }
869
870 #[test]
871 fn current_circle_ignores_a_malformed_at_clause_instead_of_erroring() {
872 let truncated = circle_of(&parse_circle("50px at 100px").unwrap());
876 assert_eq!(truncated.center, ShapePoint::zero());
877
878 let wrong_case = circle_of(&parse_circle("50px AT 100px 100px").unwrap());
879 assert_eq!(wrong_case.center, ShapePoint::zero());
880
881 let bad_keyword = circle_of(&parse_circle("50px on 100px 100px").unwrap());
882 assert_eq!(bad_keyword.center, ShapePoint::zero());
883
884 let extra = circle_of(&parse_circle("50px at 1px 2px 3px 4px").unwrap());
886 assert_eq!(extra.center, ShapePoint::new(1.0, 2.0));
887 }
888
889 #[test]
890 fn current_circle_accepts_a_negative_radius() {
891 assert_eq!(radius_of("circle(-50px)"), -50.0);
893 let neg_zero = radius_of("circle(-0px)");
895 assert!(neg_zero == 0.0 && neg_zero.is_sign_negative());
896 }
897
898 #[test]
899 fn circle_non_finite_and_saturating_radii_do_not_panic() {
900 assert!(radius_of("circle(NaN)").is_nan());
902 assert!(radius_of("circle(NaNpx)").is_nan());
903 assert!(radius_of("circle(inf)").is_infinite());
904 assert!(radius_of("circle(infinitypx)").is_infinite());
905 assert!(radius_of("circle(-inf)").is_sign_negative());
906
907 assert!(radius_of("circle(1e39px)").is_infinite());
909 assert_eq!(radius_of("circle(1e-46px)"), 0.0);
910 assert_eq!(
911 radius_of("circle(9223372036854775807px)"),
912 9223372036854775807.0_f32
913 );
914 }
915
916 #[test]
917 fn circle_percentages_are_currently_kept_as_raw_numbers() {
918 assert_eq!(radius_of("circle(50%)"), 50.0);
921 assert_eq!(radius_of("circle(50%)"), radius_of("circle(50px)"));
922 }
923
924 #[test]
929 fn ellipse_requires_two_radii() {
930 assert!(matches!(
931 parse_ellipse(""),
932 Err(ShapeParseError::MissingParameter(ref p)) if p.contains("radius_x")
933 ));
934 assert!(matches!(
935 parse_ellipse("50px"),
936 Err(ShapeParseError::MissingParameter(_))
937 ));
938 assert!(parse_ellipse("50px abc").is_err());
939 }
940
941 #[test]
942 fn current_ellipse_ignores_a_truncated_at_clause() {
943 let truncated = ellipse_of(&parse_ellipse("50px 75px at 100px").unwrap());
945 assert_eq!(truncated.center, ShapePoint::zero());
946
947 let no_keyword = ellipse_of(&parse_ellipse("1px 2px 3px 4px 5px").unwrap());
949 assert_eq!(no_keyword.center, ShapePoint::zero());
950 assert_eq!((no_keyword.radius_x, no_keyword.radius_y), (1.0, 2.0));
951 }
952
953 #[test]
954 fn ellipse_valid_input_maps_radii_and_centre_in_order() {
955 let e = ellipse_of(&parse_shape("ellipse(50px 75px at 10px 20px)").unwrap());
956 assert_eq!(e.radius_x, 50.0);
957 assert_eq!(e.radius_y, 75.0);
958 assert_eq!(e.center, ShapePoint::new(10.0, 20.0));
959 }
960
961 #[test]
962 fn ellipse_non_finite_radii_do_not_panic() {
963 let e = ellipse_of(&parse_shape("ellipse(NaN inf)").unwrap());
964 assert!(e.radius_x.is_nan());
965 assert!(e.radius_y.is_infinite());
966 }
967
968 #[test]
973 fn polygon_empty_and_whitespace_args_are_rejected() {
974 assert!(matches!(
978 parse_polygon(""),
979 Err(ShapeParseError::InvalidSyntax(_))
980 ));
981 assert!(matches!(
982 parse_polygon(" \t "),
983 Err(ShapeParseError::InvalidSyntax(_))
984 ));
985 }
986
987 #[test]
988 fn polygon_needs_at_least_three_points() {
989 assert!(matches!(
990 parse_polygon("0 0"),
991 Err(ShapeParseError::InvalidSyntax(ref m)) if m.contains("at least 3 points")
992 ));
993 assert!(matches!(
994 parse_polygon("0 0, 1 1"),
995 Err(ShapeParseError::InvalidSyntax(ref m)) if m.contains("at least 3 points")
996 ));
997 assert_eq!(
998 polygon_points(&parse_polygon("0 0, 1 1, 2 2").unwrap()).len(),
999 3
1000 );
1001 }
1002
1003 #[test]
1004 fn polygon_malformed_pairs_are_rejected() {
1005 assert!(parse_polygon("0 0, 1 1, 2").is_err());
1007 assert!(parse_polygon("0 0, 1 1, 2 2,").is_err());
1008 assert!(parse_polygon("0 0,, 1 1, 2 2").is_err());
1009 assert!(parse_polygon(",0 0, 1 1, 2 2").is_err());
1010 assert!(parse_polygon("0 0, 1 1, abc def").is_err());
1011 }
1012
1013 #[test]
1014 fn polygon_fill_rule_prefix_is_stripped_only_when_comma_attached() {
1015 let nonzero = polygon_points(&parse_polygon("nonzero, 0 0, 1 0, 1 1").unwrap());
1016 assert_eq!(nonzero.len(), 3);
1017 assert_eq!(nonzero[0], ShapePoint::zero());
1018
1019 let evenodd = polygon_points(&parse_polygon("evenodd, 0 0, 1 0, 1 1").unwrap());
1020 assert_eq!(evenodd.len(), 3);
1021
1022 assert!(matches!(
1025 parse_polygon("nonzero , 0 0, 1 0, 1 1"),
1026 Err(ShapeParseError::InvalidSyntax(_))
1027 ));
1028 assert!(matches!(
1030 parse_polygon("nonzero 0 0, 1 0, 1 1"),
1031 Err(ShapeParseError::InvalidNumber(ref n)) if n == "nonzero"
1032 ));
1033 assert!(parse_polygon("nonzero,").is_err());
1035 }
1036
1037 #[test]
1038 fn current_polygon_ignores_extra_coordinates_in_a_pair() {
1039 let points = polygon_points(&parse_polygon("0 0 999, 1 1 999, 2 2 999").unwrap());
1041 assert_eq!(points.len(), 3);
1042 assert_eq!(points[2], ShapePoint::new(2.0, 2.0));
1043 }
1044
1045 #[test]
1046 fn current_polygon_mixes_units_silently() {
1047 let points = polygon_points(&parse_polygon("0% 0px, 100 0%, 50px 100").unwrap());
1049 assert_eq!(points[1], ShapePoint::new(100.0, 0.0));
1050 assert_eq!(points[2], ShapePoint::new(50.0, 100.0));
1051 }
1052
1053 #[test]
1054 fn polygon_non_finite_coordinates_do_not_panic() {
1055 let points = polygon_points(&parse_polygon("NaN 0, inf 1, -inf 2").unwrap());
1056 assert!(points[0].x.is_nan());
1057 assert!(points[1].x.is_infinite() && points[1].x.is_sign_positive());
1058 assert!(points[2].x.is_infinite() && points[2].x.is_sign_negative());
1059 }
1060
1061 #[test]
1062 fn polygon_with_twenty_thousand_points_does_not_hang() {
1063 let mut args = String::with_capacity(20_000 * 10);
1064 for i in 0..20_000 {
1065 if i > 0 {
1066 args.push(',');
1067 }
1068 args.push_str("1px 2px");
1069 }
1070 let points = polygon_points(&parse_polygon(&args).unwrap());
1071 assert_eq!(points.len(), 20_000);
1072 assert_eq!(points[19_999], ShapePoint::new(1.0, 2.0));
1073 }
1074
1075 #[test]
1080 fn inset_empty_args_report_missing_values() {
1081 assert!(matches!(
1082 parse_inset(""),
1083 Err(ShapeParseError::MissingParameter(ref p)) if p.contains("inset values")
1084 ));
1085 assert!(matches!(
1086 parse_inset(" "),
1087 Err(ShapeParseError::MissingParameter(_))
1088 ));
1089 }
1090
1091 #[test]
1092 fn inset_shorthand_expansion_follows_the_margin_rules() {
1093 let one = inset_of(&parse_inset("10px").unwrap());
1094 assert_eq!(
1095 (
1096 one.inset_top,
1097 one.inset_right,
1098 one.inset_bottom,
1099 one.inset_left
1100 ),
1101 (10.0, 10.0, 10.0, 10.0)
1102 );
1103
1104 let two = inset_of(&parse_inset("10px 20px").unwrap());
1105 assert_eq!(
1106 (
1107 two.inset_top,
1108 two.inset_right,
1109 two.inset_bottom,
1110 two.inset_left
1111 ),
1112 (10.0, 20.0, 10.0, 20.0)
1113 );
1114
1115 let three = inset_of(&parse_inset("10px 20px 30px").unwrap());
1116 assert_eq!(
1117 (
1118 three.inset_top,
1119 three.inset_right,
1120 three.inset_bottom,
1121 three.inset_left
1122 ),
1123 (10.0, 20.0, 30.0, 20.0)
1124 );
1125
1126 let four = inset_of(&parse_inset("10px 20px 30px 40px").unwrap());
1127 assert_eq!(
1128 (
1129 four.inset_top,
1130 four.inset_right,
1131 four.inset_bottom,
1132 four.inset_left
1133 ),
1134 (10.0, 20.0, 30.0, 40.0)
1135 );
1136 }
1137
1138 #[test]
1139 fn inset_rejects_more_than_four_values() {
1140 assert!(matches!(
1141 parse_inset("1px 2px 3px 4px 5px"),
1142 Err(ShapeParseError::InvalidSyntax(ref m)) if m.contains("max 4")
1143 ));
1144 let many = ["1px"; 1_000].join(" ");
1146 assert!(matches!(
1147 parse_inset(&many),
1148 Err(ShapeParseError::InvalidSyntax(_))
1149 ));
1150 }
1151
1152 #[test]
1153 fn inset_round_keyword_splits_insets_from_the_radius() {
1154 let rounded = inset_of(&parse_inset("10px 20px round 5px").unwrap());
1155 assert_eq!(rounded.inset_top, 10.0);
1156 assert_eq!(rounded.inset_right, 20.0);
1157 assert!(matches!(rounded.border_radius, OptionF32::Some(r) if r == 5.0));
1158
1159 let plain = inset_of(&parse_inset("10px").unwrap());
1160 assert!(matches!(plain.border_radius, OptionF32::None));
1161 }
1162
1163 #[test]
1164 fn inset_malformed_round_clauses_are_rejected() {
1165 assert!(matches!(
1167 parse_inset("10px round"),
1168 Err(ShapeParseError::InvalidNumber(ref n)) if n.is_empty()
1169 ));
1170 assert!(matches!(
1173 parse_inset("round 5px"),
1174 Err(ShapeParseError::MissingParameter(_))
1175 ));
1176 assert!(parse_inset("10px round 5px round 6px").is_err());
1178 assert!(matches!(
1181 parse_inset("10px roundup"),
1182 Err(ShapeParseError::InvalidNumber(ref n)) if n == "up"
1183 ));
1184 }
1185
1186 #[test]
1187 fn inset_non_finite_and_negative_values_do_not_panic() {
1188 let nan = inset_of(&parse_inset("NaN").unwrap());
1189 assert!(nan.inset_top.is_nan() && nan.inset_left.is_nan());
1190
1191 let huge = inset_of(&parse_inset("1e39px round 1e39px").unwrap());
1192 assert!(huge.inset_top.is_infinite());
1193 assert!(matches!(huge.border_radius, OptionF32::Some(r) if r.is_infinite()));
1194
1195 let negative = inset_of(&parse_inset("-10px round -5px").unwrap());
1197 assert_eq!(negative.inset_top, -10.0);
1198 assert!(matches!(negative.border_radius, OptionF32::Some(r) if r == -5.0));
1199 }
1200
1201 #[test]
1206 fn path_requires_double_quotes_on_both_ends() {
1207 for unquoted in [
1208 "", " ", "M 0 0", "\"M 0 0", "M 0 0\"",
1209 "'M 0 0'", "`M 0 0`",
1211 ] {
1212 let parsed = catch_unwind(AssertUnwindSafe(|| parse_path(unquoted)));
1213 assert!(parsed.is_ok(), "parse_path({unquoted:?}) panicked");
1214 assert!(
1215 matches!(parsed.unwrap(), Err(ShapeParseError::InvalidSyntax(ref m)) if m.contains("quoted")),
1216 "parse_path({unquoted:?}) should be an unquoted-syntax error"
1217 );
1218 }
1219 }
1220
1221 #[test]
1222 fn path_strips_exactly_one_quote_from_each_end() {
1223 assert_eq!(path_data(&parse_path("\"\"").unwrap()), "");
1224 assert_eq!(path_data(&parse_path("\"M 0 0 Z\"").unwrap()), "M 0 0 Z");
1225 assert_eq!(path_data(&parse_path("\"a\"b\"").unwrap()), "a\"b");
1227 }
1228
1229 #[test]
1230 fn current_path_data_is_stored_without_validation() {
1231 assert_eq!(
1234 path_data(&parse_shape("path(\"not svg at all\")").unwrap()),
1235 "not svg at all"
1236 );
1237 assert_eq!(
1238 path_data(&parse_shape("path(\"\u{1F600}\")").unwrap()),
1239 "\u{1F600}"
1240 );
1241 assert_eq!(
1242 path_data(&parse_shape("path(\"M 0 0 L NaN inf\")").unwrap()),
1243 "M 0 0 L NaN inf"
1244 );
1245 }
1246
1247 #[test]
1248 fn path_multibyte_content_slices_on_char_boundaries() {
1249 let data = path_data(&parse_path("\"\u{1F600}\u{0301}\u{1F600}\"").unwrap());
1252 assert_eq!(data, "\u{1F600}\u{0301}\u{1F600}");
1253 }
1254
1255 #[test]
1256 fn path_with_a_megabyte_of_data_does_not_hang() {
1257 let inner = "L 1 1 ".repeat(150_000);
1258 let arg = format!("\"{inner}\"");
1259 assert_eq!(path_data(&parse_path(&arg).unwrap()), inner);
1260 }
1261
1262 #[test]
1267 fn length_empty_and_whitespace_are_invalid_numbers() {
1268 assert!(matches!(
1269 parse_length(""),
1270 Err(ShapeParseError::InvalidNumber(ref n)) if n.is_empty()
1271 ));
1272 assert!(matches!(
1273 parse_length(" \t\n "),
1274 Err(ShapeParseError::InvalidNumber(ref n)) if n.is_empty()
1275 ));
1276 assert!(matches!(
1278 parse_length("px"),
1279 Err(ShapeParseError::InvalidNumber(ref n)) if n == "px"
1280 ));
1281 assert!(parse_length("%").is_err());
1282 }
1283
1284 #[test]
1285 fn length_accepts_px_percent_and_unitless() {
1286 assert_eq!(parse_length("50px").unwrap(), 50.0);
1287 assert_eq!(parse_length("50%").unwrap(), 50.0);
1288 assert_eq!(parse_length("50").unwrap(), 50.0);
1289 assert_eq!(parse_length(" 50px ").unwrap(), 50.0);
1290 assert_eq!(parse_length("+5px").unwrap(), 5.0);
1291 assert_eq!(parse_length(".5px").unwrap(), 0.5);
1292 assert_eq!(parse_length("5.px").unwrap(), 5.0);
1293 assert_eq!(parse_length("5e2px").unwrap(), 500.0);
1294 }
1295
1296 #[test]
1297 fn current_length_rejects_uppercase_units_and_other_css_units() {
1298 for unsupported in [
1301 "50PX", "50Px", "1em", "1rem", "1vh", "1vw", "1pt", "1cm", "1fr",
1302 ] {
1303 assert!(
1304 parse_length(unsupported).is_err(),
1305 "parse_length({unsupported:?}) unexpectedly succeeded"
1306 );
1307 }
1308 }
1309
1310 #[test]
1311 fn length_rejects_non_css_numeric_syntax() {
1312 for bad in [
1313 "1_000px", "0x10px", "1,5px", "50px50px", "50%%", "50%px", "--5px", "5 0px", "50 px",
1314 "1e", "e5", "0b101", "١٠px", ] {
1316 let parsed = catch_unwind(AssertUnwindSafe(|| parse_length(bad)));
1317 assert!(parsed.is_ok(), "parse_length({bad:?}) panicked");
1318 assert!(
1319 parsed.unwrap().is_err(),
1320 "parse_length({bad:?}) unexpectedly succeeded"
1321 );
1322 }
1323 }
1324
1325 #[test]
1326 fn length_saturates_on_overflow_and_flushes_on_underflow() {
1327 assert!(parse_length("1e39").unwrap().is_infinite());
1328 assert!(parse_length("1e39px").unwrap().is_sign_positive());
1329 assert!(parse_length("-1e39px").unwrap().is_sign_negative());
1330 assert_eq!(parse_length("1e-46px").unwrap(), 0.0);
1331 assert_eq!(parse_length("-1e-46px").unwrap(), -0.0);
1332 assert_eq!(parse_length(&format!("{}px", f32::MAX)).unwrap(), f32::MAX);
1334 assert_eq!(parse_length(&format!("{}px", f32::MIN)).unwrap(), f32::MIN);
1335 assert_eq!(
1336 parse_length(&format!("{}px", f32::MIN_POSITIVE)).unwrap(),
1337 f32::MIN_POSITIVE
1338 );
1339 assert!(parse_length(&format!("{}px", f64::MAX))
1341 .unwrap()
1342 .is_infinite());
1343 }
1344
1345 #[test]
1346 fn current_length_accepts_nan_and_infinity_spellings() {
1347 assert!(parse_length("NaN").unwrap().is_nan());
1351 assert!(parse_length("nan").unwrap().is_nan());
1352 assert!(parse_length("NaNpx").unwrap().is_nan());
1353 assert!(parse_length("-NaN%").unwrap().is_nan());
1354 assert!(parse_length("inf").unwrap().is_infinite());
1355 assert!(parse_length("infinity").unwrap().is_infinite());
1356 assert!(parse_length("INFpx").unwrap().is_infinite());
1357 assert!(parse_length("-inf").unwrap().is_sign_negative());
1358 }
1359
1360 #[test]
1361 fn length_preserves_the_sign_of_negative_zero() {
1362 let negative_zero = parse_length("-0px").unwrap();
1363 assert!(negative_zero == 0.0 && negative_zero.is_sign_negative());
1364 assert!(parse_length("0px").unwrap().is_sign_positive());
1365 }
1366
1367 #[test]
1368 fn length_handles_huge_digit_strings_without_hanging() {
1369 let huge = format!("{}px", "9".repeat(100_000));
1372 assert!(parse_length(&huge).unwrap().is_infinite());
1373
1374 let padded = format!("{}1px", "0".repeat(100_000));
1376 assert_eq!(parse_length(&padded).unwrap(), 1.0);
1377
1378 let garbage = "q".repeat(1_000_000);
1380 assert!(parse_length(&garbage).is_err());
1381 }
1382
1383 #[test]
1384 fn length_error_payload_is_the_trimmed_input() {
1385 assert!(matches!(
1386 parse_length(" bogus "),
1387 Err(ShapeParseError::InvalidNumber(ref n)) if n == "bogus"
1388 ));
1389 assert!(matches!(
1391 parse_length("bogus px"),
1392 Err(ShapeParseError::InvalidNumber(ref n)) if n == "bogus px"
1393 ));
1394 }
1395
1396 #[test]
1401 fn error_display_is_non_empty_and_names_the_variant() {
1402 let cases = [
1403 (
1404 ShapeParseError::UnknownFunction("blob".into()),
1405 "Unknown shape function",
1406 "blob",
1407 ),
1408 (
1409 ShapeParseError::MissingParameter("radius".into()),
1410 "Missing required parameter",
1411 "radius",
1412 ),
1413 (
1414 ShapeParseError::InvalidNumber("12abc".into()),
1415 "Invalid numeric value",
1416 "12abc",
1417 ),
1418 (
1419 ShapeParseError::InvalidSyntax("bad parens".into()),
1420 "Invalid syntax",
1421 "bad parens",
1422 ),
1423 ];
1424 for (err, prefix, payload) in cases {
1425 let rendered = err.to_string();
1426 assert!(
1427 rendered.starts_with(prefix),
1428 "{rendered:?} lacks prefix {prefix:?}"
1429 );
1430 assert!(
1431 rendered.contains(payload),
1432 "{rendered:?} lost its payload {payload:?}"
1433 );
1434 }
1435 assert_eq!(ShapeParseError::EmptyInput.to_string(), "Empty input");
1436 }
1437
1438 #[test]
1439 fn error_display_survives_hostile_payloads() {
1440 let payloads = [
1443 String::new(),
1444 "{}{0}{name}%s%n".to_string(),
1445 "\u{1F600}\u{0301}\u{202e}".to_string(),
1446 "\0\u{7}\n\t".to_string(),
1447 "x".repeat(100_000),
1448 ];
1449 for payload in payloads {
1450 for err in [
1451 ShapeParseError::UnknownFunction(payload.clone()),
1452 ShapeParseError::MissingParameter(payload.clone()),
1453 ShapeParseError::InvalidNumber(payload.clone()),
1454 ShapeParseError::InvalidSyntax(payload.clone()),
1455 ] {
1456 let rendered = catch_unwind(AssertUnwindSafe(|| err.to_string()));
1457 assert!(rendered.is_ok(), "Display panicked on payload {payload:?}");
1458 let rendered = rendered.unwrap();
1459 assert!(!rendered.is_empty());
1460 assert!(
1461 rendered.ends_with(&payload),
1462 "payload must be emitted literally, not re-formatted"
1463 );
1464 }
1465 }
1466 }
1467
1468 #[test]
1469 fn error_variants_render_distinctly_and_compare_by_value() {
1470 let same_payload = "x".to_string();
1471 let unknown = ShapeParseError::UnknownFunction(same_payload.clone());
1472 let missing = ShapeParseError::MissingParameter(same_payload.clone());
1473 assert_ne!(unknown, missing);
1474 assert_ne!(unknown.to_string(), missing.to_string());
1475 assert_eq!(unknown, ShapeParseError::UnknownFunction(same_payload));
1476 assert_eq!(unknown.clone(), unknown);
1477 assert!(!format!("{unknown:?}").is_empty());
1479 }
1480
1481 fn assert_round_trips(shape: &CssShape) {
1486 let printed = shape.print_as_css_value();
1487 let reparsed =
1488 parse_shape(&printed).unwrap_or_else(|e| panic!("{printed:?} did not re-parse: {e}"));
1489 assert_eq!(
1490 &reparsed, shape,
1491 "round-trip changed the shape via {printed:?}"
1492 );
1493 assert_eq!(reparsed.print_as_css_value(), printed);
1495 }
1496
1497 #[test]
1498 fn round_trip_every_shape_variant() {
1499 assert_round_trips(&CssShape::circle(ShapePoint::new(10.0, 20.0), 50.0));
1500 assert_round_trips(&CssShape::ellipse(ShapePoint::new(1.5, -2.5), 3.25, 4.75));
1501 assert_round_trips(&CssShape::polygon(
1502 vec![
1503 ShapePoint::new(0.0, 0.0),
1504 ShapePoint::new(100.0, 0.0),
1505 ShapePoint::new(50.0, 100.0),
1506 ]
1507 .into(),
1508 ));
1509 assert_round_trips(&CssShape::inset(1.0, 2.0, 3.0, 4.0));
1510 assert_round_trips(&CssShape::inset_rounded(1.0, 2.0, 3.0, 4.0, 5.0));
1511 assert_round_trips(&CssShape::Path(ShapePath {
1512 data: crate::corety::AzString::from("M 0 0 L 100 0 Z"),
1513 }));
1514 }
1515
1516 #[test]
1517 fn round_trip_survives_extreme_and_negative_numbers() {
1518 assert_round_trips(&CssShape::circle(
1519 ShapePoint::new(f32::MIN, f32::MAX),
1520 f32::MIN_POSITIVE,
1521 ));
1522 assert_round_trips(&CssShape::inset(-0.0, -1.5, 1e-30, 1e30));
1523 assert_round_trips(&CssShape::ellipse(
1524 ShapePoint::new(-0.000_001, 123_456.79),
1525 0.1,
1526 0.2,
1527 ));
1528 }
1529
1530 #[test]
1531 fn round_trip_of_non_finite_values_preserves_them() {
1532 let printed = CssShape::circle(ShapePoint::zero(), f32::INFINITY).print_as_css_value();
1535 assert_eq!(printed, "circle(infpx at 0px 0px)");
1536 assert!(circle_of(&parse_shape(&printed).unwrap())
1537 .radius
1538 .is_infinite());
1539
1540 let printed = CssShape::circle(ShapePoint::zero(), f32::NAN).print_as_css_value();
1542 assert!(circle_of(&parse_shape(&printed).unwrap()).radius.is_nan());
1543 }
1544
1545 #[test]
1546 fn round_trip_of_a_path_containing_quotes_and_parens() {
1547 for data in ["M 0 0)", ")", "a\"b", "", "M 0 0 L 1 1 Z"] {
1550 assert_round_trips(&CssShape::Path(ShapePath {
1551 data: crate::corety::AzString::from(data),
1552 }));
1553 }
1554 }
1555
1556 #[test]
1557 fn current_round_trip_is_asymmetric_for_degenerate_polygons() {
1558 for count in 0..3 {
1561 let points: Vec<ShapePoint> = (0..count)
1562 .map(|i| ShapePoint::new(i as f32, i as f32))
1563 .collect();
1564 let printed = CssShape::polygon(points.into()).print_as_css_value();
1565 assert!(
1566 parse_shape(&printed).is_err(),
1567 "{printed:?} should not re-parse (fewer than 3 points)"
1568 );
1569 }
1570 }
1571
1572 #[test]
1573 fn round_trip_from_the_css_source_side_is_a_fixed_point() {
1574 for source in [
1576 "circle(50px at 100px 100px)",
1577 "ellipse(50px 75px at 10px 20px)",
1578 "polygon(0px 0px, 100px 0px, 100px 100px, 0px 100px)",
1579 "inset(10px 20px 30px 40px)",
1580 "inset(10px round 5px)",
1581 "path(\"M 0 0 L 100 0 L 100 100 Z\")",
1582 ] {
1583 let first = parse_shape(source).unwrap();
1584 let second = parse_shape(&first.print_as_css_value()).unwrap();
1585 assert_eq!(first, second, "{source:?} is not a parse/print fixed point");
1586 }
1587 }
1588}