Skip to main content

azul_css/props/layout/
column.rs

1//! CSS properties for multi-column layout.
2//!
3//! Covers `column-count`, `column-width`, `column-span`, `column-fill`,
4//! `column-rule-width`, `column-rule-style`, and `column-rule-color`.
5//! Types are consumed via the `CssProperty` enum in the CSS property system.
6
7use alloc::string::{String, ToString};
8use core::num::ParseIntError;
9
10use crate::props::{
11    basic::{
12        color::{parse_css_color, ColorU, CssColorParseError, CssColorParseErrorOwned},
13        pixel::{
14            parse_pixel_value, CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue,
15        },
16    },
17    formatter::PrintAsCssValue,
18    style::border::{
19        parse_border_style, BorderStyle, CssBorderStyleParseError, CssBorderStyleParseErrorOwned,
20    },
21};
22
23// --- column-count ---
24
25/// CSS `column-count` property: specifies the number of columns in a multi-column layout.
26///
27/// Values: `auto` or a positive integer.
28#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
29#[repr(C, u8)]
30#[derive(Default)]
31pub enum ColumnCount {
32    #[default]
33    Auto,
34    Integer(u32),
35}
36
37impl PrintAsCssValue for ColumnCount {
38    fn print_as_css_value(&self) -> String {
39        match self {
40            Self::Auto => "auto".to_string(),
41            Self::Integer(i) => i.to_string(),
42        }
43    }
44}
45
46// --- column-width ---
47#[allow(variant_size_differences)]
48// repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
49/// CSS `column-width` property: specifies the optimal width of columns.
50///
51/// Values: `auto` or a length value (e.g. `200px`, `15em`).
52#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
53#[repr(C, u8)]
54#[derive(Default)]
55pub enum ColumnWidth {
56    #[default]
57    Auto,
58    Length(PixelValue),
59}
60
61impl PrintAsCssValue for ColumnWidth {
62    fn print_as_css_value(&self) -> String {
63        match self {
64            Self::Auto => "auto".to_string(),
65            Self::Length(px) => px.print_as_css_value(),
66        }
67    }
68}
69
70// --- column-span ---
71
72/// CSS `column-span` property: whether an element spans across all columns.
73///
74/// Values: `none` (default) or `all`.
75#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
76#[repr(C)]
77#[derive(Default)]
78pub enum ColumnSpan {
79    #[default]
80    None,
81    All,
82}
83
84impl PrintAsCssValue for ColumnSpan {
85    fn print_as_css_value(&self) -> String {
86        String::from(match self {
87            Self::None => "none",
88            Self::All => "all",
89        })
90    }
91}
92
93// --- column-fill ---
94
95/// CSS `column-fill` property: how content is distributed across columns.
96///
97/// Values: `balance` (default) or `auto`.
98#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
99#[repr(C)]
100#[derive(Default)]
101pub enum ColumnFill {
102    Auto,
103    #[default]
104    Balance,
105}
106
107impl PrintAsCssValue for ColumnFill {
108    fn print_as_css_value(&self) -> String {
109        String::from(match self {
110            Self::Auto => "auto",
111            Self::Balance => "balance",
112        })
113    }
114}
115
116// --- column-rule ---
117
118/// CSS `column-rule-width` property: the width of the rule between columns.
119///
120/// Defaults to `medium` (3px).
121#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
122#[repr(C)]
123pub struct ColumnRuleWidth {
124    pub inner: PixelValue,
125}
126
127impl Default for ColumnRuleWidth {
128    fn default() -> Self {
129        Self {
130            inner: PixelValue::const_px(3),
131        }
132    }
133}
134
135impl PrintAsCssValue for ColumnRuleWidth {
136    fn print_as_css_value(&self) -> String {
137        self.inner.print_as_css_value()
138    }
139}
140
141/// CSS `column-rule-style` property: the style of the rule between columns.
142///
143/// Uses `BorderStyle` values (e.g. `none`, `solid`, `dotted`).
144#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
145#[repr(C)]
146pub struct ColumnRuleStyle {
147    pub inner: BorderStyle,
148}
149
150impl Default for ColumnRuleStyle {
151    fn default() -> Self {
152        Self {
153            inner: BorderStyle::None,
154        }
155    }
156}
157
158impl PrintAsCssValue for ColumnRuleStyle {
159    fn print_as_css_value(&self) -> String {
160        self.inner.print_as_css_value()
161    }
162}
163
164/// CSS `column-rule-color` property: the color of the rule between columns.
165///
166/// Per the CSS spec this should default to `currentcolor`, but currently
167/// defaults to black as `currentcolor` requires a resolved-value pass at
168/// layout time.
169#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
170#[repr(C)]
171pub struct ColumnRuleColor {
172    pub inner: ColorU,
173}
174
175impl Default for ColumnRuleColor {
176    fn default() -> Self {
177        // NOTE: should be `currentcolor` per CSS spec, see doc comment on type
178        Self {
179            inner: ColorU::BLACK,
180        }
181    }
182}
183
184impl PrintAsCssValue for ColumnRuleColor {
185    fn print_as_css_value(&self) -> String {
186        self.inner.to_hash()
187    }
188}
189
190// Formatting to Rust code
191impl crate::codegen::format::FormatAsRustCode for ColumnCount {
192    fn format_as_rust_code(&self, _tabs: usize) -> String {
193        match self {
194            Self::Auto => String::from("ColumnCount::Auto"),
195            Self::Integer(i) => format!("ColumnCount::Integer({i})"),
196        }
197    }
198}
199
200impl crate::codegen::format::FormatAsRustCode for ColumnWidth {
201    fn format_as_rust_code(&self, _tabs: usize) -> String {
202        match self {
203            Self::Auto => String::from("ColumnWidth::Auto"),
204            Self::Length(px) => format!(
205                "ColumnWidth::Length({})",
206                crate::codegen::format::format_pixel_value(px)
207            ),
208        }
209    }
210}
211
212impl crate::codegen::format::FormatAsRustCode for ColumnSpan {
213    fn format_as_rust_code(&self, _tabs: usize) -> String {
214        match self {
215            Self::None => String::from("ColumnSpan::None"),
216            Self::All => String::from("ColumnSpan::All"),
217        }
218    }
219}
220
221impl crate::codegen::format::FormatAsRustCode for ColumnFill {
222    fn format_as_rust_code(&self, _tabs: usize) -> String {
223        match self {
224            Self::Auto => String::from("ColumnFill::Auto"),
225            Self::Balance => String::from("ColumnFill::Balance"),
226        }
227    }
228}
229
230impl crate::codegen::format::FormatAsRustCode for ColumnRuleWidth {
231    fn format_as_rust_code(&self, _tabs: usize) -> String {
232        format!(
233            "ColumnRuleWidth {{ inner: {} }}",
234            crate::codegen::format::format_pixel_value(&self.inner)
235        )
236    }
237}
238
239impl crate::codegen::format::FormatAsRustCode for ColumnRuleStyle {
240    fn format_as_rust_code(&self, tabs: usize) -> String {
241        format!(
242            "ColumnRuleStyle {{ inner: {} }}",
243            self.inner.format_as_rust_code(tabs)
244        )
245    }
246}
247
248impl crate::codegen::format::FormatAsRustCode for ColumnRuleColor {
249    fn format_as_rust_code(&self, _tabs: usize) -> String {
250        format!(
251            "ColumnRuleColor {{ inner: {} }}",
252            crate::codegen::format::format_color_value(&self.inner)
253        )
254    }
255}
256
257// --- PARSERS ---
258
259#[cfg(feature = "parser")]
260pub mod parser {
261    #[allow(clippy::wildcard_imports)]
262    // parser submodule reuses the parent module's value types
263    use super::*;
264    use crate::corety::AzString;
265
266    // -- ColumnCount parser
267
268    #[derive(Clone, PartialEq, Eq)]
269    pub enum ColumnCountParseError<'a> {
270        InvalidValue(&'a str),
271        ParseInt(ParseIntError),
272    }
273
274    impl_debug_as_display!(ColumnCountParseError<'a>);
275    impl_display! { ColumnCountParseError<'a>, {
276        InvalidValue(v) => format!("Invalid column-count value: \"{}\"", v),
277        ParseInt(e) => format!("Invalid integer for column-count: {}", e),
278    }}
279
280    #[derive(Debug, Clone, PartialEq, Eq)]
281    #[repr(C, u8)]
282    pub enum ColumnCountParseErrorOwned {
283        InvalidValue(AzString),
284        ParseInt(AzString),
285    }
286
287    impl ColumnCountParseError<'_> {
288        #[must_use]
289        pub fn to_contained(&self) -> ColumnCountParseErrorOwned {
290            match self {
291                Self::InvalidValue(s) => {
292                    ColumnCountParseErrorOwned::InvalidValue((*s).to_string().into())
293                }
294                Self::ParseInt(e) => ColumnCountParseErrorOwned::ParseInt(e.to_string().into()),
295            }
296        }
297    }
298
299    impl ColumnCountParseErrorOwned {
300        #[must_use]
301        pub fn to_shared(&self) -> ColumnCountParseError<'_> {
302            match self {
303                Self::InvalidValue(s) => ColumnCountParseError::InvalidValue(s),
304                // ParseIntError cannot be reconstructed from its Display string,
305                // so we fall back to a generic message. The original error text
306                // is preserved in the owned `AzString` but not round-trippable.
307                Self::ParseInt(_) => ColumnCountParseError::InvalidValue("invalid integer"),
308            }
309        }
310    }
311
312    /// # Errors
313    ///
314    /// Returns an error if `input` is not a valid CSS `column-count` value.
315    pub fn parse_column_count(input: &str) -> Result<ColumnCount, ColumnCountParseError<'_>> {
316        let trimmed = input.trim();
317        if trimmed == "auto" {
318            return Ok(ColumnCount::Auto);
319        }
320        let val: u32 = trimmed.parse().map_err(ColumnCountParseError::ParseInt)?;
321        Ok(ColumnCount::Integer(val))
322    }
323
324    // -- ColumnWidth parser
325
326    #[derive(Clone, PartialEq, Eq)]
327    pub enum ColumnWidthParseError<'a> {
328        InvalidValue(&'a str),
329        PixelValue(CssPixelValueParseError<'a>),
330    }
331
332    impl_debug_as_display!(ColumnWidthParseError<'a>);
333    impl_display! { ColumnWidthParseError<'a>, {
334        InvalidValue(v) => format!("Invalid column-width value: \"{}\"", v),
335        PixelValue(e) => format!("{}", e),
336    }}
337    impl_from! { CssPixelValueParseError<'a>, ColumnWidthParseError::PixelValue }
338
339    #[derive(Debug, Clone, PartialEq, Eq)]
340    #[repr(C, u8)]
341    pub enum ColumnWidthParseErrorOwned {
342        InvalidValue(AzString),
343        PixelValue(CssPixelValueParseErrorOwned),
344    }
345
346    impl ColumnWidthParseError<'_> {
347        #[must_use]
348        pub fn to_contained(&self) -> ColumnWidthParseErrorOwned {
349            match self {
350                Self::InvalidValue(s) => {
351                    ColumnWidthParseErrorOwned::InvalidValue((*s).to_string().into())
352                }
353                Self::PixelValue(e) => ColumnWidthParseErrorOwned::PixelValue(e.to_contained()),
354            }
355        }
356    }
357
358    impl ColumnWidthParseErrorOwned {
359        #[must_use]
360        pub fn to_shared(&self) -> ColumnWidthParseError<'_> {
361            match self {
362                Self::InvalidValue(s) => ColumnWidthParseError::InvalidValue(s),
363                Self::PixelValue(e) => ColumnWidthParseError::PixelValue(e.to_shared()),
364            }
365        }
366    }
367
368    /// # Errors
369    ///
370    /// Returns an error if `input` is not a valid CSS `column-width` value.
371    pub fn parse_column_width(input: &str) -> Result<ColumnWidth, ColumnWidthParseError<'_>> {
372        let trimmed = input.trim();
373        if trimmed == "auto" {
374            return Ok(ColumnWidth::Auto);
375        }
376        Ok(ColumnWidth::Length(parse_pixel_value(trimmed)?))
377    }
378
379    // -- Other column parsers...
380    macro_rules! define_simple_column_parser {
381        (
382            $fn_name:ident,
383            $struct_name:ident,
384            $error_name:ident,
385            $error_owned_name:ident,
386            $prop_name:expr,
387            $($val:expr => $variant:path),+
388        ) => {
389            #[derive(Clone, PartialEq, Eq)]
390            pub enum $error_name<'a> {
391                InvalidValue(&'a str),
392            }
393
394            impl_debug_as_display!($error_name<'a>);
395            impl_display! { $error_name<'a>, {
396                InvalidValue(v) => format!("Invalid {} value: \"{}\"", $prop_name, v),
397            }}
398
399            #[derive(Debug, Clone, PartialEq, Eq)]
400            #[repr(C, u8)]
401            pub enum $error_owned_name {
402                InvalidValue(AzString),
403            }
404
405            impl $error_name<'_> {
406                #[must_use] pub fn to_contained(&self) -> $error_owned_name {
407                    match self {
408                        Self::InvalidValue(s) => $error_owned_name::InvalidValue(s.to_string().into()),
409                    }
410                }
411            }
412
413            impl $error_owned_name {
414                #[must_use] pub fn to_shared(&self) -> $error_name<'_> {
415                    match self {
416                        Self::InvalidValue(s) => $error_name::InvalidValue(s.as_str()),
417                    }
418                }
419            }
420
421            /// # Errors
422            ///
423            /// Returns an error if `input` is not a valid CSS value for this property.
424            pub fn $fn_name(input: &str) -> Result<$struct_name, $error_name<'_>> {
425                match input.trim() {
426                    $( $val => Ok($variant), )+
427                    _ => Err($error_name::InvalidValue(input)),
428                }
429            }
430        };
431    }
432
433    define_simple_column_parser!(
434        parse_column_span,
435        ColumnSpan,
436        ColumnSpanParseError,
437        ColumnSpanParseErrorOwned,
438        "column-span",
439        "none" => ColumnSpan::None,
440        "all" => ColumnSpan::All
441    );
442
443    define_simple_column_parser!(
444        parse_column_fill,
445        ColumnFill,
446        ColumnFillParseError,
447        ColumnFillParseErrorOwned,
448        "column-fill",
449        "auto" => ColumnFill::Auto,
450        "balance" => ColumnFill::Balance
451    );
452
453    // Parsers for column-rule-*
454
455    #[derive(Clone, PartialEq, Eq)]
456    pub enum ColumnRuleWidthParseError<'a> {
457        Pixel(CssPixelValueParseError<'a>),
458    }
459    impl_debug_as_display!(ColumnRuleWidthParseError<'a>);
460    impl_display! { ColumnRuleWidthParseError<'a>, { Pixel(e) => format!("{}", e) }}
461    impl_from! { CssPixelValueParseError<'a>, ColumnRuleWidthParseError::Pixel }
462    #[derive(Debug, Clone, PartialEq, Eq)]
463    #[repr(C, u8)]
464    pub enum ColumnRuleWidthParseErrorOwned {
465        Pixel(CssPixelValueParseErrorOwned),
466    }
467    impl ColumnRuleWidthParseError<'_> {
468        #[must_use]
469        pub fn to_contained(&self) -> ColumnRuleWidthParseErrorOwned {
470            match self {
471                ColumnRuleWidthParseError::Pixel(e) => {
472                    ColumnRuleWidthParseErrorOwned::Pixel(e.to_contained())
473                }
474            }
475        }
476    }
477    impl ColumnRuleWidthParseErrorOwned {
478        #[must_use]
479        pub fn to_shared(&self) -> ColumnRuleWidthParseError<'_> {
480            match self {
481                Self::Pixel(e) => ColumnRuleWidthParseError::Pixel(e.to_shared()),
482            }
483        }
484    }
485    /// # Errors
486    ///
487    /// Returns an error if `input` is not a valid CSS `column-rule-width` value.
488    pub fn parse_column_rule_width(
489        input: &str,
490    ) -> Result<ColumnRuleWidth, ColumnRuleWidthParseError<'_>> {
491        Ok(ColumnRuleWidth {
492            inner: parse_pixel_value(input)?,
493        })
494    }
495
496    #[derive(Clone, PartialEq, Eq)]
497    pub enum ColumnRuleStyleParseError<'a> {
498        Style(CssBorderStyleParseError<'a>),
499    }
500    impl_debug_as_display!(ColumnRuleStyleParseError<'a>);
501    impl_display! { ColumnRuleStyleParseError<'a>, { Style(e) => format!("{}", e) }}
502    impl_from! { CssBorderStyleParseError<'a>, ColumnRuleStyleParseError::Style }
503    #[derive(Debug, Clone, PartialEq, Eq)]
504    #[repr(C, u8)]
505    pub enum ColumnRuleStyleParseErrorOwned {
506        Style(CssBorderStyleParseErrorOwned),
507    }
508    impl ColumnRuleStyleParseError<'_> {
509        #[must_use]
510        pub fn to_contained(&self) -> ColumnRuleStyleParseErrorOwned {
511            match self {
512                ColumnRuleStyleParseError::Style(e) => {
513                    ColumnRuleStyleParseErrorOwned::Style(e.to_contained())
514                }
515            }
516        }
517    }
518    impl ColumnRuleStyleParseErrorOwned {
519        #[must_use]
520        pub fn to_shared(&self) -> ColumnRuleStyleParseError<'_> {
521            match self {
522                Self::Style(e) => ColumnRuleStyleParseError::Style(e.to_shared()),
523            }
524        }
525    }
526    /// # Errors
527    ///
528    /// Returns an error if `input` is not a valid CSS `column-rule-style` value.
529    pub fn parse_column_rule_style(
530        input: &str,
531    ) -> Result<ColumnRuleStyle, ColumnRuleStyleParseError<'_>> {
532        Ok(ColumnRuleStyle {
533            inner: parse_border_style(input)?,
534        })
535    }
536
537    #[derive(Clone, PartialEq)]
538    pub enum ColumnRuleColorParseError<'a> {
539        Color(CssColorParseError<'a>),
540    }
541    impl_debug_as_display!(ColumnRuleColorParseError<'a>);
542    impl_display! { ColumnRuleColorParseError<'a>, { Color(e) => format!("{}", e) }}
543    impl_from! { CssColorParseError<'a>, ColumnRuleColorParseError::Color }
544    #[derive(Debug, Clone, PartialEq)]
545    #[repr(C, u8)]
546    pub enum ColumnRuleColorParseErrorOwned {
547        Color(CssColorParseErrorOwned),
548    }
549    impl ColumnRuleColorParseError<'_> {
550        #[must_use]
551        pub fn to_contained(&self) -> ColumnRuleColorParseErrorOwned {
552            match self {
553                ColumnRuleColorParseError::Color(e) => {
554                    ColumnRuleColorParseErrorOwned::Color(e.to_contained())
555                }
556            }
557        }
558    }
559    impl ColumnRuleColorParseErrorOwned {
560        #[must_use]
561        pub fn to_shared(&self) -> ColumnRuleColorParseError<'_> {
562            match self {
563                Self::Color(e) => ColumnRuleColorParseError::Color(e.to_shared()),
564            }
565        }
566    }
567    /// # Errors
568    ///
569    /// Returns an error if `input` is not a valid CSS `column-rule-color` value.
570    pub fn parse_column_rule_color(
571        input: &str,
572    ) -> Result<ColumnRuleColor, ColumnRuleColorParseError<'_>> {
573        Ok(ColumnRuleColor {
574            inner: parse_css_color(input)?,
575        })
576    }
577}
578
579#[cfg(feature = "parser")]
580pub use parser::*;
581
582#[cfg(all(test, feature = "parser"))]
583mod tests {
584    use super::*;
585
586    #[test]
587    fn test_parse_column_count() {
588        assert_eq!(parse_column_count("auto").unwrap(), ColumnCount::Auto);
589        assert_eq!(parse_column_count("3").unwrap(), ColumnCount::Integer(3));
590        assert!(parse_column_count("none").is_err());
591        assert!(parse_column_count("2.5").is_err());
592    }
593
594    #[test]
595    fn test_parse_column_width() {
596        assert_eq!(parse_column_width("auto").unwrap(), ColumnWidth::Auto);
597        assert_eq!(
598            parse_column_width("200px").unwrap(),
599            ColumnWidth::Length(PixelValue::px(200.0))
600        );
601        assert_eq!(
602            parse_column_width("15em").unwrap(),
603            ColumnWidth::Length(PixelValue::em(15.0))
604        );
605        assert!(parse_column_width("50%").is_ok()); // Percentage is valid for column-width
606    }
607
608    #[test]
609    fn test_parse_column_span() {
610        assert_eq!(parse_column_span("none").unwrap(), ColumnSpan::None);
611        assert_eq!(parse_column_span("all").unwrap(), ColumnSpan::All);
612        assert!(parse_column_span("2").is_err());
613    }
614
615    #[test]
616    fn test_parse_column_fill() {
617        assert_eq!(parse_column_fill("auto").unwrap(), ColumnFill::Auto);
618        assert_eq!(parse_column_fill("balance").unwrap(), ColumnFill::Balance);
619        assert!(parse_column_fill("none").is_err());
620    }
621
622    #[test]
623    fn test_parse_column_rule() {
624        assert_eq!(
625            parse_column_rule_width("5px").unwrap().inner,
626            PixelValue::px(5.0)
627        );
628        assert_eq!(
629            parse_column_rule_style("dotted").unwrap().inner,
630            BorderStyle::Dotted
631        );
632        assert_eq!(parse_column_rule_color("blue").unwrap().inner, ColorU::BLUE);
633    }
634}
635
636#[cfg(all(test, feature = "parser"))]
637#[allow(clippy::float_cmp)] // parsed values are compared against the exact source literals
638mod autotest_generated {
639    use super::*;
640    use crate::{codegen::format::FormatAsRustCode, corety::AzString, props::basic::SizeMetric};
641
642    // A long-but-not-pathological input size for the "does not hang" cases.
643    const LONG: usize = 1_000_000;
644
645    // -----------------------------------------------------------------
646    // parse_column_count
647    // -----------------------------------------------------------------
648
649    #[test]
650    fn column_count_valid_minimal_and_trimming() {
651        assert_eq!(parse_column_count("auto").unwrap(), ColumnCount::Auto);
652        assert_eq!(parse_column_count("1").unwrap(), ColumnCount::Integer(1));
653        // The parser trims, so surrounding whitespace must not change the value.
654        assert_eq!(parse_column_count("  auto\t\n").unwrap(), ColumnCount::Auto);
655        assert_eq!(
656            parse_column_count(" \n 12 \t").unwrap(),
657            ColumnCount::Integer(12)
658        );
659    }
660
661    #[test]
662    fn column_count_rejects_empty_and_whitespace_only() {
663        assert!(parse_column_count("").is_err());
664        assert!(parse_column_count("   ").is_err());
665        assert!(parse_column_count("\t\n\r ").is_err());
666    }
667
668    #[test]
669    fn column_count_rejects_garbage_without_panicking() {
670        for bad in [
671            "none",
672            "auto auto",
673            "3px",
674            "3;garbage",
675            "3 4",
676            "2.5",
677            "0x10",
678            "1_000",
679            "--3",
680            "+-3",
681            "\0",
682            "3\0",
683            "١٢٣",
684            "Ù£",
685            "3",
686            "NaN",
687            "inf",
688            "-inf",
689            "e5",
690            "1e3",
691        ] {
692            assert!(
693                parse_column_count(bad).is_err(),
694                "column-count accepted garbage: {bad:?}"
695            );
696        }
697    }
698
699    #[test]
700    fn column_count_case_sensitivity_is_exact() {
701        // NOTE: CSS keywords are ASCII-case-insensitive; this parser is not.
702        // Documented here as the *current* contract - it must at least not panic.
703        assert!(parse_column_count("AUTO").is_err());
704        assert!(parse_column_count("Auto").is_err());
705    }
706
707    #[test]
708    fn column_count_u32_boundaries_saturate_into_err_not_wrap() {
709        // Lower bound: 0 is accepted even though CSS requires a positive integer.
710        assert_eq!(parse_column_count("0").unwrap(), ColumnCount::Integer(0));
711        // `u32::from_str` accepts a leading '+'.
712        assert_eq!(parse_column_count("+7").unwrap(), ColumnCount::Integer(7));
713        // Exact u32 ceiling parses; one above must be a clean Err, never a wrap to 0.
714        assert_eq!(
715            parse_column_count("4294967295").unwrap(),
716            ColumnCount::Integer(u32::MAX)
717        );
718        assert!(parse_column_count("4294967296").is_err());
719        // i64::MAX / u64::MAX / a 40-digit number all overflow u32 -> Err, no wraparound.
720        assert!(parse_column_count("9223372036854775807").is_err());
721        assert!(parse_column_count("18446744073709551615").is_err());
722        assert!(parse_column_count("9999999999999999999999999999999999999999").is_err());
723        // Negatives (including -0) are not representable in u32.
724        assert!(parse_column_count("-1").is_err());
725        assert!(parse_column_count("-0").is_err());
726    }
727
728    #[test]
729    fn column_count_extremely_long_input_terminates() {
730        let huge = "9".repeat(LONG);
731        assert!(parse_column_count(&huge).is_err());
732        let huge_keyword = "auto".repeat(LONG / 4);
733        assert!(parse_column_count(&huge_keyword).is_err());
734        // Whitespace-padded huge input: trimming must not be quadratic or panic.
735        let padded = format!("{}{}{}", " ".repeat(10_000), "7", " ".repeat(10_000));
736        assert_eq!(
737            parse_column_count(&padded).unwrap(),
738            ColumnCount::Integer(7)
739        );
740    }
741
742    #[test]
743    fn column_count_deeply_nested_input_does_not_stack_overflow() {
744        let nested = "(".repeat(10_000);
745        assert!(parse_column_count(&nested).is_err());
746        let balanced = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
747        assert!(parse_column_count(&balanced).is_err());
748    }
749
750    #[test]
751    fn column_count_unicode_input_does_not_panic() {
752        for bad in [
753            "\u{1F600}",
754            "auto\u{1F600}",
755            "e\u{301}",     // combining acute accent
756            "\u{202E}3",    // RTL override
757            "\u{FEFF}auto", // BOM (not whitespace -> not trimmed)
758            "au\u{0000}to",
759        ] {
760            assert!(
761                parse_column_count(bad).is_err(),
762                "column-count accepted unicode junk: {bad:?}"
763            );
764        }
765    }
766
767    #[test]
768    fn column_count_roundtrips_through_print_as_css_value() {
769        for value in [
770            ColumnCount::Auto,
771            ColumnCount::Integer(0),
772            ColumnCount::Integer(1),
773            ColumnCount::Integer(u32::MAX),
774        ] {
775            let printed = value.print_as_css_value();
776            assert_eq!(
777                parse_column_count(&printed).unwrap(),
778                value,
779                "round-trip failed for {value:?} (printed as {printed:?})"
780            );
781        }
782    }
783
784    // -----------------------------------------------------------------
785    // ColumnCountParseError::to_contained / ColumnCountParseErrorOwned::to_shared
786    // -----------------------------------------------------------------
787
788    #[test]
789    fn column_count_error_invalid_value_roundtrips_losslessly() {
790        for s in ["", "x", "  padded  ", "\u{1F600}"] {
791            let shared = ColumnCountParseError::InvalidValue(s);
792            let owned = shared.to_contained();
793            assert_eq!(
794                owned,
795                ColumnCountParseErrorOwned::InvalidValue(AzString::from(s))
796            );
797            assert_eq!(owned.to_shared(), shared);
798        }
799    }
800
801    #[test]
802    fn column_count_error_parse_int_roundtrip_is_lossy_but_safe() {
803        let err = parse_column_count("abc").unwrap_err();
804        assert_eq!(
805            err,
806            ColumnCountParseError::ParseInt("abc".parse::<u32>().unwrap_err())
807        );
808
809        let owned = err.to_contained();
810        match &owned {
811            ColumnCountParseErrorOwned::ParseInt(msg) => {
812                assert!(!msg.as_str().is_empty(), "ParseInt message was dropped");
813            }
814            other => panic!("expected ParseInt, got {other:?}"),
815        }
816
817        // Documented lossy path: a ParseIntError cannot be rebuilt from its Display
818        // string, so to_shared() degrades to a generic InvalidValue instead of panicking.
819        assert_eq!(
820            owned.to_shared(),
821            ColumnCountParseError::InvalidValue("invalid integer")
822        );
823    }
824
825    #[test]
826    fn column_count_error_debug_equals_display_and_keeps_input() {
827        let err = ColumnCountParseError::InvalidValue("weird\u{1F600}input");
828        assert_eq!(format!("{err:?}"), format!("{err}"));
829        assert!(format!("{err}").contains("weird\u{1F600}input"));
830
831        // Overflow errors must render without panicking on an empty/huge instance.
832        let overflow = parse_column_count("4294967296").unwrap_err();
833        assert!(!format!("{overflow}").is_empty());
834        assert!(!format!("{:?}", overflow.to_contained()).is_empty());
835    }
836
837    // -----------------------------------------------------------------
838    // parse_column_width
839    // -----------------------------------------------------------------
840
841    #[test]
842    fn column_width_valid_minimal_and_trimming() {
843        assert_eq!(parse_column_width("auto").unwrap(), ColumnWidth::Auto);
844        assert_eq!(parse_column_width("  auto  ").unwrap(), ColumnWidth::Auto);
845        assert_eq!(
846            parse_column_width("\t1px\n").unwrap(),
847            ColumnWidth::Length(PixelValue::px(1.0))
848        );
849    }
850
851    #[test]
852    fn column_width_rejects_empty_whitespace_and_garbage() {
853        assert!(parse_column_width("").is_err());
854        assert!(parse_column_width("   ").is_err());
855        assert!(parse_column_width("\t\n").is_err());
856        for bad in [
857            "px",
858            "em",
859            "%",
860            "ten-px",
861            "200px;garbage",
862            "200 px extra",
863            "#200px",
864            "\u{1F600}",
865            "20\u{301}px",
866            "AUTO",
867        ] {
868            assert!(
869                parse_column_width(bad).is_err(),
870                "column-width accepted garbage: {bad:?}"
871            );
872        }
873    }
874
875    #[test]
876    fn column_width_non_finite_numbers_are_stored_finite() {
877        // f32::from_str accepts "NaN"/"inf", so these reach FloatValue::new().
878        // The isize-backed FloatValue must clamp them - a NaN/inf leaking into
879        // layout would poison every downstream computation.
880        let nan = parse_column_width("NaN").unwrap();
881        assert_eq!(nan, ColumnWidth::Length(PixelValue::px(0.0)));
882
883        for input in ["inf", "Infinity", "-inf", "-Infinity", "1e40px", "-1e40px"] {
884            let parsed = parse_column_width(input).unwrap();
885            match parsed {
886                ColumnWidth::Length(px) => assert!(
887                    px.number.get().is_finite(),
888                    "{input:?} produced a non-finite PixelValue"
889                ),
890                ColumnWidth::Auto => panic!("{input:?} unexpectedly parsed as auto"),
891            }
892        }
893    }
894
895    #[test]
896    fn column_width_zero_and_subnormal_boundaries() {
897        assert_eq!(
898            parse_column_width("0").unwrap(),
899            ColumnWidth::Length(PixelValue::px(0.0))
900        );
901        // -0.0 must normalize to the same stored value as +0.0 (FloatValue is an isize).
902        assert_eq!(
903            parse_column_width("-0").unwrap(),
904            parse_column_width("0").unwrap()
905        );
906        assert_eq!(
907            parse_column_width("-0px").unwrap(),
908            ColumnWidth::Length(PixelValue::px(0.0))
909        );
910        // Subnormal f32: 1e-45 * 1000 truncates to 0 rather than panicking.
911        assert_eq!(
912            parse_column_width("1e-45px").unwrap(),
913            ColumnWidth::Length(PixelValue::px(0.0))
914        );
915        // Negative lengths are (currently) accepted by the parser; must not wrap sign.
916        match parse_column_width("-10px").unwrap() {
917            ColumnWidth::Length(px) => assert!(px.number.get() < 0.0),
918            ColumnWidth::Auto => panic!("-10px parsed as auto"),
919        }
920    }
921
922    #[test]
923    fn column_width_extremely_long_input_terminates() {
924        let huge_digits = format!("{}px", "9".repeat(LONG));
925        let parsed = parse_column_width(&huge_digits).unwrap();
926        match parsed {
927            ColumnWidth::Length(px) => assert!(px.number.get().is_finite()),
928            ColumnWidth::Auto => panic!("digit soup parsed as auto"),
929        }
930        assert!(parse_column_width(&"a".repeat(LONG)).is_err());
931        assert!(parse_column_width(&"auto".repeat(LONG / 4)).is_err());
932    }
933
934    #[test]
935    fn column_width_deeply_nested_input_does_not_stack_overflow() {
936        assert!(parse_column_width(&"(".repeat(10_000)).is_err());
937        let balanced = format!("calc{}{}", "(".repeat(10_000), ")".repeat(10_000));
938        assert!(parse_column_width(&balanced).is_err());
939    }
940
941    #[test]
942    fn column_width_roundtrips_through_print_as_css_value() {
943        // Only exactly-representable numbers: FloatValue truncates to 1/1000ths,
944        // so e.g. 2.54cm is *not* expected to survive a print/parse cycle.
945        let values = [
946            ColumnWidth::Auto,
947            ColumnWidth::Length(PixelValue::px(0.0)),
948            ColumnWidth::Length(PixelValue::px(200.0)),
949            ColumnWidth::Length(PixelValue::px(-12.5)),
950            ColumnWidth::Length(PixelValue::em(1.5)),
951            ColumnWidth::Length(PixelValue::rem(2.0)),
952            ColumnWidth::Length(PixelValue::pt(-20.0)),
953            ColumnWidth::Length(PixelValue::percent(50.0)),
954            ColumnWidth::Length(PixelValue::inch(1.0)),
955            ColumnWidth::Length(PixelValue::cm(3.0)),
956            ColumnWidth::Length(PixelValue::mm(10.0)),
957            ColumnWidth::Length(PixelValue::from_metric(SizeMetric::Vw, 10.0)),
958            ColumnWidth::Length(PixelValue::from_metric(SizeMetric::Vh, 10.0)),
959            ColumnWidth::Length(PixelValue::from_metric(SizeMetric::Vmax, 10.0)),
960        ];
961        for value in values {
962            let printed = value.print_as_css_value();
963            assert_eq!(
964                parse_column_width(&printed).unwrap(),
965                value,
966                "round-trip failed for {value:?} (printed as {printed:?})"
967            );
968        }
969    }
970
971    // -----------------------------------------------------------------
972    // ColumnWidthParseError::to_contained / ColumnWidthParseErrorOwned::to_shared
973    // -----------------------------------------------------------------
974
975    #[test]
976    fn column_width_error_roundtrips_through_owned() {
977        // InvalidValue is only reachable by hand - the parser always delegates to
978        // the pixel parser - but the conversion still has to be lossless.
979        for s in ["", "bogus", "\u{1F600}"] {
980            let shared = ColumnWidthParseError::InvalidValue(s);
981            let owned = shared.to_contained();
982            assert_eq!(
983                owned,
984                ColumnWidthParseErrorOwned::InvalidValue(AzString::from(s))
985            );
986            assert_eq!(owned.to_shared(), shared);
987        }
988
989        // The variants the parser actually produces.
990        for bad in ["", "ten-px", "px", "%"] {
991            let err = parse_column_width(bad).unwrap_err();
992            assert!(
993                matches!(err, ColumnWidthParseError::PixelValue(_)),
994                "{bad:?} produced an unexpected error variant: {err:?}"
995            );
996            let owned = err.to_contained();
997            assert_eq!(owned.to_shared(), err, "lossy round-trip for {bad:?}");
998            assert_eq!(format!("{err:?}"), format!("{err}"));
999        }
1000    }
1001
1002    // -----------------------------------------------------------------
1003    // parse_column_span / parse_column_fill
1004    // -----------------------------------------------------------------
1005
1006    #[test]
1007    fn column_span_and_fill_accept_only_their_keywords() {
1008        assert_eq!(parse_column_span("none").unwrap(), ColumnSpan::None);
1009        assert_eq!(parse_column_span(" all \t").unwrap(), ColumnSpan::All);
1010        assert_eq!(parse_column_fill("auto").unwrap(), ColumnFill::Auto);
1011        assert_eq!(
1012            parse_column_fill("\n balance ").unwrap(),
1013            ColumnFill::Balance
1014        );
1015
1016        for bad in [
1017            "",
1018            "   ",
1019            "2",
1020            "ALL",
1021            "None",
1022            "all all",
1023            "all;",
1024            "\u{1F600}",
1025            "nonee",
1026            "\0",
1027        ] {
1028            assert!(
1029                parse_column_span(bad).is_err(),
1030                "column-span accepted {bad:?}"
1031            );
1032        }
1033        for bad in [
1034            "",
1035            "   ",
1036            "none",
1037            "AUTO",
1038            "balanced",
1039            "auto balance",
1040            "\u{1F600}",
1041        ] {
1042            assert!(
1043                parse_column_fill(bad).is_err(),
1044                "column-fill accepted {bad:?}"
1045            );
1046        }
1047        // The two properties must not accept each other's keywords.
1048        assert!(parse_column_span("balance").is_err());
1049        assert!(parse_column_fill("all").is_err());
1050    }
1051
1052    #[test]
1053    fn column_span_and_fill_survive_long_and_nested_input() {
1054        let huge = "all".repeat(LONG / 3);
1055        assert!(parse_column_span(&huge).is_err());
1056        assert!(parse_column_fill(&huge).is_err());
1057        let nested = "(".repeat(10_000);
1058        assert!(parse_column_span(&nested).is_err());
1059        assert!(parse_column_fill(&nested).is_err());
1060    }
1061
1062    #[test]
1063    fn column_span_error_reports_the_untrimmed_input() {
1064        // The macro-generated parser matches on the trimmed input but reports the
1065        // *original* one - that asymmetry is load-bearing for error messages.
1066        let err = parse_column_span("  bogus  ").unwrap_err();
1067        match err {
1068            ColumnSpanParseError::InvalidValue(s) => assert_eq!(s, "  bogus  "),
1069        }
1070        assert_eq!(format!("{err:?}"), format!("{err}"));
1071        assert!(format!("{err}").contains("column-span"));
1072
1073        let owned = err.to_contained();
1074        assert_eq!(
1075            owned,
1076            ColumnSpanParseErrorOwned::InvalidValue(AzString::from("  bogus  "))
1077        );
1078        assert_eq!(owned.to_shared(), err);
1079
1080        let fill_err = parse_column_fill("\u{1F600}").unwrap_err();
1081        let fill_owned = fill_err.to_contained();
1082        assert_eq!(fill_owned.to_shared(), fill_err);
1083        assert!(format!("{fill_err}").contains("column-fill"));
1084    }
1085
1086    // -----------------------------------------------------------------
1087    // parse_column_rule_width
1088    // -----------------------------------------------------------------
1089
1090    #[test]
1091    fn column_rule_width_valid_and_invalid() {
1092        assert_eq!(
1093            parse_column_rule_width("5px").unwrap().inner,
1094            PixelValue::px(5.0)
1095        );
1096        assert_eq!(
1097            parse_column_rule_width("  0  ").unwrap().inner,
1098            PixelValue::px(0.0)
1099        );
1100        for bad in [
1101            "",
1102            "   ",
1103            "auto",
1104            "solid",
1105            "px",
1106            "\u{1F600}",
1107            "5px;5px",
1108            "5 px extra",
1109        ] {
1110            assert!(
1111                parse_column_rule_width(bad).is_err(),
1112                "column-rule-width accepted {bad:?}"
1113            );
1114        }
1115    }
1116
1117    #[test]
1118    fn column_rule_width_extremes_stay_finite() {
1119        for input in ["NaN", "inf", "-inf", "1e40px", "-1e40px", "1e-45px"] {
1120            let w = parse_column_rule_width(input).unwrap();
1121            assert!(
1122                w.inner.number.get().is_finite(),
1123                "{input:?} produced a non-finite column-rule-width"
1124            );
1125        }
1126        assert!(parse_column_rule_width(&"9".repeat(LONG))
1127            .unwrap()
1128            .inner
1129            .number
1130            .get()
1131            .is_finite());
1132        assert!(parse_column_rule_width(&"(".repeat(10_000)).is_err());
1133    }
1134
1135    #[test]
1136    fn column_rule_width_default_is_medium_and_roundtrips() {
1137        let default = ColumnRuleWidth::default();
1138        assert_eq!(default.inner, PixelValue::const_px(3));
1139        assert_eq!(default.print_as_css_value(), "3px");
1140        assert_eq!(parse_column_rule_width("3px").unwrap(), default);
1141
1142        for value in [
1143            ColumnRuleWidth::default(),
1144            ColumnRuleWidth {
1145                inner: PixelValue::px(0.0),
1146            },
1147            ColumnRuleWidth {
1148                inner: PixelValue::em(2.5),
1149            },
1150            ColumnRuleWidth {
1151                inner: PixelValue::percent(-25.0),
1152            },
1153        ] {
1154            let printed = value.print_as_css_value();
1155            assert_eq!(parse_column_rule_width(&printed).unwrap(), value);
1156        }
1157    }
1158
1159    #[test]
1160    fn column_rule_width_error_roundtrips_through_owned() {
1161        for bad in ["", "auto", "px"] {
1162            let err = parse_column_rule_width(bad).unwrap_err();
1163            let owned = err.to_contained();
1164            assert_eq!(owned.to_shared(), err, "lossy round-trip for {bad:?}");
1165            assert_eq!(format!("{err:?}"), format!("{err}"));
1166            assert!(!format!("{err}").is_empty());
1167        }
1168    }
1169
1170    // -----------------------------------------------------------------
1171    // parse_column_rule_style
1172    // -----------------------------------------------------------------
1173
1174    #[test]
1175    fn column_rule_style_accepts_every_border_style_and_roundtrips() {
1176        for style in [
1177            BorderStyle::None,
1178            BorderStyle::Solid,
1179            BorderStyle::Double,
1180            BorderStyle::Dotted,
1181            BorderStyle::Dashed,
1182            BorderStyle::Hidden,
1183            BorderStyle::Groove,
1184            BorderStyle::Ridge,
1185            BorderStyle::Inset,
1186            BorderStyle::Outset,
1187        ] {
1188            let value = ColumnRuleStyle { inner: style };
1189            let printed = value.print_as_css_value();
1190            assert_eq!(
1191                parse_column_rule_style(&printed).unwrap(),
1192                value,
1193                "round-trip failed for {style:?} (printed as {printed:?})"
1194            );
1195        }
1196    }
1197
1198    #[test]
1199    fn column_rule_style_rejects_garbage_without_panicking() {
1200        for bad in [
1201            "",
1202            "   ",
1203            "SOLID",
1204            "solidd",
1205            "solid solid",
1206            "3px",
1207            "\u{1F600}",
1208            "\0",
1209            "soli\u{0301}d",
1210        ] {
1211            assert!(
1212                parse_column_rule_style(bad).is_err(),
1213                "column-rule-style accepted {bad:?}"
1214            );
1215        }
1216        assert!(parse_column_rule_style(&"solid".repeat(LONG / 5)).is_err());
1217        assert!(parse_column_rule_style(&"(".repeat(10_000)).is_err());
1218        // Leading/trailing whitespace *is* trimmed.
1219        assert_eq!(
1220            parse_column_rule_style("  dotted \n").unwrap().inner,
1221            BorderStyle::Dotted
1222        );
1223    }
1224
1225    #[test]
1226    fn column_rule_style_error_roundtrips_through_owned() {
1227        let err = parse_column_rule_style("bogus").unwrap_err();
1228        let owned = err.to_contained();
1229        assert_eq!(owned.to_shared(), err);
1230        assert_eq!(format!("{err:?}"), format!("{err}"));
1231        assert!(format!("{err}").contains("bogus"));
1232
1233        let unicode_err = parse_column_rule_style("\u{1F600}").unwrap_err();
1234        let unicode_owned = unicode_err.to_contained();
1235        assert_eq!(unicode_owned.to_shared(), unicode_err);
1236    }
1237
1238    // -----------------------------------------------------------------
1239    // parse_column_rule_color
1240    // -----------------------------------------------------------------
1241
1242    #[test]
1243    fn column_rule_color_accepts_names_hex_and_functions() {
1244        assert_eq!(parse_column_rule_color("blue").unwrap().inner, ColorU::BLUE);
1245        // Named colors *are* case-insensitive here (unlike column-span/fill/count).
1246        assert_eq!(
1247            parse_column_rule_color("  BLUE \t").unwrap().inner,
1248            ColorU::BLUE
1249        );
1250        assert_eq!(
1251            parse_column_rule_color("#000f").unwrap().inner,
1252            ColorU::BLACK
1253        );
1254        assert_eq!(
1255            parse_column_rule_color("#ff000080").unwrap().inner,
1256            ColorU::rgba(255, 0, 0, 128)
1257        );
1258        assert_eq!(parse_column_rule_color("transparent").unwrap().inner.a, 0);
1259        assert_eq!(
1260            parse_column_rule_color("rgba(255, 0, 0, 1.0)")
1261                .unwrap()
1262                .inner,
1263            ColorU::RED
1264        );
1265    }
1266
1267    #[test]
1268    fn column_rule_color_rejects_garbage_without_panicking() {
1269        for bad in [
1270            "",
1271            "   ",
1272            "#",
1273            "#f",
1274            "#ff",
1275            "#fffff",
1276            "#zzzzzz",
1277            "notacolor",
1278            "rgb(",
1279            "rgb(1,2",
1280            "rgb()",
1281            "rgb(300)",
1282            "hsl(",
1283            "\u{1F600}",
1284            "#\u{1F600}",
1285            "\u{130}", // dotted capital I: to_lowercase() expands to 2 chars
1286        ] {
1287            assert!(
1288                parse_column_rule_color(bad).is_err(),
1289                "column-rule-color accepted {bad:?}"
1290            );
1291        }
1292    }
1293
1294    #[test]
1295    fn column_rule_color_survives_long_and_nested_input() {
1296        assert!(parse_column_rule_color(&"a".repeat(100_000)).is_err());
1297        assert!(parse_column_rule_color(&"#".repeat(100_000)).is_err());
1298        // Unbalanced and deeply nested parens must not recurse or hang.
1299        assert!(parse_column_rule_color(&"(".repeat(10_000)).is_err());
1300        assert!(parse_column_rule_color(&format!("rgb{}", "(".repeat(10_000))).is_err());
1301        let nested = format!("rgb{}{}", "(".repeat(10_000), ")".repeat(10_000));
1302        assert!(parse_column_rule_color(&nested).is_err());
1303    }
1304
1305    #[test]
1306    fn column_rule_color_roundtrips_through_to_hash() {
1307        for color in [
1308            ColorU::BLACK,
1309            ColorU::WHITE,
1310            ColorU::RED,
1311            ColorU::BLUE,
1312            ColorU::TRANSPARENT,
1313            ColorU::rgba(1, 2, 3, 4),
1314            ColorU::rgba(255, 255, 255, 0),
1315            ColorU::rgba(0, 0, 0, 255),
1316        ] {
1317            let value = ColumnRuleColor { inner: color };
1318            let printed = value.print_as_css_value(); // "#rrggbbaa"
1319            assert_eq!(printed.len(), 9, "unexpected hash form: {printed:?}");
1320            assert_eq!(
1321                parse_column_rule_color(&printed).unwrap(),
1322                value,
1323                "round-trip failed for {color:?} (printed as {printed:?})"
1324            );
1325        }
1326    }
1327
1328    #[test]
1329    fn column_rule_color_error_roundtrips_through_owned() {
1330        for bad in ["", "notacolor", "rgb(", "#zzzzzz"] {
1331            let err = parse_column_rule_color(bad).unwrap_err();
1332            let owned = err.to_contained();
1333            assert_eq!(owned.to_shared(), err, "lossy round-trip for {bad:?}");
1334            assert_eq!(format!("{err:?}"), format!("{err}"));
1335            assert!(!format!("{err}").is_empty());
1336        }
1337    }
1338
1339    // -----------------------------------------------------------------
1340    // Type invariants: defaults, ordering, hashing, codegen
1341    // -----------------------------------------------------------------
1342
1343    #[test]
1344    fn defaults_match_the_documented_css_initial_values() {
1345        assert_eq!(ColumnCount::default(), ColumnCount::Auto);
1346        assert_eq!(ColumnWidth::default(), ColumnWidth::Auto);
1347        assert_eq!(ColumnSpan::default(), ColumnSpan::None);
1348        assert_eq!(ColumnFill::default(), ColumnFill::Balance);
1349        assert_eq!(ColumnRuleStyle::default().inner, BorderStyle::None);
1350        // NOTE: per CSS this should be `currentcolor`; the type doc records the deviation.
1351        assert_eq!(ColumnRuleColor::default().inner, ColorU::BLACK);
1352
1353        // Every default must print to something its own parser accepts.
1354        assert_eq!(
1355            parse_column_count(&ColumnCount::default().print_as_css_value()).unwrap(),
1356            ColumnCount::default()
1357        );
1358        assert_eq!(
1359            parse_column_width(&ColumnWidth::default().print_as_css_value()).unwrap(),
1360            ColumnWidth::default()
1361        );
1362        assert_eq!(
1363            parse_column_span(&ColumnSpan::default().print_as_css_value()).unwrap(),
1364            ColumnSpan::default()
1365        );
1366        assert_eq!(
1367            parse_column_fill(&ColumnFill::default().print_as_css_value()).unwrap(),
1368            ColumnFill::default()
1369        );
1370        assert_eq!(
1371            parse_column_rule_width(&ColumnRuleWidth::default().print_as_css_value()).unwrap(),
1372            ColumnRuleWidth::default()
1373        );
1374        assert_eq!(
1375            parse_column_rule_style(&ColumnRuleStyle::default().print_as_css_value()).unwrap(),
1376            ColumnRuleStyle::default()
1377        );
1378        assert_eq!(
1379            parse_column_rule_color(&ColumnRuleColor::default().print_as_css_value()).unwrap(),
1380            ColumnRuleColor::default()
1381        );
1382    }
1383
1384    #[test]
1385    fn ord_and_hash_agree_with_eq() {
1386        use std::{
1387            collections::hash_map::DefaultHasher,
1388            hash::{Hash, Hasher},
1389        };
1390
1391        fn hash_of<T: Hash>(t: &T) -> u64 {
1392            let mut h = DefaultHasher::new();
1393            t.hash(&mut h);
1394            h.finish()
1395        }
1396
1397        // Keyword variants sort before their value-carrying counterparts.
1398        assert!(ColumnCount::Auto < ColumnCount::Integer(0));
1399        assert!(ColumnCount::Integer(1) < ColumnCount::Integer(u32::MAX));
1400        assert!(ColumnWidth::Auto < ColumnWidth::Length(PixelValue::px(0.0)));
1401        assert!(ColumnSpan::None < ColumnSpan::All);
1402        assert!(ColumnFill::Auto < ColumnFill::Balance);
1403
1404        // Eq implies equal hashes (these types are used as prop-cache keys).
1405        assert_eq!(
1406            hash_of(&ColumnCount::Integer(7)),
1407            hash_of(&parse_column_count("7").unwrap())
1408        );
1409        assert_eq!(
1410            hash_of(&ColumnWidth::Length(PixelValue::px(0.0))),
1411            hash_of(&parse_column_width("-0px").unwrap())
1412        );
1413        assert_ne!(hash_of(&ColumnFill::Auto), hash_of(&ColumnFill::Balance));
1414    }
1415
1416    #[test]
1417    fn format_as_rust_code_emits_constructible_snippets() {
1418        assert_eq!(
1419            ColumnCount::Auto.format_as_rust_code(0),
1420            "ColumnCount::Auto"
1421        );
1422        assert_eq!(
1423            ColumnCount::Integer(u32::MAX).format_as_rust_code(0),
1424            "ColumnCount::Integer(4294967295)"
1425        );
1426        assert_eq!(
1427            ColumnWidth::Auto.format_as_rust_code(0),
1428            "ColumnWidth::Auto"
1429        );
1430        assert_eq!(ColumnSpan::All.format_as_rust_code(0), "ColumnSpan::All");
1431        assert_eq!(ColumnSpan::None.format_as_rust_code(0), "ColumnSpan::None");
1432        assert_eq!(ColumnFill::Auto.format_as_rust_code(0), "ColumnFill::Auto");
1433        assert_eq!(
1434            ColumnFill::Balance.format_as_rust_code(0),
1435            "ColumnFill::Balance"
1436        );
1437
1438        // Extreme instances must format without panicking.
1439        let wide = ColumnWidth::Length(PixelValue::px(f32::MAX));
1440        assert!(wide
1441            .format_as_rust_code(0)
1442            .starts_with("ColumnWidth::Length("));
1443        let rule = ColumnRuleWidth {
1444            inner: PixelValue::px(-0.5),
1445        };
1446        assert!(rule.format_as_rust_code(0).starts_with("ColumnRuleWidth {"));
1447        let style = ColumnRuleStyle {
1448            inner: BorderStyle::Dotted,
1449        };
1450        assert!(style.format_as_rust_code(0).contains("Dotted"));
1451        let color = ColumnRuleColor {
1452            inner: ColorU::TRANSPARENT,
1453        };
1454        assert!(color
1455            .format_as_rust_code(0)
1456            .starts_with("ColumnRuleColor {"));
1457    }
1458}