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