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
33
34impl PrintAsCssValue for PageBreak {
35    fn print_as_css_value(&self) -> String {
36        String::from(match self {
37            Self::Auto => "auto",
38            Self::Avoid => "avoid",
39            Self::Always => "always",
40            Self::All => "all",
41            Self::Page => "page",
42            Self::AvoidPage => "avoid-page",
43            Self::Left => "left",
44            Self::Right => "right",
45            Self::Recto => "recto",
46            Self::Verso => "verso",
47            Self::Column => "column",
48            Self::AvoidColumn => "avoid-column",
49        })
50    }
51}
52
53// --- 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)]
59pub enum BreakInside {
60    #[default]
61    Auto,
62    Avoid,
63    AvoidPage,
64    AvoidColumn,
65}
66
67
68impl PrintAsCssValue for BreakInside {
69    fn print_as_css_value(&self) -> String {
70        String::from(match self {
71            Self::Auto => "auto",
72            Self::Avoid => "avoid",
73            Self::AvoidPage => "avoid-page",
74            Self::AvoidColumn => "avoid-column",
75        })
76    }
77}
78
79// --- 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)]
85pub struct Widows {
86    pub inner: u32,
87}
88
89impl Default for Widows {
90    fn default() -> Self {
91        Self { inner: 2 }
92    }
93}
94
95impl PrintAsCssValue for Widows {
96    fn print_as_css_value(&self) -> String {
97        self.inner.to_string()
98    }
99}
100
101/// 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)]
105pub struct Orphans {
106    pub inner: u32,
107}
108
109impl Default for Orphans {
110    fn default() -> Self {
111        Self { inner: 2 }
112    }
113}
114
115impl PrintAsCssValue for Orphans {
116    fn print_as_css_value(&self) -> String {
117        self.inner.to_string()
118    }
119}
120
121// --- 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)]
127pub enum BoxDecorationBreak {
128    #[default]
129    Slice,
130    Clone,
131}
132
133
134impl PrintAsCssValue for BoxDecorationBreak {
135    fn print_as_css_value(&self) -> String {
136        String::from(match self {
137            Self::Slice => "slice",
138            Self::Clone => "clone",
139        })
140    }
141}
142
143// Formatting to Rust code
144impl crate::codegen::format::FormatAsRustCode for PageBreak {
145    fn format_as_rust_code(&self, _tabs: usize) -> String {
146        match self {
147            Self::Auto => String::from("PageBreak::Auto"),
148            Self::Avoid => String::from("PageBreak::Avoid"),
149            Self::Always => String::from("PageBreak::Always"),
150            Self::All => String::from("PageBreak::All"),
151            Self::Page => String::from("PageBreak::Page"),
152            Self::AvoidPage => String::from("PageBreak::AvoidPage"),
153            Self::Left => String::from("PageBreak::Left"),
154            Self::Right => String::from("PageBreak::Right"),
155            Self::Recto => String::from("PageBreak::Recto"),
156            Self::Verso => String::from("PageBreak::Verso"),
157            Self::Column => String::from("PageBreak::Column"),
158            Self::AvoidColumn => String::from("PageBreak::AvoidColumn"),
159        }
160    }
161}
162
163impl crate::codegen::format::FormatAsRustCode for BreakInside {
164    fn format_as_rust_code(&self, _tabs: usize) -> String {
165        match self {
166            Self::Auto => String::from("BreakInside::Auto"),
167            Self::Avoid => String::from("BreakInside::Avoid"),
168            Self::AvoidPage => String::from("BreakInside::AvoidPage"),
169            Self::AvoidColumn => String::from("BreakInside::AvoidColumn"),
170        }
171    }
172}
173
174impl crate::codegen::format::FormatAsRustCode for Widows {
175    fn format_as_rust_code(&self, _tabs: usize) -> String {
176        format!("Widows {{ inner: {} }}", self.inner)
177    }
178}
179
180impl crate::codegen::format::FormatAsRustCode for Orphans {
181    fn format_as_rust_code(&self, _tabs: usize) -> String {
182        format!("Orphans {{ inner: {} }}", self.inner)
183    }
184}
185
186impl crate::codegen::format::FormatAsRustCode for BoxDecorationBreak {
187    fn format_as_rust_code(&self, _tabs: usize) -> String {
188        match self {
189            Self::Slice => String::from("BoxDecorationBreak::Slice"),
190            Self::Clone => String::from("BoxDecorationBreak::Clone"),
191        }
192    }
193}
194
195// --- PARSERS ---
196
197#[cfg(feature = "parser")]
198pub 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        #[must_use] pub fn to_contained(&self) -> PageBreakParseErrorOwned {
227            match self {
228                Self::InvalidValue(s) => PageBreakParseErrorOwned::InvalidValue((*s).to_string().into()),
229            }
230        }
231    }
232
233    impl PageBreakParseErrorOwned {
234        #[must_use] pub fn to_shared(&self) -> PageBreakParseError<'_> {
235            match self {
236                Self::InvalidValue(s) => PageBreakParseError::InvalidValue(s.as_str()),
237            }
238        }
239    }
240
241    /// # Errors
242    ///
243    /// Returns an error if `input` is not a valid CSS `page-break` value.
244    pub fn parse_page_break(input: &str) -> Result<PageBreak, PageBreakParseError<'_>> {
245        match input.trim() {
246            "auto" => Ok(PageBreak::Auto),
247            "avoid" => Ok(PageBreak::Avoid),
248            "always" => Ok(PageBreak::Always),
249            "all" => Ok(PageBreak::All),
250            "page" => Ok(PageBreak::Page),
251            "avoid-page" => Ok(PageBreak::AvoidPage),
252            "left" => Ok(PageBreak::Left),
253            "right" => Ok(PageBreak::Right),
254            "recto" => Ok(PageBreak::Recto),
255            "verso" => Ok(PageBreak::Verso),
256            "column" => Ok(PageBreak::Column),
257            "avoid-column" => Ok(PageBreak::AvoidColumn),
258            _ => Err(PageBreakParseError::InvalidValue(input)),
259        }
260    }
261
262    // -- 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        #[must_use] pub fn to_contained(&self) -> BreakInsideParseErrorOwned {
284            match self {
285                Self::InvalidValue(s) => BreakInsideParseErrorOwned::InvalidValue((*s).to_string().into()),
286            }
287        }
288    }
289
290    impl BreakInsideParseErrorOwned {
291        #[must_use] pub fn to_shared(&self) -> BreakInsideParseError<'_> {
292            match self {
293                Self::InvalidValue(s) => BreakInsideParseError::InvalidValue(s.as_str()),
294            }
295        }
296    }
297
298    /// # Errors
299    ///
300    /// Returns an error if `input` is not a valid CSS `break-inside` value.
301    pub fn parse_break_inside(
302        input: &str,
303    ) -> Result<BreakInside, BreakInsideParseError<'_>> {
304        match input.trim() {
305            "auto" => Ok(BreakInside::Auto),
306            "avoid" => Ok(BreakInside::Avoid),
307            "avoid-page" => Ok(BreakInside::AvoidPage),
308            "avoid-column" => Ok(BreakInside::AvoidColumn),
309            _ => Err(BreakInsideParseError::InvalidValue(input)),
310        }
311    }
312
313    // -- 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                #[must_use] pub fn to_contained(&self) -> $error_owned_name {
340                    match self {
341                        Self::ParseInt(e, s) => $error_owned_name::ParseInt(ParseIntErrorWithInput { error: e.to_string().into(), input: s.to_string().into() }),
342                        Self::ParseIntOwned(e, s) => $error_owned_name::ParseInt(ParseIntErrorWithInput { error: e.to_string().into(), input: s.to_string().into() }),
343                        Self::NegativeValue(s) => $error_owned_name::NegativeValue(s.to_string().into()),
344                    }
345                }
346            }
347
348            impl $error_owned_name {
349                #[must_use] pub fn to_shared(&self) -> $error_name<'_> {
350                     match self {
351                        Self::ParseInt(e) => $error_name::ParseIntOwned(e.error.as_str(), e.input.as_str()),
352                        Self::NegativeValue(s) => $error_name::NegativeValue(s),
353                    }
354                }
355            }
356
357            /// # Errors
358            ///
359            /// Returns an error if `input` is not a valid CSS value for this property.
360            pub fn $fn_name(input: &str) -> Result<$struct_name, $error_name<'_>> {
361                let trimmed = input.trim();
362                let val: i32 = trimmed.parse().map_err(|e| $error_name::ParseInt(e, trimmed))?;
363                if val < 0 {
364                    return Err($error_name::NegativeValue(trimmed));
365                }
366                Ok($struct_name { inner: u32::try_from(val).unwrap_or(0) })
367            }
368        };
369    }
370
371    define_widow_orphan_parser!(
372        parse_widows,
373        Widows,
374        WidowsParseError,
375        WidowsParseErrorOwned,
376        "widows"
377    );
378    define_widow_orphan_parser!(
379        parse_orphans,
380        Orphans,
381        OrphansParseError,
382        OrphansParseErrorOwned,
383        "orphans"
384    );
385
386    // -- 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        #[must_use] pub fn to_contained(&self) -> BoxDecorationBreakParseErrorOwned {
408            match self {
409                Self::InvalidValue(s) => {
410                    BoxDecorationBreakParseErrorOwned::InvalidValue((*s).to_string().into())
411                }
412            }
413        }
414    }
415
416    impl BoxDecorationBreakParseErrorOwned {
417        #[must_use] pub fn to_shared(&self) -> BoxDecorationBreakParseError<'_> {
418            match self {
419                Self::InvalidValue(s) => BoxDecorationBreakParseError::InvalidValue(s.as_str()),
420            }
421        }
422    }
423
424    /// # Errors
425    ///
426    /// Returns an error if `input` is not a valid CSS `box-decoration-break` value.
427    pub fn parse_box_decoration_break(
428        input: &str,
429    ) -> Result<BoxDecorationBreak, BoxDecorationBreakParseError<'_>> {
430        match input.trim() {
431            "slice" => Ok(BoxDecorationBreak::Slice),
432            "clone" => Ok(BoxDecorationBreak::Clone),
433            _ => Err(BoxDecorationBreakParseError::InvalidValue(input)),
434        }
435    }
436}
437
438#[cfg(feature = "parser")]
439pub use parser::*;
440
441#[cfg(all(test, feature = "parser"))]
442mod tests {
443    use super::*;
444
445    #[test]
446    fn test_parse_page_break() {
447        assert_eq!(parse_page_break("auto").unwrap(), PageBreak::Auto);
448        assert_eq!(parse_page_break("page").unwrap(), PageBreak::Page);
449        assert_eq!(
450            parse_page_break("avoid-column").unwrap(),
451            PageBreak::AvoidColumn
452        );
453        assert!(parse_page_break("invalid").is_err());
454    }
455
456    #[test]
457    fn test_parse_break_inside() {
458        assert_eq!(parse_break_inside("auto").unwrap(), BreakInside::Auto);
459        assert_eq!(parse_break_inside("avoid").unwrap(), BreakInside::Avoid);
460        assert!(parse_break_inside("always").is_err());
461    }
462
463    #[test]
464    fn test_parse_widows_orphans() {
465        assert_eq!(parse_widows("3").unwrap().inner, 3);
466        assert_eq!(parse_orphans("  1  ").unwrap().inner, 1);
467        assert!(parse_widows("-2").is_err());
468        assert!(parse_orphans("auto").is_err());
469    }
470
471    #[test]
472    fn test_parse_box_decoration_break() {
473        assert_eq!(
474            parse_box_decoration_break("slice").unwrap(),
475            BoxDecorationBreak::Slice
476        );
477        assert_eq!(
478            parse_box_decoration_break("clone").unwrap(),
479            BoxDecorationBreak::Clone
480        );
481        assert!(parse_box_decoration_break("copy").is_err());
482    }
483}