Skip to main content

azul_css/props/layout/
fragmentation.rs

1//! CSS properties for controlling fragmentation (page/column breaks).
2//!
3//! Defines [`PageBreak`], [`BreakInside`], [`Widows`], [`Orphans`], and
4//! [`BoxDecorationBreak`]. The `parser` sub-module (behind the `parser`
5//! feature) provides CSS-value parsing for each type.
6
7use alloc::string::{String, ToString};
8
9use crate::props::formatter::PrintAsCssValue;
10
11// --- break-before / break-after ---
12
13/// Represents a `break-before` or `break-after` CSS property value.
14#[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// --- break-inside ---
53
54/// Represents a `break-inside` CSS property value.
55#[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// --- widows / orphans ---
78
79/// CSS `widows` property - minimum number of lines in a block container
80/// that must be shown at the top of a page, region, or column.
81#[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/// CSS `orphans` property - minimum number of lines in a block container
100/// that must be shown at the bottom of a page, region, or column.
101#[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// --- box-decoration-break ---
120
121/// Represents a `box-decoration-break` CSS property value.
122#[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
140// Formatting to Rust code
141impl 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// --- PARSERS ---
193
194#[cfg(feature = "parser")]
195pub mod parser {
196    #[allow(clippy::wildcard_imports)]
197    // parser submodule reuses the parent module's value types
198    use super::*;
199    use crate::corety::AzString;
200    use crate::props::layout::position::ParseIntErrorWithInput;
201    use core::num::ParseIntError;
202
203    // -- PageBreak parser (`break-before`, `break-after`)
204
205    /// Error returned when parsing a `break-before` or `break-after` value.
206    #[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    /// Owned version of [`PageBreakParseError`] for FFI and storage.
217    #[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    /// # Errors
244    ///
245    /// Returns an error if `input` is not a valid CSS `page-break` value.
246    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    // -- BreakInside parser
265
266    /// Error returned when parsing a `break-inside` value.
267    #[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    /// Owned version of [`BreakInsideParseError`] for FFI and storage.
278    #[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    /// # Errors
305    ///
306    /// Returns an error if `input` is not a valid CSS `break-inside` value.
307    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    // -- Widows / Orphans parsers
318
319    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            /// # Errors
362            ///
363            /// Returns an error if `input` is not a valid CSS value for this property.
364            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    // -- BoxDecorationBreak parser
391
392    /// Error returned when parsing a `box-decoration-break` value.
393    #[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    /// Owned version of [`BoxDecorationBreakParseError`] for FFI and storage.
404    #[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    /// # Errors
431    ///
432    /// Returns an error if `input` is not a valid CSS `box-decoration-break` value.
433    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    /// Every `PageBreak` variant, so the round-trip tests stay exhaustive if a
499    /// variant is added.
500    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    /// Inputs that must never parse and must never panic, for any of the
526    /// keyword parsers.
527    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",  // i64::MAX
562            "-9223372036854775808", // i64::MIN
563            "18446744073709551615", // u64::MAX
564            "4294967295",           // u32::MAX
565            "AUTO",
566            "Auto",
567            "aUtO",
568            "AVOID-PAGE",
569            "\u{1F600}",
570            "auto\u{1F600}",
571            "\u{0301}",     // lone combining acute accent
572            "auto\u{0301}", // "auto" + combining mark
573            "\u{200B}auto", // zero-width space (NOT Unicode whitespace)
574            "auto\u{200B}",
575            "\u{FEFF}auto", // BOM (NOT Unicode whitespace)
576            "аuto",         // Cyrillic 'а' (U+0430) homoglyph
577            "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    // ---------------------------------------------------------------
590    // parse_page_break
591    // ---------------------------------------------------------------
592
593    #[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    /// Round-trip: `print_as_css_value` -> `parse_page_break` is the identity on
611    /// every variant, and the printed form is stable across a second pass.
612    #[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    /// Printed forms must be distinct, otherwise the round-trip above would be
627    /// lossy (two variants collapsing onto one keyword).
628    #[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            // The error must be constructible/printable for every input.
648            let err = result.unwrap_err();
649            let _ = format!("{err}");
650            let _ = err.to_contained();
651        }
652    }
653
654    /// The error borrows the *untrimmed* input, not the trimmed slice.
655    #[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    /// Surrounding ASCII whitespace is trimmed; interior junk is not.
663    #[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    /// Characterization: `str::trim` follows the Unicode `White_Space` property,
676    /// so NBSP (U+00A0) *is* stripped even though CSS tokenization would not
677    /// treat it as whitespace. ZWSP/BOM are not whitespace and stay.
678    #[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    /// Characterization: keyword matching is byte-exact, so CSS's ASCII
696    /// case-insensitivity for keywords is *not* implemented.
697    #[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); // 1M chars, trims to itself
708        assert_eq!(long.len(), 1_000_000);
709        assert!(parse_page_break(&long).is_err());
710
711        // 1M chars of pure padding around a valid keyword still trims to "auto".
712        let padded = format!("{}auto{}", " ".repeat(500_000), "\t".repeat(500_000));
713        assert_eq!(parse_page_break(&padded), Ok(PageBreak::Auto));
714
715        // A 1M-char run of a single byte must not blow up either.
716        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    // ---------------------------------------------------------------
727    // parse_break_inside
728    // ---------------------------------------------------------------
729
730    #[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    /// `break-inside` accepts a strict subset of the `page-break` keywords;
746    /// the ones it does not accept must be rejected rather than silently
747    /// falling through to `Auto`.
748    #[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    // ---------------------------------------------------------------
797    // parse_box_decoration_break
798    // ---------------------------------------------------------------
799
800    #[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        // "clone" is a keyword here but nowhere else; "copy"/"slice-clone" are not.
832        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    // ---------------------------------------------------------------
848    // Error <-> Owned conversions (to_contained / to_shared)
849    // ---------------------------------------------------------------
850
851    #[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        // shared -> owned -> shared is lossless
860        assert_eq!(owned.to_shared(), shared);
861        // and owned -> shared -> owned is idempotent
862        assert_eq!(owned.to_shared().to_contained(), owned);
863    }
864
865    /// Empty / whitespace / huge / non-ASCII payloads must survive the FFI
866    /// round-trip byte-for-byte and must not panic.
867    #[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    /// The `Display` impl embeds the raw input; a 1M-char input must format
919    /// without panicking (and must actually contain the payload).
920    #[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    // ---------------------------------------------------------------
930    // Widows / Orphans: numeric limits, overflow, saturation
931    // ---------------------------------------------------------------
932
933    #[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    /// Round-trip through the printed form for boundary values that are
942    /// representable (i.e. `<= i32::MAX`, since the parser goes through `i32`).
943    #[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    /// Characterization: parsing goes through `i32`, so values above
954    /// `i32::MAX` are *rejected*, not saturated — even though the field is
955    /// `u32` and can hold them. `Widows { inner: u32::MAX }` therefore does not
956    /// survive a print/parse round-trip.
957    #[test]
958    fn widows_above_i32_max_is_rejected_not_saturated() {
959        assert!(parse_widows("2147483648").is_err()); // i32::MAX + 1
960        assert!(parse_widows("4294967295").is_err()); // u32::MAX
961        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        // "-0" parses as 0 and is *not* treated as negative.
974        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"), // i32::MIN, parses then fails the sign check
984            Err(WidowsParseError::NegativeValue("-2147483648"))
985        ));
986        assert!(matches!(
987            parse_orphans("-1"),
988            Err(OrphansParseError::NegativeValue("-1"))
989        ));
990    }
991
992    /// The `NegativeValue` / `ParseInt` errors carry the *trimmed* input (unlike
993    /// the keyword parsers, which carry the raw input).
994    #[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            "١٢", // Arabic-Indic digits — not accepted by i32::from_str
1026            "\u{1F600}",
1027            "9223372036854775807",  // i64::MAX
1028            "-9223372036854775808", // i64::MIN
1029            "18446744073709551615", // u64::MAX
1030            "+",
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    /// A leading `+` is accepted by `i32::from_str`, so it is accepted here too.
1044    #[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}"); // Display embeds the 1M-char input
1055        assert!(parse_orphans(&digits).is_err());
1056    }
1057
1058    /// `to_contained` folds both `ParseInt` and `ParseIntOwned` onto the single
1059    /// owned `ParseInt` variant, so `owned -> shared -> owned` must be a
1060    /// fixed point (it is not the identity on the *shared* side).
1061    #[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        // shared -> owned -> shared is *lossy*: ParseInt becomes ParseIntOwned.
1069        match &back {
1070            WidowsParseError::ParseIntOwned(_, input) => assert_eq!(*input, "abc"),
1071            other => panic!("expected ParseIntOwned, got {other:?}"),
1072        }
1073        // ...but the owned representation is stable under a further round-trip.
1074        assert_eq!(back.to_contained(), owned);
1075
1076        // Both spellings render the same message.
1077        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    // ---------------------------------------------------------------
1097    // Value-type invariants
1098    // ---------------------------------------------------------------
1099
1100    #[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    /// `Ord` is derived, so it follows declaration order. Anything relying on
1121    /// `PageBreak::Auto` sorting first (e.g. a `BTreeMap` keyed by these) would
1122    /// break if the variants were reordered.
1123    #[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        // Ord on the newtypes follows the inner u32.
1149        assert!(Widows { inner: 0 } < Widows { inner: u32::MAX });
1150        assert!(Orphans { inner: 1 } < Orphans::default());
1151    }
1152}