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

            
7
use alloc::string::{String, ToString};
8

            
9
use 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)]
17
pub 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

            
34
impl PrintAsCssValue for PageBreak {
35
61
    fn print_as_css_value(&self) -> String {
36
61
        String::from(match self {
37
6
            Self::Auto => "auto",
38
5
            Self::Avoid => "avoid",
39
5
            Self::Always => "always",
40
5
            Self::All => "all",
41
5
            Self::Page => "page",
42
5
            Self::AvoidPage => "avoid-page",
43
5
            Self::Left => "left",
44
5
            Self::Right => "right",
45
5
            Self::Recto => "recto",
46
5
            Self::Verso => "verso",
47
5
            Self::Column => "column",
48
5
            Self::AvoidColumn => "avoid-column",
49
        })
50
61
    }
51
}
52

            
53
// --- break-inside ---
54

            
55
/// Represents a `break-inside` CSS property value.
56
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
57
#[repr(C)]
58
#[derive(Default)]
59
pub enum BreakInside {
60
    #[default]
61
    Auto,
62
    Avoid,
63
    AvoidPage,
64
    AvoidColumn,
65
}
66

            
67

            
68
impl PrintAsCssValue for BreakInside {
69
51
    fn print_as_css_value(&self) -> String {
70
51
        String::from(match self {
71
15
            Self::Auto => "auto",
72
13
            Self::Avoid => "avoid",
73
12
            Self::AvoidPage => "avoid-page",
74
11
            Self::AvoidColumn => "avoid-column",
75
        })
76
51
    }
77
}
78

            
79
// --- widows / orphans ---
80

            
81
/// CSS `widows` property - minimum number of lines in a block container
82
/// that must be shown at the top of a page, region, or column.
83
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
84
#[repr(C)]
85
pub struct Widows {
86
    pub inner: u32,
87
}
88

            
89
impl Default for Widows {
90
2
    fn default() -> Self {
91
2
        Self { inner: 2 }
92
2
    }
93
}
94

            
95
impl PrintAsCssValue for Widows {
96
11
    fn print_as_css_value(&self) -> String {
97
11
        self.inner.to_string()
98
11
    }
99
}
100

            
101
/// CSS `orphans` property - minimum number of lines in a block container
102
/// that must be shown at the bottom of a page, region, or column.
103
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
104
#[repr(C)]
105
pub struct Orphans {
106
    pub inner: u32,
107
}
108

            
109
impl Default for Orphans {
110
3
    fn default() -> Self {
111
3
        Self { inner: 2 }
112
3
    }
113
}
114

            
115
impl PrintAsCssValue for Orphans {
116
9
    fn print_as_css_value(&self) -> String {
117
9
        self.inner.to_string()
118
9
    }
119
}
120

            
121
// --- box-decoration-break ---
122

            
123
/// Represents a `box-decoration-break` CSS property value.
124
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
125
#[repr(C)]
126
#[derive(Default)]
127
pub enum BoxDecorationBreak {
128
    #[default]
129
    Slice,
130
    Clone,
131
}
132

            
133

            
134
impl PrintAsCssValue for BoxDecorationBreak {
135
5
    fn print_as_css_value(&self) -> String {
136
5
        String::from(match self {
137
3
            Self::Slice => "slice",
138
2
            Self::Clone => "clone",
139
        })
140
5
    }
141
}
142

            
143
// Formatting to Rust code
144
impl 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

            
163
impl 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

            
174
impl 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

            
180
impl 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

            
186
impl 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
// --- PARSERS ---
196

            
197
#[cfg(feature = "parser")]
198
pub mod parser {
199
    #[allow(clippy::wildcard_imports)] // parser submodule reuses the parent module's value types
200
    use super::*;
201
    use core::num::ParseIntError;
202
    use crate::corety::AzString;
203
    use crate::props::layout::position::ParseIntErrorWithInput;
204

            
205
    // -- PageBreak parser (`break-before`, `break-after`)
206

            
207
    /// Error returned when parsing a `break-before` or `break-after` value.
208
    #[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
    /// Owned version of [`PageBreakParseError`] for FFI and storage.
219
    #[derive(Debug, Clone, PartialEq, Eq)]
220
    #[repr(C, u8)]
221
    pub enum PageBreakParseErrorOwned {
222
        InvalidValue(AzString),
223
    }
224

            
225
    impl PageBreakParseError<'_> {
226
67
        #[must_use] pub fn to_contained(&self) -> PageBreakParseErrorOwned {
227
67
            match self {
228
67
                Self::InvalidValue(s) => PageBreakParseErrorOwned::InvalidValue((*s).to_string().into()),
229
            }
230
67
        }
