1use alloc::string::{String, ToString};
8
9use crate::props::formatter::PrintAsCssValue;
10
11#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
15#[repr(C)]
16#[derive(Default)]
17pub enum PageBreak {
18 #[default]
19 Auto,
20 Avoid,
21 Always,
22 All,
23 Page,
24 AvoidPage,
25 Left,
26 Right,
27 Recto,
28 Verso,
29 Column,
30 AvoidColumn,
31}
32
33
34impl PrintAsCssValue for PageBreak {
35 fn print_as_css_value(&self) -> String {
36 String::from(match self {
37 Self::Auto => "auto",
38 Self::Avoid => "avoid",
39 Self::Always => "always",
40 Self::All => "all",
41 Self::Page => "page",
42 Self::AvoidPage => "avoid-page",
43 Self::Left => "left",
44 Self::Right => "right",
45 Self::Recto => "recto",
46 Self::Verso => "verso",
47 Self::Column => "column",
48 Self::AvoidColumn => "avoid-column",
49 })
50 }
51}
52
53#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
57#[repr(C)]
58#[derive(Default)]
59pub enum BreakInside {
60 #[default]
61 Auto,
62 Avoid,
63 AvoidPage,
64 AvoidColumn,
65}
66
67
68impl PrintAsCssValue for BreakInside {
69 fn print_as_css_value(&self) -> String {
70 String::from(match self {
71 Self::Auto => "auto",
72 Self::Avoid => "avoid",
73 Self::AvoidPage => "avoid-page",
74 Self::AvoidColumn => "avoid-column",
75 })
76 }
77}
78
79#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
84#[repr(C)]
85pub struct Widows {
86 pub inner: u32,
87}
88
89impl Default for Widows {
90 fn default() -> Self {
91 Self { inner: 2 }
92 }
93}
94
95impl PrintAsCssValue for Widows {
96 fn print_as_css_value(&self) -> String {
97 self.inner.to_string()
98 }
99}
100
101#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
104#[repr(C)]
105pub struct Orphans {
106 pub inner: u32,
107}
108
109impl Default for Orphans {
110 fn default() -> Self {
111 Self { inner: 2 }
112 }
113}
114
115impl PrintAsCssValue for Orphans {
116 fn print_as_css_value(&self) -> String {
117 self.inner.to_string()
118 }
119}
120
121#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
125#[repr(C)]
126#[derive(Default)]
127pub enum BoxDecorationBreak {
128 #[default]
129 Slice,
130 Clone,
131}
132
133
134impl PrintAsCssValue for BoxDecorationBreak {
135 fn print_as_css_value(&self) -> String {
136 String::from(match self {
137 Self::Slice => "slice",
138 Self::Clone => "clone",
139 })
140 }
141}
142
143impl crate::codegen::format::FormatAsRustCode for PageBreak {
145 fn format_as_rust_code(&self, _tabs: usize) -> String {
146 match self {
147 Self::Auto => String::from("PageBreak::Auto"),
148 Self::Avoid => String::from("PageBreak::Avoid"),
149 Self::Always => String::from("PageBreak::Always"),
150 Self::All => String::from("PageBreak::All"),
151 Self::Page => String::from("PageBreak::Page"),
152 Self::AvoidPage => String::from("PageBreak::AvoidPage"),
153 Self::Left => String::from("PageBreak::Left"),
154 Self::Right => String::from("PageBreak::Right"),
155 Self::Recto => String::from("PageBreak::Recto"),
156 Self::Verso => String::from("PageBreak::Verso"),
157 Self::Column => String::from("PageBreak::Column"),
158 Self::AvoidColumn => String::from("PageBreak::AvoidColumn"),
159 }
160 }
161}
162
163impl crate::codegen::format::FormatAsRustCode for BreakInside {
164 fn format_as_rust_code(&self, _tabs: usize) -> String {
165 match self {
166 Self::Auto => String::from("BreakInside::Auto"),
167 Self::Avoid => String::from("BreakInside::Avoid"),
168 Self::AvoidPage => String::from("BreakInside::AvoidPage"),
169 Self::AvoidColumn => String::from("BreakInside::AvoidColumn"),
170 }
171 }
172}
173
174impl crate::codegen::format::FormatAsRustCode for Widows {
175 fn format_as_rust_code(&self, _tabs: usize) -> String {
176 format!("Widows {{ inner: {} }}", self.inner)
177 }
178}
179
180impl crate::codegen::format::FormatAsRustCode for Orphans {
181 fn format_as_rust_code(&self, _tabs: usize) -> String {
182 format!("Orphans {{ inner: {} }}", self.inner)
183 }
184}
185
186impl crate::codegen::format::FormatAsRustCode for BoxDecorationBreak {
187 fn format_as_rust_code(&self, _tabs: usize) -> String {
188 match self {
189 Self::Slice => String::from("BoxDecorationBreak::Slice"),
190 Self::Clone => String::from("BoxDecorationBreak::Clone"),
191 }
192 }
193}
194
195#[cfg(feature = "parser")]
198pub mod parser {
199 #[allow(clippy::wildcard_imports)] use super::*;
201 use core::num::ParseIntError;
202 use crate::corety::AzString;
203 use crate::props::layout::position::ParseIntErrorWithInput;
204
205 #[derive(Clone, PartialEq, Eq)]
209 pub enum PageBreakParseError<'a> {
210 InvalidValue(&'a str),
211 }
212
213 impl_debug_as_display!(PageBreakParseError<'a>);
214 impl_display! { PageBreakParseError<'a>, {
215 InvalidValue(v) => format!("Invalid break value: \"{}\"", v),
216 }}
217
218 #[derive(Debug, Clone, PartialEq, Eq)]
220 #[repr(C, u8)]
221 pub enum PageBreakParseErrorOwned {
222 InvalidValue(AzString),
223 }
224
225 impl PageBreakParseError<'_> {
226 #[must_use] pub fn to_contained(&self) -> PageBreakParseErrorOwned {
227 match self {
228 Self::InvalidValue(s) => PageBreakParseErrorOwned::InvalidValue((*s).to_string().into()),
229 }
230 }
231 }
232
233 impl PageBreakParseErrorOwned {
234 #[must_use] pub fn to_shared(&self) -> PageBreakParseError<'_> {
235 match self {
236 Self::InvalidValue(s) => PageBreakParseError::InvalidValue(s.as_str()),
237 }
238 }
239 }
240
241 pub fn parse_page_break(input: &str) -> Result<PageBreak, PageBreakParseError<'_>> {
245 match input.trim() {
246 "auto" => Ok(PageBreak::Auto),
247 "avoid" => Ok(PageBreak::Avoid),
248 "always" => Ok(PageBreak::Always),
249 "all" => Ok(PageBreak::All),
250 "page" => Ok(PageBreak::Page),
251 "avoid-page" => Ok(PageBreak::AvoidPage),
252 "left" => Ok(PageBreak::Left),
253 "right" => Ok(PageBreak::Right),
254 "recto" => Ok(PageBreak::Recto),
255 "verso" => Ok(PageBreak::Verso),
256 "column" => Ok(PageBreak::Column),
257 "avoid-column" => Ok(PageBreak::AvoidColumn),
258 _ => Err(PageBreakParseError::InvalidValue(input)),
259 }
260 }
261
262 #[derive(Clone, PartialEq, Eq)]
266 pub enum BreakInsideParseError<'a> {
267 InvalidValue(&'a str),
268 }
269
270 impl_debug_as_display!(BreakInsideParseError<'a>);
271 impl_display! { BreakInsideParseError<'a>, {
272 InvalidValue(v) => format!("Invalid break-inside value: \"{}\"", v),
273 }}
274
275 #[derive(Debug, Clone, PartialEq, Eq)]
277 #[repr(C, u8)]
278 pub enum BreakInsideParseErrorOwned {
279 InvalidValue(AzString),
280 }
281
282 impl BreakInsideParseError<'_> {
283 #[must_use] pub fn to_contained(&self) -> BreakInsideParseErrorOwned {
284 match self {
285 Self::InvalidValue(s) => BreakInsideParseErrorOwned::InvalidValue((*s).to_string().into()),
286 }
287 }
288 }
289
290 impl BreakInsideParseErrorOwned {
291 #[must_use] pub fn to_shared(&self) -> BreakInsideParseError<'_> {
292 match self {
293 Self::InvalidValue(s) => BreakInsideParseError::InvalidValue(s.as_str()),
294 }
295 }
296 }
297
298 pub fn parse_break_inside(
302 input: &str,
303 ) -> Result<BreakInside, BreakInsideParseError<'_>> {
304 match input.trim() {
305 "auto" => Ok(BreakInside::Auto),
306 "avoid" => Ok(BreakInside::Avoid),
307 "avoid-page" => Ok(BreakInside::AvoidPage),
308 "avoid-column" => Ok(BreakInside::AvoidColumn),
309 _ => Err(BreakInsideParseError::InvalidValue(input)),
310 }
311 }
312
313 macro_rules! define_widow_orphan_parser {
316 ($fn_name:ident, $struct_name:ident, $error_name:ident, $error_owned_name:ident, $prop_name:expr) => {
317 #[derive(Clone, PartialEq, Eq)]
318 pub enum $error_name<'a> {
319 ParseInt(ParseIntError, &'a str),
320 ParseIntOwned(&'a str, &'a str),
321 NegativeValue(&'a str),
322 }
323
324 impl_debug_as_display!($error_name<'a>);
325 impl_display! { $error_name<'a>, {
326 ParseInt(e, s) => format!("Invalid integer for {}: \"{}\". Reason: {}", $prop_name, s, e),
327 ParseIntOwned(e, s) => format!("Invalid integer for {}: \"{}\". Reason: {}", $prop_name, s, e),
328 NegativeValue(s) => format!("Invalid value for {}: \"{}\". Value cannot be negative.", $prop_name, s),
329 }}
330
331 #[derive(Debug, Clone, PartialEq, Eq)]
332 #[repr(C, u8)]
333 pub enum $error_owned_name {
334 ParseInt(ParseIntErrorWithInput),
335 NegativeValue(AzString),
336 }
337
338 impl $error_name<'_> {
339 #[must_use] pub fn to_contained(&self) -> $error_owned_name {
340 match self {
341 Self::ParseInt(e, s) => $error_owned_name::ParseInt(ParseIntErrorWithInput { error: e.to_string().into(), input: s.to_string().into() }),
342 Self::ParseIntOwned(e, s) => $error_owned_name::ParseInt(ParseIntErrorWithInput { error: e.to_string().into(), input: s.to_string().into() }),
343 Self::NegativeValue(s) => $error_owned_name::NegativeValue(s.to_string().into()),
344 }
345 }
346 }
347
348 impl $error_owned_name {
349 #[must_use] pub fn to_shared(&self) -> $error_name<'_> {
350 match self {
351 Self::ParseInt(e) => $error_name::ParseIntOwned(e.error.as_str(), e.input.as_str()),
352 Self::NegativeValue(s) => $error_name::NegativeValue(s),
353 }
354 }
355 }
356
357 pub fn $fn_name(input: &str) -> Result<$struct_name, $error_name<'_>> {
361 let trimmed = input.trim();
362 let val: i32 = trimmed.parse().map_err(|e| $error_name::ParseInt(e, trimmed))?;
363 if val < 0 {
364 return Err($error_name::NegativeValue(trimmed));
365 }
366 Ok($struct_name { inner: u32::try_from(val).unwrap_or(0) })
367 }
368 };
369 }
370
371 define_widow_orphan_parser!(
372 parse_widows,
373 Widows,
374 WidowsParseError,
375 WidowsParseErrorOwned,
376 "widows"
377 );
378 define_widow_orphan_parser!(
379 parse_orphans,
380 Orphans,
381 OrphansParseError,
382 OrphansParseErrorOwned,
383 "orphans"
384 );
385
386 #[derive(Clone, PartialEq, Eq)]
390 pub enum BoxDecorationBreakParseError<'a> {
391 InvalidValue(&'a str),
392 }
393
394 impl_debug_as_display!(BoxDecorationBreakParseError<'a>);
395 impl_display! { BoxDecorationBreakParseError<'a>, {
396 InvalidValue(v) => format!("Invalid box-decoration-break value: \"{}\"", v),
397 }}
398
399 #[derive(Debug, Clone, PartialEq, Eq)]
401 #[repr(C, u8)]
402 pub enum BoxDecorationBreakParseErrorOwned {
403 InvalidValue(AzString),
404 }
405
406 impl BoxDecorationBreakParseError<'_> {
407 #[must_use] pub fn to_contained(&self) -> BoxDecorationBreakParseErrorOwned {
408 match self {
409 Self::InvalidValue(s) => {
410 BoxDecorationBreakParseErrorOwned::InvalidValue((*s).to_string().into())
411 }
412 }
413 }
414 }
415
416 impl BoxDecorationBreakParseErrorOwned {
417 #[must_use] pub fn to_shared(&self) -> BoxDecorationBreakParseError<'_> {
418 match self {
419 Self::InvalidValue(s) => BoxDecorationBreakParseError::InvalidValue(s.as_str()),
420 }
421 }
422 }
423
424 pub fn parse_box_decoration_break(
428 input: &str,
429 ) -> Result<BoxDecorationBreak, BoxDecorationBreakParseError<'_>> {
430 match input.trim() {
431 "slice" => Ok(BoxDecorationBreak::Slice),
432 "clone" => Ok(BoxDecorationBreak::Clone),
433 _ => Err(BoxDecorationBreakParseError::InvalidValue(input)),
434 }
435 }
436}
437
438#[cfg(feature = "parser")]
439pub use parser::*;
440
441#[cfg(all(test, feature = "parser"))]
442mod tests {
443 use super::*;
444
445 #[test]
446 fn test_parse_page_break() {
447 assert_eq!(parse_page_break("auto").unwrap(), PageBreak::Auto);
448 assert_eq!(parse_page_break("page").unwrap(), PageBreak::Page);
449 assert_eq!(
450 parse_page_break("avoid-column").unwrap(),
451 PageBreak::AvoidColumn
452 );
453 assert!(parse_page_break("invalid").is_err());
454 }
455
456 #[test]
457 fn test_parse_break_inside() {
458 assert_eq!(parse_break_inside("auto").unwrap(), BreakInside::Auto);
459 assert_eq!(parse_break_inside("avoid").unwrap(), BreakInside::Avoid);
460 assert!(parse_break_inside("always").is_err());
461 }
462
463 #[test]
464 fn test_parse_widows_orphans() {
465 assert_eq!(parse_widows("3").unwrap().inner, 3);
466 assert_eq!(parse_orphans(" 1 ").unwrap().inner, 1);
467 assert!(parse_widows("-2").is_err());
468 assert!(parse_orphans("auto").is_err());
469 }
470
471 #[test]
472 fn test_parse_box_decoration_break() {
473 assert_eq!(
474 parse_box_decoration_break("slice").unwrap(),
475 BoxDecorationBreak::Slice
476 );
477 assert_eq!(
478 parse_box_decoration_break("clone").unwrap(),
479 BoxDecorationBreak::Clone
480 );
481 assert!(parse_box_decoration_break("copy").is_err());
482 }
483}
484
485#[cfg(all(test, feature = "parser"))]
486mod autotest_generated {
487 use alloc::{format, string::String, vec::Vec};
488
489 use super::*;
490 use crate::props::formatter::PrintAsCssValue;
491
492 const ALL_PAGE_BREAKS: [PageBreak; 12] = [
495 PageBreak::Auto,
496 PageBreak::Avoid,
497 PageBreak::Always,
498 PageBreak::All,
499 PageBreak::Page,
500 PageBreak::AvoidPage,
501 PageBreak::Left,
502 PageBreak::Right,
503 PageBreak::Recto,
504 PageBreak::Verso,
505 PageBreak::Column,
506 PageBreak::AvoidColumn,
507 ];
508
509 const ALL_BREAK_INSIDES: [BreakInside; 4] = [
510 BreakInside::Auto,
511 BreakInside::Avoid,
512 BreakInside::AvoidPage,
513 BreakInside::AvoidColumn,
514 ];
515
516 const ALL_BOX_DECORATION_BREAKS: [BoxDecorationBreak; 2] =
517 [BoxDecorationBreak::Slice, BoxDecorationBreak::Clone];
518
519 fn hostile_inputs() -> Vec<String> {
522 let mut v = Vec::new();
523 for s in [
524 "",
525 " ",
526 " ",
527 "\t\n\r\x0c",
528 "\0",
529 "auto\0",
530 "\0auto",
531 ";",
532 "auto;",
533 "auto;garbage",
534 "auto garbage",
535 "auto auto",
536 "auto/**/",
537 "/* auto */",
538 "\"auto\"",
539 "'auto'",
540 "-",
541 "--",
542 "0",
543 "-0",
544 "+0",
545 "1",
546 "-1",
547 "0.0",
548 "1e309",
549 "-1e309",
550 "NaN",
551 "nan",
552 "inf",
553 "-inf",
554 "Infinity",
555 "9223372036854775807", "-9223372036854775808", "18446744073709551615", "4294967295", "AUTO",
560 "Auto",
561 "aUtO",
562 "AVOID-PAGE",
563 "\u{1F600}",
564 "auto\u{1F600}",
565 "\u{0301}", "auto\u{0301}", "\u{200B}auto", "auto\u{200B}",
569 "\u{FEFF}auto", "аuto", "auto\u{0000}\u{FFFD}",
572 "initial",
573 "inherit",
574 "unset",
575 "revert",
576 "none",
577 ] {
578 v.push(String::from(s));
579 }
580 v
581 }
582
583 #[test]
588 fn page_break_valid_minimal() {
589 assert_eq!(parse_page_break("auto"), Ok(PageBreak::Auto));
590 }
591
592 #[test]
593 fn page_break_all_keywords_parse() {
594 for expected in ALL_PAGE_BREAKS {
595 let printed = expected.print_as_css_value();
596 assert_eq!(
597 parse_page_break(&printed),
598 Ok(expected),
599 "keyword {printed:?} must parse back"
600 );
601 }
602 }
603
604 #[test]
607 fn page_break_round_trip_is_identity() {
608 for expected in ALL_PAGE_BREAKS {
609 let once = expected.print_as_css_value();
610 let decoded = parse_page_break(&once).expect("printed value must re-parse");
611 assert_eq!(decoded, expected);
612 assert_eq!(decoded.print_as_css_value(), once, "encoding must be stable");
613 }
614 }
615
616 #[test]
619 fn page_break_printed_forms_are_distinct() {
620 let mut seen: Vec<String> = Vec::new();
621 for pb in ALL_PAGE_BREAKS {
622 let s = pb.print_as_css_value();
623 assert!(!seen.contains(&s), "duplicate printed form: {s:?}");
624 seen.push(s);
625 }
626 assert_eq!(seen.len(), ALL_PAGE_BREAKS.len());
627 }
628
629 #[test]
630 fn page_break_hostile_inputs_are_rejected_without_panic() {
631 for input in hostile_inputs() {
632 let result = parse_page_break(&input);
633 assert!(
634 result.is_err(),
635 "expected Err for {input:?}, got {result:?}"
636 );
637 let err = result.unwrap_err();
639 let _ = format!("{err}");
640 let _ = err.to_contained();
641 }
642 }
643
644 #[test]
646 fn page_break_error_carries_untrimmed_input() {
647 let err = parse_page_break(" bogus ").unwrap_err();
648 let PageBreakParseError::InvalidValue(v) = err;
649 assert_eq!(v, " bogus ");
650 }
651
652 #[test]
654 fn page_break_leading_trailing_junk() {
655 assert_eq!(parse_page_break(" auto "), Ok(PageBreak::Auto));
656 assert_eq!(parse_page_break("\n\tavoid-page\r\n"), Ok(PageBreak::AvoidPage));
657 assert!(parse_page_break("auto;").is_err());
658 assert!(parse_page_break("auto garbage").is_err());
659 assert!(parse_page_break("(auto)").is_err());
660 }
661
662 #[test]
666 fn page_break_unicode_whitespace_semantics() {
667 assert_eq!(
668 parse_page_break("\u{00A0}auto\u{00A0}"),
669 Ok(PageBreak::Auto),
670 "NBSP is Unicode whitespace, so trim() removes it (lenient vs. CSS)"
671 );
672 assert!(parse_page_break("\u{200B}auto").is_err(), "ZWSP is not whitespace");
673 assert!(parse_page_break("\u{FEFF}auto").is_err(), "BOM is not whitespace");
674 }
675
676 #[test]
679 fn page_break_is_case_sensitive() {
680 assert!(parse_page_break("AUTO").is_err());
681 assert!(parse_page_break("Auto").is_err());
682 assert!(parse_page_break("Avoid-Column").is_err());
683 assert_eq!(parse_page_break("auto"), Ok(PageBreak::Auto));
684 }
685
686 #[test]
687 fn page_break_extremely_long_input_does_not_hang() {
688 let long = "auto".repeat(250_000); assert_eq!(long.len(), 1_000_000);
690 assert!(parse_page_break(&long).is_err());
691
692 let padded = format!("{}auto{}", " ".repeat(500_000), "\t".repeat(500_000));
694 assert_eq!(parse_page_break(&padded), Ok(PageBreak::Auto));
695
696 let blob = "x".repeat(1_000_000);
698 assert!(parse_page_break(&blob).is_err());
699 }
700
701 #[test]
702 fn page_break_deeply_nested_input_does_not_stack_overflow() {
703 let nested = format!("{}auto{}", "(".repeat(10_000), ")".repeat(10_000));
704 assert!(parse_page_break(&nested).is_err());
705 }
706
707 #[test]
712 fn break_inside_valid_minimal() {
713 assert_eq!(parse_break_inside("auto"), Ok(BreakInside::Auto));
714 }
715
716 #[test]
717 fn break_inside_round_trip_is_identity() {
718 for expected in ALL_BREAK_INSIDES {
719 let printed = expected.print_as_css_value();
720 let decoded = parse_break_inside(&printed).expect("printed value must re-parse");
721 assert_eq!(decoded, expected);
722 assert_eq!(decoded.print_as_css_value(), printed);
723 }
724 }
725
726 #[test]
730 fn break_inside_rejects_page_break_only_keywords() {
731 for pb in ALL_PAGE_BREAKS {
732 let kw = pb.print_as_css_value();
733 let accepted = ALL_BREAK_INSIDES
734 .iter()
735 .any(|bi| bi.print_as_css_value() == kw);
736 assert_eq!(
737 parse_break_inside(&kw).is_ok(),
738 accepted,
739 "break-inside acceptance of {kw:?} must match its keyword set"
740 );
741 }
742 assert!(parse_break_inside("always").is_err());
743 assert!(parse_break_inside("left").is_err());
744 assert!(parse_break_inside("recto").is_err());
745 }
746
747 #[test]
748 fn break_inside_hostile_inputs_are_rejected_without_panic() {
749 for input in hostile_inputs() {
750 let result = parse_break_inside(&input);
751 assert!(
752 result.is_err(),
753 "expected Err for {input:?}, got {result:?}"
754 );
755 let err = result.unwrap_err();
756 let _ = format!("{err}");
757 let _ = err.to_contained();
758 }
759 }
760
761 #[test]
762 fn break_inside_error_carries_untrimmed_input() {
763 let err = parse_break_inside(" \u{1F600} ").unwrap_err();
764 let BreakInsideParseError::InvalidValue(v) = err;
765 assert_eq!(v, " \u{1F600} ");
766 }
767
768 #[test]
769 fn break_inside_extremely_long_input_does_not_hang() {
770 let long = "avoid-column".repeat(100_000);
771 assert!(parse_break_inside(&long).is_err());
772
773 let nested = format!("{}{}", "[".repeat(10_000), "]".repeat(10_000));
774 assert!(parse_break_inside(&nested).is_err());
775 }
776
777 #[test]
782 fn box_decoration_break_valid_minimal() {
783 assert_eq!(
784 parse_box_decoration_break("slice"),
785 Ok(BoxDecorationBreak::Slice)
786 );
787 }
788
789 #[test]
790 fn box_decoration_break_round_trip_is_identity() {
791 for expected in ALL_BOX_DECORATION_BREAKS {
792 let printed = expected.print_as_css_value();
793 let decoded =
794 parse_box_decoration_break(&printed).expect("printed value must re-parse");
795 assert_eq!(decoded, expected);
796 assert_eq!(decoded.print_as_css_value(), printed);
797 }
798 }
799
800 #[test]
801 fn box_decoration_break_hostile_inputs_are_rejected_without_panic() {
802 for input in hostile_inputs() {
803 let result = parse_box_decoration_break(&input);
804 assert!(
805 result.is_err(),
806 "expected Err for {input:?}, got {result:?}"
807 );
808 let err = result.unwrap_err();
809 let _ = format!("{err}");
810 let _ = err.to_contained();
811 }
812 assert!(parse_box_decoration_break("copy").is_err());
814 assert!(parse_box_decoration_break("slice-clone").is_err());
815 assert!(parse_box_decoration_break("Clone").is_err());
816 }
817
818 #[test]
819 fn box_decoration_break_whitespace_and_long_input() {
820 assert_eq!(
821 parse_box_decoration_break("\t\n clone \r\n"),
822 Ok(BoxDecorationBreak::Clone)
823 );
824 let long = "slice".repeat(200_000);
825 assert!(parse_box_decoration_break(&long).is_err());
826 }
827
828 #[test]
833 fn page_break_error_to_contained_basic_and_round_trip() {
834 let shared = PageBreakParseError::InvalidValue("bogus");
835 let owned = shared.to_contained();
836 assert_eq!(
837 owned,
838 PageBreakParseErrorOwned::InvalidValue(String::from("bogus").into())
839 );
840 assert_eq!(owned.to_shared(), shared);
842 assert_eq!(owned.to_shared().to_contained(), owned);
844 }
845
846 #[test]
849 fn page_break_error_to_contained_edge_payloads() {
850 let huge = "\u{1F600}".repeat(100_000);
851 for payload in [
852 String::new(),
853 String::from(" "),
854 String::from("\0"),
855 String::from("\u{1F600}\u{0301}"),
856 String::from("\u{FFFD}"),
857 huge,
858 ] {
859 let shared = PageBreakParseError::InvalidValue(payload.as_str());
860 let owned = shared.to_contained();
861 let back = owned.to_shared();
862 let PageBreakParseError::InvalidValue(v) = back;
863 assert_eq!(v, payload.as_str());
864 assert_eq!(owned.to_shared().to_contained(), owned);
865 let _ = format!("{shared}");
866 }
867 }
868
869 #[test]
870 fn break_inside_error_to_contained_round_trip() {
871 for payload in ["", " ", "always", "\u{1F600}", "\0\0\0"] {
872 let shared = BreakInsideParseError::InvalidValue(payload);
873 let owned = shared.to_contained();
874 assert_eq!(
875 owned,
876 BreakInsideParseErrorOwned::InvalidValue(String::from(payload).into())
877 );
878 assert_eq!(owned.to_shared(), shared);
879 assert_eq!(owned.to_shared().to_contained(), owned);
880 let _ = format!("{shared}");
881 }
882 }
883
884 #[test]
885 fn box_decoration_break_error_to_contained_round_trip() {
886 for payload in ["", "copy", " ", "\u{1F600}"] {
887 let shared = BoxDecorationBreakParseError::InvalidValue(payload);
888 let owned = shared.to_contained();
889 assert_eq!(
890 owned,
891 BoxDecorationBreakParseErrorOwned::InvalidValue(String::from(payload).into())
892 );
893 assert_eq!(owned.to_shared(), shared);
894 assert_eq!(owned.to_shared().to_contained(), owned);
895 let _ = format!("{shared}");
896 }
897 }
898
899 #[test]
902 fn error_display_handles_huge_payload() {
903 let payload = "x".repeat(1_000_000);
904 let err = parse_page_break(&payload).unwrap_err();
905 let msg = format!("{err}");
906 assert!(msg.contains(&payload));
907 assert!(msg.starts_with("Invalid break value: \""));
908 }
909
910 #[test]
915 fn widows_orphans_defaults_are_two() {
916 assert_eq!(Widows::default().inner, 2);
917 assert_eq!(Orphans::default().inner, 2);
918 assert_eq!(Widows::default().print_as_css_value(), "2");
919 assert_eq!(Orphans::default().print_as_css_value(), "2");
920 }
921
922 #[test]
925 fn widows_round_trip_representable_values() {
926 for inner in [0_u32, 1, 2, 3, 100, 65_535, 2_147_483_647] {
927 let printed = Widows { inner }.print_as_css_value();
928 assert_eq!(parse_widows(&printed).unwrap().inner, inner);
929 let printed = Orphans { inner }.print_as_css_value();
930 assert_eq!(parse_orphans(&printed).unwrap().inner, inner);
931 }
932 }
933
934 #[test]
939 fn widows_above_i32_max_is_rejected_not_saturated() {
940 assert!(parse_widows("2147483648").is_err()); assert!(parse_widows("4294967295").is_err()); assert!(parse_orphans("4294967295").is_err());
943
944 let printed = Widows { inner: u32::MAX }.print_as_css_value();
945 assert_eq!(printed, "4294967295");
946 assert!(
947 parse_widows(&printed).is_err(),
948 "u32::MAX widows cannot round-trip through the i32-based parser"
949 );
950 }
951
952 #[test]
953 fn widows_negative_and_zero_boundaries() {
954 assert_eq!(parse_widows("-0").unwrap().inner, 0);
956 assert_eq!(parse_orphans("-0").unwrap().inner, 0);
957 assert_eq!(parse_widows("0").unwrap().inner, 0);
958
959 assert!(matches!(
960 parse_widows("-1"),
961 Err(WidowsParseError::NegativeValue("-1"))
962 ));
963 assert!(matches!(
964 parse_widows("-2147483648"), Err(WidowsParseError::NegativeValue("-2147483648"))
966 ));
967 assert!(matches!(
968 parse_orphans("-1"),
969 Err(OrphansParseError::NegativeValue("-1"))
970 ));
971 }
972
973 #[test]
976 fn widows_error_carries_trimmed_input() {
977 assert!(matches!(
978 parse_widows(" -5 "),
979 Err(WidowsParseError::NegativeValue("-5"))
980 ));
981 match parse_widows(" abc ") {
982 Err(WidowsParseError::ParseInt(_, s)) => assert_eq!(s, "abc"),
983 other => panic!("expected ParseInt error, got {other:?}"),
984 }
985 }
986
987 #[test]
988 fn widows_orphans_reject_non_integers_without_panic() {
989 for input in [
990 "",
991 " ",
992 "\t\n",
993 "auto",
994 "NaN",
995 "nan",
996 "inf",
997 "-inf",
998 "Infinity",
999 "1e309",
1000 "0.0",
1001 "2.5",
1002 "1_000",
1003 "0x10",
1004 "1 2",
1005 "2;",
1006 "١٢", "\u{1F600}",
1008 "9223372036854775807", "-9223372036854775808", "18446744073709551615", "+",
1012 "-",
1013 ] {
1014 let w = parse_widows(input);
1015 assert!(w.is_err(), "expected Err for widows {input:?}, got {w:?}");
1016 let _ = format!("{}", w.unwrap_err());
1017
1018 let o = parse_orphans(input);
1019 assert!(o.is_err(), "expected Err for orphans {input:?}, got {o:?}");
1020 let _ = format!("{}", o.unwrap_err());
1021 }
1022 }
1023
1024 #[test]
1026 fn widows_accepts_explicit_plus_sign() {
1027 assert_eq!(parse_widows("+3").unwrap().inner, 3);
1028 assert_eq!(parse_orphans(" +0 ").unwrap().inner, 0);
1029 }
1030
1031 #[test]
1032 fn widows_extremely_long_digit_run_does_not_hang() {
1033 let digits = "9".repeat(1_000_000);
1034 let err = parse_widows(&digits).unwrap_err();
1035 let _ = format!("{err}"); assert!(parse_orphans(&digits).is_err());
1037 }
1038
1039 #[test]
1043 fn widows_error_owned_round_trip_is_a_fixed_point() {
1044 let parse_err = "abc".parse::<i32>().unwrap_err();
1045 let shared = WidowsParseError::ParseInt(parse_err, "abc");
1046 let owned = shared.to_contained();
1047
1048 let back = owned.to_shared();
1049 match &back {
1051 WidowsParseError::ParseIntOwned(_, input) => assert_eq!(*input, "abc"),
1052 other => panic!("expected ParseIntOwned, got {other:?}"),
1053 }
1054 assert_eq!(back.to_contained(), owned);
1056
1057 assert_eq!(format!("{shared}"), format!("{back}"));
1059
1060 let neg = WidowsParseError::NegativeValue("-7");
1061 let neg_owned = neg.to_contained();
1062 assert_eq!(neg_owned.to_shared(), neg);
1063 assert_eq!(neg_owned.to_shared().to_contained(), neg_owned);
1064 }
1065
1066 #[test]
1067 fn orphans_error_owned_round_trip_is_a_fixed_point() {
1068 let err = parse_orphans("nope").unwrap_err();
1069 let owned = err.to_contained();
1070 assert_eq!(owned.to_shared().to_contained(), owned);
1071 let _ = format!("{}", owned.to_shared());
1072
1073 let neg = parse_orphans("-3").unwrap_err().to_contained();
1074 assert_eq!(neg.to_shared().to_contained(), neg);
1075 }
1076
1077 #[test]
1082 fn enum_defaults_match_the_auto_slice_keywords() {
1083 assert_eq!(PageBreak::default(), PageBreak::Auto);
1084 assert_eq!(BreakInside::default(), BreakInside::Auto);
1085 assert_eq!(BoxDecorationBreak::default(), BoxDecorationBreak::Slice);
1086
1087 assert_eq!(
1088 parse_page_break(&PageBreak::default().print_as_css_value()),
1089 Ok(PageBreak::default())
1090 );
1091 assert_eq!(
1092 parse_break_inside(&BreakInside::default().print_as_css_value()),
1093 Ok(BreakInside::default())
1094 );
1095 assert_eq!(
1096 parse_box_decoration_break(&BoxDecorationBreak::default().print_as_css_value()),
1097 Ok(BoxDecorationBreak::default())
1098 );
1099 }
1100
1101 #[test]
1105 fn derived_ord_follows_declaration_order() {
1106 let mut sorted = ALL_PAGE_BREAKS;
1107 sorted.sort_unstable();
1108 assert_eq!(sorted, ALL_PAGE_BREAKS);
1109 assert!(PageBreak::Auto < PageBreak::AvoidColumn);
1110
1111 let mut bi = ALL_BREAK_INSIDES;
1112 bi.sort_unstable();
1113 assert_eq!(bi, ALL_BREAK_INSIDES);
1114
1115 assert!(BoxDecorationBreak::Slice < BoxDecorationBreak::Clone);
1116 }
1117
1118 #[test]
1119 fn widows_orphans_print_saturating_extremes() {
1120 assert_eq!(Widows { inner: 0 }.print_as_css_value(), "0");
1121 assert_eq!(
1122 Widows { inner: u32::MAX }.print_as_css_value(),
1123 "4294967295"
1124 );
1125 assert_eq!(
1126 Orphans { inner: u32::MAX }.print_as_css_value(),
1127 "4294967295"
1128 );
1129 assert!(Widows { inner: 0 } < Widows { inner: u32::MAX });
1131 assert!(Orphans { inner: 1 } < Orphans::default());
1132 }
1133}