231
    }
232

            
233
    impl PageBreakParseErrorOwned {
234
14
        #[must_use] pub fn to_shared(&self) -> PageBreakParseError<'_> {
235
14
            match self {
236
14
                Self::InvalidValue(s) => PageBreakParseError::InvalidValue(s.as_str()),
237
            }
238
14
        }
239
    }
240

            
241
    /// # Errors
242
    ///
243
    /// Returns an error if `input` is not a valid CSS `page-break` value.
244
113
    pub fn parse_page_break(input: &str) -> Result<PageBreak, PageBreakParseError<'_>> {
245
113
        match input.trim() {
246
113
            "auto" => Ok(PageBreak::Auto),
247
104
            "avoid" => Ok(PageBreak::Avoid),
248
102
            "always" => Ok(PageBreak::Always),
249
100
            "all" => Ok(PageBreak::All),
250
98
            "page" => Ok(PageBreak::Page),
251
83
            "avoid-page" => Ok(PageBreak::AvoidPage),
252
80
            "left" => Ok(PageBreak::Left),
253
78
            "right" => Ok(PageBreak::Right),
254
76
            "recto" => Ok(PageBreak::Recto),
255
74
            "verso" => Ok(PageBreak::Verso),
256
72
            "column" => Ok(PageBreak::Column),
257
70
            "avoid-column" => Ok(PageBreak::AvoidColumn),
258
67
            _ => Err(PageBreakParseError::InvalidValue(input)),
259
        }
260
113
    }
261

            
262
    // -- BreakInside parser
263

            
264
    /// Error returned when parsing a `break-inside` value.
265
    #[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
    /// Owned version of [`BreakInsideParseError`] for FFI and storage.
276
    #[derive(Debug, Clone, PartialEq, Eq)]
277
    #[repr(C, u8)]
278
    pub enum BreakInsideParseErrorOwned {
279
        InvalidValue(AzString),
280
    }
281

            
282
    impl BreakInsideParseError<'_> {
283
63
        #[must_use] pub fn to_contained(&self) -> BreakInsideParseErrorOwned {
284
63
            match self {
285
63
                Self::InvalidValue(s) => BreakInsideParseErrorOwned::InvalidValue((*s).to_string().into()),
286
            }
287
63
        }
288
    }
289

            
290
    impl BreakInsideParseErrorOwned {
291
10
        #[must_use] pub fn to_shared(&self) -> BreakInsideParseError<'_> {
292
10
            match self {
293
10
                Self::InvalidValue(s) => BreakInsideParseError::InvalidValue(s.as_str()),
294
            }
295
10
        }
296
    }
297

            
298
    /// # Errors
299
    ///
300
    /// Returns an error if `input` is not a valid CSS `break-inside` value.
301
188
    pub fn parse_break_inside(
302
188
        input: &str,
303
188
    ) -> Result<BreakInside, BreakInsideParseError<'_>> {
304
188
        match input.trim() {
305
188
            "auto" => Ok(BreakInside::Auto),
306
183
            "avoid" => Ok(BreakInside::Avoid),
307
72
            "avoid-page" => Ok(BreakInside::AvoidPage),
308
70
            "avoid-column" => Ok(BreakInside::AvoidColumn),
309
68
            _ => Err(BreakInsideParseError::InvalidValue(input)),
310
        }
311
188
    }
312

            
313
    // -- Widows / Orphans parsers
314

            
315
    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
8
                #[must_use] pub fn to_contained(&self) -> $error_owned_name {
340
8
                    match self {
341
2
                        Self::ParseInt(e, s) => $error_owned_name::ParseInt(ParseIntErrorWithInput { error: e.to_string().into(), input: s.to_string().into() }),
342
2
                        Self::ParseIntOwned(e, s) => $error_owned_name::ParseInt(ParseIntErrorWithInput { error: e.to_string().into(), input: s.to_string().into() }),
343
4
                        Self::NegativeValue(s) => $error_owned_name::NegativeValue(s.to_string().into()),
344
                    }
345
8
                }
346
            }
347

            
348
            impl $error_owned_name {
349
6
                #[must_use] pub fn to_shared(&self) -> $error_name<'_> {
350
6
                     match self {
351
3
                        Self::ParseInt(e) => $error_name::ParseIntOwned(e.error.as_str(), e.input.as_str()),
352
3
                        Self::NegativeValue(s) => $error_name::NegativeValue(s),
353
                    }
354
6
                }
355
            }
356

            
357
            /// # Errors
358
            ///
359
            /// Returns an error if `input` is not a valid CSS value for this property.
360
346
            pub fn $fn_name(input: &str) -> Result<$struct_name, $error_name<'_>> {
361
346
                let trimmed = input.trim();
362
346
                let val: i32 = trimmed.parse().map_err(|e| $error_name::ParseInt(e, trimmed))?;
363
291
                if val < 0 {
364
6
                    return Err($error_name::NegativeValue(trimmed));
365
285
                }
366
285
                Ok($struct_name { inner: u32::try_from(val).unwrap_or(0) })
367
346
            }
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
    // -- BoxDecorationBreak parser
387

            
388
    /// Error returned when parsing a `box-decoration-break` value.
389
    #[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
    /// Owned version of [`BoxDecorationBreakParseError`] for FFI and storage.
400
    #[derive(Debug, Clone, PartialEq, Eq)]
401
    #[repr(C, u8)]
402
    pub enum BoxDecorationBreakParseErrorOwned {
403
        InvalidValue(AzString),
404
    }
405

            
406
    impl BoxDecorationBreakParseError<'_> {
407
61
        #[must_use] pub fn to_contained(&self) -> BoxDecorationBreakParseErrorOwned {
408
61
            match self {
409
61
                Self::InvalidValue(s) => {
410
61
                    BoxDecorationBreakParseErrorOwned::InvalidValue((*s).to_string().into())
411
                }
412
            }
413
61
        }
414
    }
415

            
416
    impl BoxDecorationBreakParseErrorOwned {
417
8
        #[must_use] pub fn to_shared(&self) -> BoxDecorationBreakParseError<'_> {
418
8
            match self {
419
8
                Self::InvalidValue(s) => BoxDecorationBreakParseError::InvalidValue(s.as_str()),
420
            }
421
8
        }
422
    }
423

            
424
    /// # Errors
425
    ///
426
    /// Returns an error if `input` is not a valid CSS `box-decoration-break` value.
427
65
    pub fn parse_box_decoration_break(
428
65
        input: &str,
429
65
    ) -> Result<BoxDecorationBreak, BoxDecorationBreakParseError<'_>> {
430
65
        match input.trim() {
431
65
            "slice" => Ok(BoxDecorationBreak::Slice),
432
61
            "clone" => Ok(BoxDecorationBreak::Clone),
433
58
            _ => Err(BoxDecorationBreakParseError::InvalidValue(input)),
434
        }
435
65
    }
436
}
437

            
438
#[cfg(feature = "parser")]
439
pub use parser::*;
440

            
441
#[cfg(all(test, feature = "parser"))]
442
mod tests {
443
    use super::*;
444

            
445
    #[test]
446
1
    fn test_parse_page_break() {
447
1
        assert_eq!(parse_page_break("auto").unwrap(), PageBreak::Auto);
448
1
        assert_eq!(parse_page_break("page").unwrap(), PageBreak::Page);
449
1
        assert_eq!(
450
1
            parse_page_break("avoid-column").unwrap(),
451
            PageBreak::AvoidColumn
452
        );
453
1
        assert!(parse_page_break("invalid").is_err());
454
1
    }
455

            
456
    #[test]
457
1
    fn test_parse_break_inside() {
458
1
        assert_eq!(parse_break_inside("auto").unwrap(), BreakInside::Auto);
459
1
        assert_eq!(parse_break_inside("avoid").unwrap(), BreakInside::Avoid);
460
1
        assert!(parse_break_inside("always").is_err());
461
1
    }
462

            
463
    #[test]
464
1
    fn test_parse_widows_orphans() {
465
1
        assert_eq!(parse_widows("3").unwrap().inner, 3);
466
1
        assert_eq!(parse_orphans("  1  ").unwrap().inner, 1);
467
1
        assert!(parse_widows("-2").is_err());
468
1
        assert!(parse_orphans("auto").is_err());
469
1
    }
470

            
471
    #[test]
472
1
    fn test_parse_box_decoration_break() {
473
1
        assert_eq!(
474
1
            parse_box_decoration_break("slice").unwrap(),
475
            BoxDecorationBreak::Slice
476
        );
477
1
        assert_eq!(
478
1
            parse_box_decoration_break("clone").unwrap(),
479
            BoxDecorationBreak::Clone
480
        );
481
1
        assert!(parse_box_decoration_break("copy").is_err());
482
1
    }
483
}
484

            
485
#[cfg(all(test, feature = "parser"))]
486
mod autotest_generated {
487
    use alloc::{format, string::String, vec::Vec};
488

            
489
    use super::*;
490
    use crate::props::formatter::PrintAsCssValue;
491

            
492
    /// Every `PageBreak` variant, so the round-trip tests stay exhaustive if a
493
    /// variant is added.
494
    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
    /// Inputs that must never parse and must never panic, for any of the
520
    /// keyword parsers.
521
    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",  // i64::MAX
556
            "-9223372036854775808", // i64::MIN
557
            "18446744073709551615", // u64::MAX
558
            "4294967295",           // u32::MAX
559
            "AUTO",
560
            "Auto",
561
            "aUtO",
562
            "AVOID-PAGE",
563
            "\u{1F600}",
564
            "auto\u{1F600}",
565
            "\u{0301}",       // lone combining acute accent
566
            "auto\u{0301}",   // "auto" + combining mark
567
            "\u{200B}auto",   // zero-width space (NOT Unicode whitespace)
568
            "auto\u{200B}",
569
            "\u{FEFF}auto",   // BOM (NOT Unicode whitespace)
570
            "аuto",           // Cyrillic 'а' (U+0430) homoglyph
571
            "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
    // ---------------------------------------------------------------
584
    // parse_page_break
585
    // ---------------------------------------------------------------
586

            
587
    #[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
    /// Round-trip: `print_as_css_value` -> `parse_page_break` is the identity on
605
    /// every variant, and the printed form is stable across a second pass.
606
    #[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
    /// Printed forms must be distinct, otherwise the round-trip above would be
617
    /// lossy (two variants collapsing onto one keyword).
618
    #[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
            // The error must be constructible/printable for every input.
638
            let err = result.unwrap_err();
639
            let _ = format!("{err}");
640
            let _ = err.to_contained();
641
        }
642
    }
643

            
644
    /// The error borrows the *untrimmed* input, not the trimmed slice.
645
    #[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
    /// Surrounding ASCII whitespace is trimmed; interior junk is not.
653
    #[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
    /// Characterization: `str::trim` follows the Unicode `White_Space` property,
663
    /// so NBSP (U+00A0) *is* stripped even though CSS tokenization would not
664
    /// treat it as whitespace. ZWSP/BOM are not whitespace and stay.
665
    #[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
    /// Characterization: keyword matching is byte-exact, so CSS's ASCII
677
    /// case-insensitivity for keywords is *not* implemented.
678
    #[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); // 1M chars, trims to itself
689
        assert_eq!(long.len(), 1_000_000);
690
        assert!(parse_page_break(&long).is_err());
691

            
692
        // 1M chars of pure padding around a valid keyword still trims to "auto".
693
        let padded = format!("{}auto{}", " ".repeat(500_000), "\t".repeat(500_000));
694
        assert_eq!(parse_page_break(&padded), Ok(PageBreak::Auto));
695

            
696
        // A 1M-char run of a single byte must not blow up either.
697
        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
    // ---------------------------------------------------------------
708
    // parse_break_inside
709
    // ---------------------------------------------------------------
710

            
711
    #[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
    /// `break-inside` accepts a strict subset of the `page-break` keywords;
727
    /// the ones it does not accept must be rejected rather than silently
728
    /// falling through to `Auto`.
729
    #[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
    // ---------------------------------------------------------------
778
    // parse_box_decoration_break
779
    // ---------------------------------------------------------------
780

            
781
    #[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
        // "clone" is a keyword here but nowhere else; "copy"/"slice-clone" are not.
813
        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
    // ---------------------------------------------------------------
829
    // Error <-> Owned conversions (to_contained / to_shared)
830
    // ---------------------------------------------------------------
831

            
832
    #[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
        // shared -> owned -> shared is lossless
841
        assert_eq!(owned.to_shared(), shared);
842
        // and owned -> shared -> owned is idempotent
843
        assert_eq!(owned.to_shared().to_contained(), owned);
844
    }
845

            
846
    /// Empty / whitespace / huge / non-ASCII payloads must survive the FFI
847
    /// round-trip byte-for-byte and must not panic.
848
    #[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
    /// The `Display` impl embeds the raw input; a 1M-char input must format
900
    /// without panicking (and must actually contain the payload).
901
    #[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
    // ---------------------------------------------------------------
911
    // Widows / Orphans: numeric limits, overflow, saturation
912
    // ---------------------------------------------------------------
913

            
914
    #[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
    /// Round-trip through the printed form for boundary values that are
923
    /// representable (i.e. `<= i32::MAX`, since the parser goes through `i32`).
924
    #[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
    /// Characterization: parsing goes through `i32`, so values above
935
    /// `i32::MAX` are *rejected*, not saturated — even though the field is
936
    /// `u32` and can hold them. `Widows { inner: u32::MAX }` therefore does not
937
    /// survive a print/parse round-trip.
938
    #[test]
939
    fn widows_above_i32_max_is_rejected_not_saturated() {
940
        assert!(parse_widows("2147483648").is_err()); // i32::MAX + 1
941
        assert!(parse_widows("4294967295").is_err()); // u32::MAX
942
        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
        // "-0" parses as 0 and is *not* treated as negative.
955
        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"), // i32::MIN, parses then fails the sign check
965
            Err(WidowsParseError::NegativeValue("-2147483648"))
966
        ));
967
        assert!(matches!(
968
            parse_orphans("-1"),
969
            Err(OrphansParseError::NegativeValue("-1"))
970
        ));
971
    }
972

            
973
    /// The `NegativeValue` / `ParseInt` errors carry the *trimmed* input (unlike
974
    /// the keyword parsers, which carry the raw input).
975
    #[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",
            "0.0",
            "2.5",
            "1_000",
            "0x10",
            "1 2",
            "2;",
            "١٢", // Arabic-Indic digits — not accepted by i32::from_str
            "\u{1F600}",
            "9223372036854775807",  // i64::MAX
            "-9223372036854775808", // i64::MIN
            "18446744073709551615", // u64::MAX
            "+",
            "-",
        ] {
            let w = parse_widows(input);
            assert!(w.is_err(), "expected Err for widows {input:?}, got {w:?}");
            let _ = format!("{}", w.unwrap_err());
            let o = parse_orphans(input);
            assert!(o.is_err(), "expected Err for orphans {input:?}, got {o:?}");
            let _ = format!("{}", o.unwrap_err());
        }
    }
    /// A leading `+` is accepted by `i32::from_str`, so it is accepted here too.
    #[test]
    fn widows_accepts_explicit_plus_sign() {
        assert_eq!(parse_widows("+3").unwrap().inner, 3);
        assert_eq!(parse_orphans("  +0  ").unwrap().inner, 0);
    }
    #[test]
    fn widows_extremely_long_digit_run_does_not_hang() {
        let digits = "9".repeat(1_000_000);
        let err = parse_widows(&digits).unwrap_err();
        let _ = format!("{err}"); // Display embeds the 1M-char input
        assert!(parse_orphans(&digits).is_err());
    }
    /// `to_contained` folds both `ParseInt` and `ParseIntOwned` onto the single
    /// owned `ParseInt` variant, so `owned -> shared -> owned` must be a
    /// fixed point (it is not the identity on the *shared* side).
    #[test]
    fn widows_error_owned_round_trip_is_a_fixed_point() {
        let parse_err = "abc".parse::<i32>().unwrap_err();
        let shared = WidowsParseError::ParseInt(parse_err, "abc");
        let owned = shared.to_contained();
        let back = owned.to_shared();
        // shared -> owned -> shared is *lossy*: ParseInt becomes ParseIntOwned.
        match &back {
            WidowsParseError::ParseIntOwned(_, input) => assert_eq!(*input, "abc"),
            other => panic!("expected ParseIntOwned, got {other:?}"),
        }
        // ...but the owned representation is stable under a further round-trip.
        assert_eq!(back.to_contained(), owned);
        // Both spellings render the same message.
        assert_eq!(format!("{shared}"), format!("{back}"));
        let neg = WidowsParseError::NegativeValue("-7");
        let neg_owned = neg.to_contained();
        assert_eq!(neg_owned.to_shared(), neg);
        assert_eq!(neg_owned.to_shared().to_contained(), neg_owned);
    }
    #[test]
    fn orphans_error_owned_round_trip_is_a_fixed_point() {
        let err = parse_orphans("nope").unwrap_err();
        let owned = err.to_contained();
        assert_eq!(owned.to_shared().to_contained(), owned);
        let _ = format!("{}", owned.to_shared());
        let neg = parse_orphans("-3").unwrap_err().to_contained();
        assert_eq!(neg.to_shared().to_contained(), neg);
    }
    // ---------------------------------------------------------------
    // Value-type invariants
    // ---------------------------------------------------------------
    #[test]
    fn enum_defaults_match_the_auto_slice_keywords() {
        assert_eq!(PageBreak::default(), PageBreak::Auto);
        assert_eq!(BreakInside::default(), BreakInside::Auto);
        assert_eq!(BoxDecorationBreak::default(), BoxDecorationBreak::Slice);
        assert_eq!(
            parse_page_break(&PageBreak::default().print_as_css_value()),
            Ok(PageBreak::default())
        );
        assert_eq!(
            parse_break_inside(&BreakInside::default().print_as_css_value()),
            Ok(BreakInside::default())
        );
        assert_eq!(
            parse_box_decoration_break(&BoxDecorationBreak::default().print_as_css_value()),
            Ok(BoxDecorationBreak::default())
        );
    }
    /// `Ord` is derived, so it follows declaration order. Anything relying on
    /// `PageBreak::Auto` sorting first (e.g. a `BTreeMap` keyed by these) would
    /// break if the variants were reordered.
    #[test]
    fn derived_ord_follows_declaration_order() {
        let mut sorted = ALL_PAGE_BREAKS;
        sorted.sort_unstable();
        assert_eq!(sorted, ALL_PAGE_BREAKS);
        assert!(PageBreak::Auto < PageBreak::AvoidColumn);
        let mut bi = ALL_BREAK_INSIDES;
        bi.sort_unstable();
        assert_eq!(bi, ALL_BREAK_INSIDES);
        assert!(BoxDecorationBreak::Slice < BoxDecorationBreak::Clone);
    }
    #[test]
    fn widows_orphans_print_saturating_extremes() {
        assert_eq!(Widows { inner: 0 }.print_as_css_value(), "0");
        assert_eq!(
            Widows { inner: u32::MAX }.print_as_css_value(),
            "4294967295"
        );
        assert_eq!(
            Orphans { inner: u32::MAX }.print_as_css_value(),
            "4294967295"
        );
        // Ord on the newtypes follows the inner u32.
        assert!(Widows { inner: 0 } < Widows { inner: u32::MAX });
        assert!(Orphans { inner: 1 } < Orphans::default());
    }
}