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

            
7
use alloc::string::{String, ToString};
8
use core::num::ParseIntError;
9

            
10
use 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)]
31
pub enum ColumnCount {
32
    #[default]
33
    Auto,
34
    Integer(u32),
35
}
36

            
37

            
38
impl PrintAsCssValue for ColumnCount {
39
5
    fn print_as_css_value(&self) -> String {
40
5
        match self {
41
2
            Self::Auto => "auto".to_string(),
42
3
            Self::Integer(i) => i.to_string(),
43
        }
44
5
    }
45
}
46

            
47
// --- column-width ---
48
#[allow(variant_size_differences)] // 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)]
55
pub enum ColumnWidth {
56
    #[default]
57
    Auto,
58
    Length(PixelValue),
59
}
60

            
61

            
62
impl PrintAsCssValue for ColumnWidth {
63
15
    fn print_as_css_value(&self) -> String {
64
15
        match self {
65
2
            Self::Auto => "auto".to_string(),
66
13
            Self::Length(px) => px.print_as_css_value(),
67
        }
68
15
    }
69
}
70

            
71
// --- column-span ---
72

            
73
/// CSS `column-span` property: whether an element spans across all columns.
74
///
75
/// Values: `none` (default) or `all`.
76
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
77
#[repr(C)]
78
#[derive(Default)]
79
pub enum ColumnSpan {
80
    #[default]
81
    None,
82
    All,
83
}
84

            
85

            
86
impl PrintAsCssValue for ColumnSpan {
87
1
    fn print_as_css_value(&self) -> String {
88
1
        String::from(match self {
89
1
            Self::None => "none",
90
            Self::All => "all",
91
        })
92
1
    }
93
}
94

            
95
// --- column-fill ---
96

            
97
/// CSS `column-fill` property: how content is distributed across columns.
98
///
99
/// Values: `balance` (default) or `auto`.
100
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
101
#[repr(C)]
102
#[derive(Default)]
103
pub enum ColumnFill {
104
    Auto,
105
    #[default]
106
    Balance,
107
}
108

            
109

            
110
impl PrintAsCssValue for ColumnFill {
111
1
    fn print_as_css_value(&self) -> String {
112
1
        String::from(match self {
113
            Self::Auto => "auto",
114
1
            Self::Balance => "balance",
115
        })
116
1
    }
117
}
118

            
119
// --- column-rule ---
120

            
121
/// CSS `column-rule-width` property: the width of the rule between columns.
122
///
123
/// Defaults to `medium` (3px).
124
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
125
#[repr(C)]
126
pub struct ColumnRuleWidth {
127
    pub inner: PixelValue,
128
}
129

            
130
impl Default for ColumnRuleWidth {
131
4
    fn default() -> Self {
132
4
        Self {
133
4
            inner: PixelValue::const_px(3),
134
4
        }
135
4
    }
136
}
137

            
138
impl PrintAsCssValue for ColumnRuleWidth {
139
6
    fn print_as_css_value(&self) -> String {
140
6
        self.inner.print_as_css_value()
141
6
    }
142
}
143

            
144
/// CSS `column-rule-style` property: the style of the rule between columns.
145
///
146
/// Uses `BorderStyle` values (e.g. `none`, `solid`, `dotted`).
147
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
148
#[repr(C)]
149
pub struct ColumnRuleStyle {
150
    pub inner: BorderStyle,
151
}
152

            
153
impl Default for ColumnRuleStyle {
154
3
    fn default() -> Self {
155
3
        Self {
156
3
            inner: BorderStyle::None,
157
3
        }
158
3
    }
159
}
160

            
161
impl PrintAsCssValue for ColumnRuleStyle {
162
11
    fn print_as_css_value(&self) -> String {
163
11
        self.inner.print_as_css_value()
164
11
    }
165
}
166

            
167
/// CSS `column-rule-color` property: the color of the rule between columns.
168
///
169
/// Per the CSS spec this should default to `currentcolor`, but currently
170
/// defaults to black as `currentcolor` requires a resolved-value pass at
171
/// layout time.
172
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
173
#[repr(C)]
174
pub struct ColumnRuleColor {
175
    pub inner: ColorU,
176
}
177

            
178
impl Default for ColumnRuleColor {
179
3
    fn default() -> Self {
180
        // NOTE: should be `currentcolor` per CSS spec, see doc comment on type
181
3
        Self {
182
3
            inner: ColorU::BLACK,
183
3
        }
184
3
    }
185
}
186

            
187
impl PrintAsCssValue for ColumnRuleColor {
188
9
    fn print_as_css_value(&self) -> String {
189
9
        self.inner.to_hash()
190
9
    }
191
}
192

            
193
// Formatting to Rust code
194
impl crate::codegen::format::FormatAsRustCode for ColumnCount {
195
2
    fn format_as_rust_code(&self, _tabs: usize) -> String {
196
2
        match self {
197
1
            Self::Auto => String::from("ColumnCount::Auto"),
198
1
            Self::Integer(i) => format!("ColumnCount::Integer({i})"),
199
        }
200
2
    }
201
}
202

            
203
impl crate::codegen::format::FormatAsRustCode for ColumnWidth {
204
2
    fn format_as_rust_code(&self, _tabs: usize) -> String {
205
2
        match self {
206
1
            Self::Auto => String::from("ColumnWidth::Auto"),
207
1
            Self::Length(px) => format!(
208
1
                "ColumnWidth::Length({})",
209
1
                crate::codegen::format::format_pixel_value(px)
210
            ),
211
        }
212
2
    }
213
}
214

            
215
impl crate::codegen::format::FormatAsRustCode for ColumnSpan {
216
2
    fn format_as_rust_code(&self, _tabs: usize) -> String {
217
2
        match self {
218
1
            Self::None => String::from("ColumnSpan::None"),
219
1
            Self::All => String::from("ColumnSpan::All"),
220
        }
221
2
    }
222
}
223

            
224
impl crate::codegen::format::FormatAsRustCode for ColumnFill {
225
2
    fn format_as_rust_code(&self, _tabs: usize) -> String {
226
2
        match self {
227
1
            Self::Auto => String::from("ColumnFill::Auto"),
228
1
            Self::Balance => String::from("ColumnFill::Balance"),
229
        }
230
2
    }
231
}
232

            
233
impl crate::codegen::format::FormatAsRustCode for ColumnRuleWidth {
234
1
    fn format_as_rust_code(&self, _tabs: usize) -> String {
235
1
        format!(
236
1
            "ColumnRuleWidth {{ inner: {} }}",
237
1
            crate::codegen::format::format_pixel_value(&self.inner)
238
        )
239
1
    }
240
}
241

            
242
impl crate::codegen::format::FormatAsRustCode for ColumnRuleStyle {
243
1
    fn format_as_rust_code(&self, tabs: usize) -> String {
244
1
        format!(
245
1
            "ColumnRuleStyle {{ inner: {} }}",
246
1
            self.inner.format_as_rust_code(tabs)
247
        )
248
1
    }
249
}
250

            
251
impl crate::codegen::format::FormatAsRustCode for ColumnRuleColor {
252
1
    fn format_as_rust_code(&self, _tabs: usize) -> String {
253
1
        format!(
254
1
            "ColumnRuleColor {{ inner: {} }}",
255
1
            crate::codegen::format::format_color_value(&self.inner)
256
        )
257
1
    }
258
}
259

            
260
// --- PARSERS ---
261

            
262
#[cfg(feature = "parser")]
263
pub mod parser {
264
    #[allow(clippy::wildcard_imports)] // parser submodule reuses the parent module's value types
265
    use super::*;
266
    use crate::corety::AzString;
267

            
268
    // -- ColumnCount parser
269

            
270
    #[derive(Clone, PartialEq, Eq)]
271
    pub enum ColumnCountParseError<'a> {
272
        InvalidValue(&'a str),
273
        ParseInt(ParseIntError),
274
    }
275

            
276
    impl_debug_as_display!(ColumnCountParseError<'a>);
277
    impl_display! { ColumnCountParseError<'a>, {
278
        InvalidValue(v) => format!("Invalid column-count value: \"{}\"", v),
279
        ParseInt(e) => format!("Invalid integer for column-count: {}", e),
280
    }}
281

            
282
    #[derive(Debug, Clone, PartialEq, Eq)]
283
    #[repr(C, u8)]
284
    pub enum ColumnCountParseErrorOwned {
285
        InvalidValue(AzString),
286
        ParseInt(AzString),
287
    }
288

            
289
    impl ColumnCountParseError<'_> {
290
6
        #[must_use] pub fn to_contained(&self) -> ColumnCountParseErrorOwned {
291
6
            match self {
292
4
                Self::InvalidValue(s) => ColumnCountParseErrorOwned::InvalidValue((*s).to_string().into()),
293
2
                Self::ParseInt(e) => ColumnCountParseErrorOwned::ParseInt(e.to_string().into()),
294
            }
295
6
        }
296
    }
297

            
298
    impl ColumnCountParseErrorOwned {
299
5
        #[must_use] pub fn to_shared(&self) -> ColumnCountParseError<'_> {
300
5
            match self {
301
4
                Self::InvalidValue(s) => ColumnCountParseError::InvalidValue(s),
302
                // ParseIntError cannot be reconstructed from its Display string,
303
                // so we fall back to a generic message. The original error text
304
                // is preserved in the owned `AzString` but not round-trippable.
305
1
                Self::ParseInt(_) => ColumnCountParseError::InvalidValue("invalid integer"),
306
            }
307
5
        }
308
    }
309

            
310
    /// # Errors
311
    ///
312
    /// Returns an error if `input` is not a valid CSS `column-count` value.
313
61
    pub fn parse_column_count(
314
61
        input: &str,
315
61
    ) -> Result<ColumnCount, ColumnCountParseError<'_>> {
316
61
        let trimmed = input.trim();
317
61
        if trimmed == "auto" {
318
5
            return Ok(ColumnCount::Auto);
319
56
        }
320
56
        let val: u32 = trimmed
321
56
            .parse()
322
56
            .map_err(ColumnCountParseError::ParseInt)?;
323
11
        Ok(ColumnCount::Integer(val))
324
61
    }
325

            
326
    // -- ColumnWidth parser
327

            
328
    #[derive(Clone, PartialEq, Eq)]
329
    pub enum ColumnWidthParseError<'a> {
330
        InvalidValue(&'a str),
331
        PixelValue(CssPixelValueParseError<'a>),
332
    }
333

            
334
    impl_debug_as_display!(ColumnWidthParseError<'a>);
335
    impl_display! { ColumnWidthParseError<'a>, {
336
        InvalidValue(v) => format!("Invalid column-width value: \"{}\"", v),
337
        PixelValue(e) => format!("{}", e),
338
    }}
339
    impl_from! { CssPixelValueParseError<'a>, ColumnWidthParseError::PixelValue }
340

            
341
    #[derive(Debug, Clone, PartialEq, Eq)]
342
    #[repr(C, u8)]
343
    pub enum ColumnWidthParseErrorOwned {
344
        InvalidValue(AzString),
345
        PixelValue(CssPixelValueParseErrorOwned),
346
    }
347

            
348
    impl ColumnWidthParseError<'_> {
349
7
        #[must_use] pub fn to_contained(&self) -> ColumnWidthParseErrorOwned {
350
7
            match self {
351
3
                Self::InvalidValue(s) => ColumnWidthParseErrorOwned::InvalidValue((*s).to_string().into()),
352
4
                Self::PixelValue(e) => ColumnWidthParseErrorOwned::PixelValue(e.to_contained()),
353
            }
354
7
        }
355
    }
356

            
357
    impl ColumnWidthParseErrorOwned {
358
7
        #[must_use] pub fn to_shared(&self) -> ColumnWidthParseError<'_> {
359
7
            match self {
360
3
                Self::InvalidValue(s) => ColumnWidthParseError::InvalidValue(s),
361
4
                Self::PixelValue(e) => ColumnWidthParseError::PixelValue(e.to_shared()),
362
            }
363
7
        }
364
    }
365

            
366
    /// # Errors
367
    ///
368
    /// Returns an error if `input` is not a valid CSS `column-width` value.
369
58
    pub fn parse_column_width(
370
58
        input: &str,
371
58
    ) -> Result<ColumnWidth, ColumnWidthParseError<'_>> {
372
58
        let trimmed = input.trim();
373
58
        if trimmed == "auto" {
374
5
            return Ok(ColumnWidth::Auto);
375
53
        }
376
53
        Ok(ColumnWidth::Length(parse_pixel_value(trimmed)?))
377
58
    }
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
2
                #[must_use] pub fn to_contained(&self) -> $error_owned_name {
407
2
                    match self {
408
2
                        Self::InvalidValue(s) => $error_owned_name::InvalidValue(s.to_string().into()),
409
                    }
410
2
                }
411
            }
412

            
413
            impl $error_owned_name {
414
2
                #[must_use] pub fn to_shared(&self) -> $error_name<'_> {
415
2
                    match self {
416
2
                        Self::InvalidValue(s) => $error_name::InvalidValue(s.as_str()),
417
                    }
418
2
                }
419
            }
420

            
421
            /// # Errors
422
            ///
423
            /// Returns an error if `input` is not a valid CSS value for this property.
424
37
            pub fn $fn_name(input: &str) -> Result<$struct_name, $error_name<'_>> {
425
37
                match input.trim() {
426
32
                    $( $val => Ok($variant), )+
427
27
                    _ => Err($error_name::InvalidValue(input)),
428
                }
429
37
            }
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
3
        #[must_use] pub fn to_contained(&self) -> ColumnRuleWidthParseErrorOwned {
469
3
            match self {
470
3
                ColumnRuleWidthParseError::Pixel(e) => {
471
3
                    ColumnRuleWidthParseErrorOwned::Pixel(e.to_contained())
472
                }
473
            }
474
3
        }
475
    }
476
    impl ColumnRuleWidthParseErrorOwned {
477
3
        #[must_use] pub fn to_shared(&self) -> ColumnRuleWidthParseError<'_> {
478
3
            match self {
479
3
                Self::Pixel(e) => {
480
3
                    ColumnRuleWidthParseError::Pixel(e.to_shared())
481
                }
482
            }
483
3
        }
484
    }
485
    /// # Errors
486
    ///
487
    /// Returns an error if `input` is not a valid CSS `column-rule-width` value.
488
28
    pub fn parse_column_rule_width(
489
28
        input: &str,
490
28
    ) -> Result<ColumnRuleWidth, ColumnRuleWidthParseError<'_>> {
491
        Ok(ColumnRuleWidth {
492
28
            inner: parse_pixel_value(input)?,
493
        })
494
28
    }
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
2
        #[must_use] pub fn to_contained(&self) -> ColumnRuleStyleParseErrorOwned {
510
2
            match self {
511
2
                ColumnRuleStyleParseError::Style(e) => {
512
2
                    ColumnRuleStyleParseErrorOwned::Style(e.to_contained())
513
                }
514
            }
515
2
        }
516
    }
517
    impl ColumnRuleStyleParseErrorOwned {
518
2
        #[must_use] pub fn to_shared(&self) -> ColumnRuleStyleParseError<'_> {
519
2
            match self {
520
2
                Self::Style(e) => {
521
2
                    ColumnRuleStyleParseError::Style(e.to_shared())
522
                }
523
            }
524
2
        }
525
    }
526
    /// # Errors
527
    ///
528
    /// Returns an error if `input` is not a valid CSS `column-rule-style` value.
529
26
    pub fn parse_column_rule_style(
530
26
        input: &str,
531
26
    ) -> Result<ColumnRuleStyle, ColumnRuleStyleParseError<'_>> {
532
        Ok(ColumnRuleStyle {
533
26
            inner: parse_border_style(input)?,
534
        })
535
26
    }
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
4
        #[must_use] pub fn to_contained(&self) -> ColumnRuleColorParseErrorOwned {
551
4
            match self {
552
4
                ColumnRuleColorParseError::Color(e) => {
553
4
                    ColumnRuleColorParseErrorOwned::Color(e.to_contained())
554
                }
555
            }
556
4
        }
557
    }
558
    impl ColumnRuleColorParseErrorOwned {
559
4
        #[must_use] pub fn to_shared(&self) -> ColumnRuleColorParseError<'_> {
560
4
            match self {
561
4
                Self::Color(e) => {
562
4
                    ColumnRuleColorParseError::Color(e.to_shared())
563
                }
564
            }
565
4
        }
566
    }
567
    /// # Errors
568
    ///
569
    /// Returns an error if `input` is not a valid CSS `column-rule-color` value.
570
41
    pub fn parse_column_rule_color(
571
41
        input: &str,
572
41
    ) -> Result<ColumnRuleColor, ColumnRuleColorParseError<'_>> {
573
        Ok(ColumnRuleColor {
574
41
            inner: parse_css_color(input)?,
575
        })
576
41
    }
577
}
578

            
579
#[cfg(feature = "parser")]
580
pub use parser::*;
581

            
582
#[cfg(all(test, feature = "parser"))]
583
mod tests {
584
    use super::*;
585

            
586
    #[test]
587
1
    fn test_parse_column_count() {
588
1
        assert_eq!(parse_column_count("auto").unwrap(), ColumnCount::Auto);
589
1
        assert_eq!(parse_column_count("3").unwrap(), ColumnCount::Integer(3));
590
1
        assert!(parse_column_count("none").is_err());
591
1
        assert!(parse_column_count("2.5").is_err());
592
1
    }
593

            
594
    #[test]
595
1
    fn test_parse_column_width() {
596
1
        assert_eq!(parse_column_width("auto").unwrap(), ColumnWidth::Auto);
597
1
        assert_eq!(
598
1
            parse_column_width("200px").unwrap(),
599
1
            ColumnWidth::Length(PixelValue::px(200.0))
600
        );
601
1
        assert_eq!(
602
1
            parse_column_width("15em").unwrap(),
603
1
            ColumnWidth::Length(PixelValue::em(15.0))
604
        );
605
1
        assert!(parse_column_width("50%").is_ok()); // Percentage is valid for column-width
606
1
    }
607

            
608
    #[test]
609
1
    fn test_parse_column_span() {
610
1
        assert_eq!(parse_column_span("none").unwrap(), ColumnSpan::None);
611
1
        assert_eq!(parse_column_span("all").unwrap(), ColumnSpan::All);
612
1
        assert!(parse_column_span("2").is_err());
613
1
    }
614

            
615
    #[test]
616
1
    fn test_parse_column_fill() {
617
1
        assert_eq!(parse_column_fill("auto").unwrap(), ColumnFill::Auto);
618
1
        assert_eq!(parse_column_fill("balance").unwrap(), ColumnFill::Balance);
619
1
        assert!(parse_column_fill("none").is_err());
620
1
    }
621

            
622
    #[test]
623
1
    fn test_parse_column_rule() {
624
1
        assert_eq!(
625
1
            parse_column_rule_width("5px").unwrap().inner,
626
1
            PixelValue::px(5.0)
627
        );
628
1
        assert_eq!(
629
1
            parse_column_rule_style("dotted").unwrap().inner,
630
            BorderStyle::Dotted
631
        );
632
1
        assert_eq!(parse_column_rule_color("blue").unwrap().inner, ColorU::BLUE);
633
1
    }
634
}
635

            
636
#[cfg(all(test, feature = "parser"))]
637
#[allow(clippy::float_cmp)] // parsed values are compared against the exact source literals
638
mod 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!(parse_column_count(" \n 12 \t").unwrap(), ColumnCount::Integer(12));
656
    }
657

            
658
    #[test]
659
    fn column_count_rejects_empty_and_whitespace_only() {
660
        assert!(parse_column_count("").is_err());
661
        assert!(parse_column_count("   ").is_err());
662
        assert!(parse_column_count("\t\n\r ").is_err());
663
    }
664

            
665
    #[test]
666
    fn column_count_rejects_garbage_without_panicking() {
667
        for bad in [
668
            "none", "auto auto", "3px", "3;garbage", "3 4", "2.5", "0x10", "1_000", "--3", "+-3",
669
            "\0", "3\0", "١٢٣", "٣", "3", "NaN", "inf", "-inf", "e5", "1e3",
670
        ] {
671
            assert!(
672
                parse_column_count(bad).is_err(),
673
                "column-count accepted garbage: {bad:?}"
674
            );
675
        }
676
    }
677

            
678
    #[test]
679
    fn column_count_case_sensitivity_is_exact() {
680
        // NOTE: CSS keywords are ASCII-case-insensitive; this parser is not.
681
        // Documented here as the *current* contract - it must at least not panic.
682
        assert!(parse_column_count("AUTO").is_err());
683
        assert!(parse_column_count("Auto").is_err());
684
    }
685

            
686
    #[test]
687
    fn column_count_u32_boundaries_saturate_into_err_not_wrap() {
688
        // Lower bound: 0 is accepted even though CSS requires a positive integer.
689
        assert_eq!(parse_column_count("0").unwrap(), ColumnCount::Integer(0));
690
        // `u32::from_str` accepts a leading '+'.
691
        assert_eq!(parse_column_count("+7").unwrap(), ColumnCount::Integer(7));
692
        // Exact u32 ceiling parses; one above must be a clean Err, never a wrap to 0.
693
        assert_eq!(
694
            parse_column_count("4294967295").unwrap(),
695
            ColumnCount::Integer(u32::MAX)
696
        );
697
        assert!(parse_column_count("4294967296").is_err());
698
        // i64::MAX / u64::MAX / a 40-digit number all overflow u32 -> Err, no wraparound.
699
        assert!(parse_column_count("9223372036854775807").is_err());
700
        assert!(parse_column_count("18446744073709551615").is_err());
701
        assert!(parse_column_count("9999999999999999999999999999999999999999").is_err());
702
        // Negatives (including -0) are not representable in u32.
703
        assert!(parse_column_count("-1").is_err());
704
        assert!(parse_column_count("-0").is_err());
705
    }
706

            
707
    #[test]
708
    fn column_count_extremely_long_input_terminates() {
709
        let huge = "9".repeat(LONG);
710
        assert!(parse_column_count(&huge).is_err());
711
        let huge_keyword = "auto".repeat(LONG / 4);
712
        assert!(parse_column_count(&huge_keyword).is_err());
713
        // Whitespace-padded huge input: trimming must not be quadratic or panic.
714
        let padded = format!("{}{}{}", " ".repeat(10_000), "7", " ".repeat(10_000));
715
        assert_eq!(parse_column_count(&padded).unwrap(), ColumnCount::Integer(7));
716
    }
717

            
718
    #[test]
719
    fn column_count_deeply_nested_input_does_not_stack_overflow() {
720
        let nested = "(".repeat(10_000);
721
        assert!(parse_column_count(&nested).is_err());
722
        let balanced = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
723
        assert!(parse_column_count(&balanced).is_err());
724
    }
725

            
726
    #[test]
727
    fn column_count_unicode_input_does_not_panic() {
728
        for bad in [
729
            "\u{1F600}",
730
            "auto\u{1F600}",
731
            "e\u{301}",       // combining acute accent
732
            "\u{202E}3",      // RTL override
733
            "\u{FEFF}auto",   // BOM (not whitespace -> not trimmed)
734
            "au\u{0000}to",
735
        ] {
736
            assert!(
737
                parse_column_count(bad).is_err(),
738
                "column-count accepted unicode junk: {bad:?}"
739
            );
740
        }
741
    }
742

            
743
    #[test]
744
    fn column_count_roundtrips_through_print_as_css_value() {
745
        for value in [
746
            ColumnCount::Auto,
747
            ColumnCount::Integer(0),
748
            ColumnCount::Integer(1),
749
            ColumnCount::Integer(u32::MAX),
750
        ] {
751
            let printed = value.print_as_css_value();
752
            assert_eq!(
753
                parse_column_count(&printed).unwrap(),
754
                value,
755
                "round-trip failed for {value:?} (printed as {printed:?})"
756
            );
757
        }
758
    }
759

            
760
    // -----------------------------------------------------------------
761
    // ColumnCountParseError::to_contained / ColumnCountParseErrorOwned::to_shared
762
    // -----------------------------------------------------------------
763

            
764
    #[test]
765
    fn column_count_error_invalid_value_roundtrips_losslessly() {
766
        for s in ["", "x", "  padded  ", "\u{1F600}"] {
767
            let shared = ColumnCountParseError::InvalidValue(s);
768
            let owned = shared.to_contained();
769
            assert_eq!(owned, ColumnCountParseErrorOwned::InvalidValue(AzString::from(s)));
770
            assert_eq!(owned.to_shared(), shared);
771
        }
772
    }
773

            
774
    #[test]
775
    fn column_count_error_parse_int_roundtrip_is_lossy_but_safe() {
776
        let err = parse_column_count("abc").unwrap_err();
777
        assert_eq!(
778
            err,
779
            ColumnCountParseError::ParseInt("abc".parse::<u32>().unwrap_err())
780
        );
781

            
782
        let owned = err.to_contained();
783
        match &owned {
784
            ColumnCountParseErrorOwned::ParseInt(msg) => {
785
                assert!(!msg.as_str().is_empty(), "ParseInt message was dropped");
786
            }
787
            other => panic!("expected ParseInt, got {other:?}"),
788
        }
789

            
790
        // Documented lossy path: a ParseIntError cannot be rebuilt from its Display
791
        // string, so to_shared() degrades to a generic InvalidValue instead of panicking.
792
        assert_eq!(
793
            owned.to_shared(),
794
            ColumnCountParseError::InvalidValue("invalid integer")
795
        );
796
    }
797

            
798
    #[test]
799
    fn column_count_error_debug_equals_display_and_keeps_input() {
800
        let err = ColumnCountParseError::InvalidValue("weird\u{1F600}input");
801
        assert_eq!(format!("{err:?}"), format!("{err}"));
802
        assert!(format!("{err}").contains("weird\u{1F600}input"));
803

            
804
        // Overflow errors must render without panicking on an empty/huge instance.
805
        let overflow = parse_column_count("4294967296").unwrap_err();
806
        assert!(!format!("{overflow}").is_empty());
807
        assert!(!format!("{:?}", overflow.to_contained()).is_empty());
808
    }
809

            
810
    // -----------------------------------------------------------------
811
    // parse_column_width
812
    // -----------------------------------------------------------------
813

            
814
    #[test]
815
    fn column_width_valid_minimal_and_trimming() {
816
        assert_eq!(parse_column_width("auto").unwrap(), ColumnWidth::Auto);
817
        assert_eq!(parse_column_width("  auto  ").unwrap(), ColumnWidth::Auto);
818
        assert_eq!(
819
            parse_column_width("\t1px\n").unwrap(),
820
            ColumnWidth::Length(PixelValue::px(1.0))
821
        );
822
    }
823

            
824
    #[test]
825
    fn column_width_rejects_empty_whitespace_and_garbage() {
826
        assert!(parse_column_width("").is_err());
827
        assert!(parse_column_width("   ").is_err());
828
        assert!(parse_column_width("\t\n").is_err());
829
        for bad in [
830
            "px", "em", "%", "ten-px", "200px;garbage", "200 px extra", "#200px", "\u{1F600}",
831
            "20\u{301}px", "AUTO",
832
        ] {
833
            assert!(
834
                parse_column_width(bad).is_err(),
835
                "column-width accepted garbage: {bad:?}"
836
            );
837
        }
838
    }
839

            
840
    #[test]
841
    fn column_width_non_finite_numbers_are_stored_finite() {
842
        // f32::from_str accepts "NaN"/"inf", so these reach FloatValue::new().
843
        // The isize-backed FloatValue must clamp them - a NaN/inf leaking into
844
        // layout would poison every downstream computation.
845
        let nan = parse_column_width("NaN").unwrap();
846
        assert_eq!(nan, ColumnWidth::Length(PixelValue::px(0.0)));
847

            
848
        for input in ["inf", "Infinity", "-inf", "-Infinity", "1e40px", "-1e40px"] {
849
            let parsed = parse_column_width(input).unwrap();
850
            match parsed {
851
                ColumnWidth::Length(px) => assert!(
852
                    px.number.get().is_finite(),
853
                    "{input:?} produced a non-finite PixelValue"
854
                ),
855
                ColumnWidth::Auto => panic!("{input:?} unexpectedly parsed as auto"),
856
            }
857
        }
858
    }
859

            
860
    #[test]
861
    fn column_width_zero_and_subnormal_boundaries() {
862
        assert_eq!(
863
            parse_column_width("0").unwrap(),
864
            ColumnWidth::Length(PixelValue::px(0.0))
865
        );
866
        // -0.0 must normalize to the same stored value as +0.0 (FloatValue is an isize).
867
        assert_eq!(
868
            parse_column_width("-0").unwrap(),
869
            parse_column_width("0").unwrap()
870
        );
871
        assert_eq!(
872
            parse_column_width("-0px").unwrap(),
873
            ColumnWidth::Length(PixelValue::px(0.0))
874
        );
875
        // Subnormal f32: 1e-45 * 1000 truncates to 0 rather than panicking.
876
        assert_eq!(
877
            parse_column_width("1e-45px").unwrap(),
878
            ColumnWidth::Length(PixelValue::px(0.0))
879
        );
880
        // Negative lengths are (currently) accepted by the parser; must not wrap sign.
881
        match parse_column_width("-10px").unwrap() {
882
            ColumnWidth::Length(px) => assert!(px.number.get() < 0.0),
883
            ColumnWidth::Auto => panic!("-10px parsed as auto"),
884
        }
885
    }
886

            
887
    #[test]
888
    fn column_width_extremely_long_input_terminates() {
889
        let huge_digits = format!("{}px", "9".repeat(LONG));
890
        let parsed = parse_column_width(&huge_digits).unwrap();
891
        match parsed {
892
            ColumnWidth::Length(px) => assert!(px.number.get().is_finite()),
893
            ColumnWidth::Auto => panic!("digit soup parsed as auto"),
894
        }
895
        assert!(parse_column_width(&"a".repeat(LONG)).is_err());
896
        assert!(parse_column_width(&"auto".repeat(LONG / 4)).is_err());
897
    }
898

            
899
    #[test]
900
    fn column_width_deeply_nested_input_does_not_stack_overflow() {
901
        assert!(parse_column_width(&"(".repeat(10_000)).is_err());
902
        let balanced = format!("calc{}{}", "(".repeat(10_000), ")".repeat(10_000));
903
        assert!(parse_column_width(&balanced).is_err());
904
    }
905

            
906
    #[test]
907
    fn column_width_roundtrips_through_print_as_css_value() {
908
        // Only exactly-representable numbers: FloatValue truncates to 1/1000ths,
909
        // so e.g. 2.54cm is *not* expected to survive a print/parse cycle.
910
        let values = [
911
            ColumnWidth::Auto,
912
            ColumnWidth::Length(PixelValue::px(0.0)),
913
            ColumnWidth::Length(PixelValue::px(200.0)),
914
            ColumnWidth::Length(PixelValue::px(-12.5)),
915
            ColumnWidth::Length(PixelValue::em(1.5)),
916
            ColumnWidth::Length(PixelValue::rem(2.0)),
917
            ColumnWidth::Length(PixelValue::pt(-20.0)),
918
            ColumnWidth::Length(PixelValue::percent(50.0)),
919
            ColumnWidth::Length(PixelValue::inch(1.0)),
920
            ColumnWidth::Length(PixelValue::cm(3.0)),
921
            ColumnWidth::Length(PixelValue::mm(10.0)),
922
            ColumnWidth::Length(PixelValue::from_metric(SizeMetric::Vw, 10.0)),
923
            ColumnWidth::Length(PixelValue::from_metric(SizeMetric::Vh, 10.0)),
924
            ColumnWidth::Length(PixelValue::from_metric(SizeMetric::Vmax, 10.0)),
925
        ];
926
        for value in values {
927
            let printed = value.print_as_css_value();
928
            assert_eq!(
929
                parse_column_width(&printed).unwrap(),
930
                value,
931
                "round-trip failed for {value:?} (printed as {printed:?})"
932
            );
933
        }
934
    }
935

            
936
    // -----------------------------------------------------------------
937
    // ColumnWidthParseError::to_contained / ColumnWidthParseErrorOwned::to_shared
938
    // -----------------------------------------------------------------
939

            
940
    #[test]
941
    fn column_width_error_roundtrips_through_owned() {
942
        // InvalidValue is only reachable by hand - the parser always delegates to
943
        // the pixel parser - but the conversion still has to be lossless.
944
        for s in ["", "bogus", "\u{1F600}"] {
945
            let shared = ColumnWidthParseError::InvalidValue(s);
946
            let owned = shared.to_contained();
947
            assert_eq!(owned, ColumnWidthParseErrorOwned::InvalidValue(AzString::from(s)));
948
            assert_eq!(owned.to_shared(), shared);
949
        }
950

            
951
        // The variants the parser actually produces.
952
        for bad in ["", "ten-px", "px", "%"] {
953
            let err = parse_column_width(bad).unwrap_err();
954
            assert!(
955
                matches!(err, ColumnWidthParseError::PixelValue(_)),
956
                "{bad:?} produced an unexpected error variant: {err:?}"
957
            );
958
            let owned = err.to_contained();
959
            assert_eq!(owned.to_shared(), err, "lossy round-trip for {bad:?}");
960
            assert_eq!(format!("{err:?}"), format!("{err}"));
961
        }
962
    }
963

            
964
    // -----------------------------------------------------------------
965
    // parse_column_span / parse_column_fill
966
    // -----------------------------------------------------------------
967

            
968
    #[test]
969
    fn column_span_and_fill_accept_only_their_keywords() {
970
        assert_eq!(parse_column_span("none").unwrap(), ColumnSpan::None);
971
        assert_eq!(parse_column_span(" all \t").unwrap(), ColumnSpan::All);
972
        assert_eq!(parse_column_fill("auto").unwrap(), ColumnFill::Auto);
973
        assert_eq!(parse_column_fill("\n balance ").unwrap(), ColumnFill::Balance);
974

            
975
        for bad in ["", "   ", "2", "ALL", "None", "all all", "all;", "\u{1F600}", "nonee", "\0"] {
976
            assert!(parse_column_span(bad).is_err(), "column-span accepted {bad:?}");
977
        }
978
        for bad in ["", "   ", "none", "AUTO", "balanced", "auto balance", "\u{1F600}"] {
979
            assert!(parse_column_fill(bad).is_err(), "column-fill accepted {bad:?}");
980
        }
981
        // The two properties must not accept each other's keywords.
982
        assert!(parse_column_span("balance").is_err());
983
        assert!(parse_column_fill("all").is_err());
984
    }
985

            
986
    #[test]
987
    fn column_span_and_fill_survive_long_and_nested_input() {
988
        let huge = "all".repeat(LONG / 3);
989
        assert!(parse_column_span(&huge).is_err());
990
        assert!(parse_column_fill(&huge).is_err());
991
        let nested = "(".repeat(10_000);
992
        assert!(parse_column_span(&nested).is_err());
993
        assert!(parse_column_fill(&nested).is_err());
994
    }
995

            
996
    #[test]
997
    fn column_span_error_reports_the_untrimmed_input() {
998
        // The macro-generated parser matches on the trimmed input but reports the
999
        // *original* one - that asymmetry is load-bearing for error messages.
        let err = parse_column_span("  bogus  ").unwrap_err();
        match err {
            ColumnSpanParseError::InvalidValue(s) => assert_eq!(s, "  bogus  "),
        }
        assert_eq!(format!("{err:?}"), format!("{err}"));
        assert!(format!("{err}").contains("column-span"));
        let owned = err.to_contained();
        assert_eq!(owned, ColumnSpanParseErrorOwned::InvalidValue(AzString::from("  bogus  ")));
        assert_eq!(owned.to_shared(), err);
        let fill_err = parse_column_fill("\u{1F600}").unwrap_err();
        let fill_owned = fill_err.to_contained();
        assert_eq!(fill_owned.to_shared(), fill_err);
        assert!(format!("{fill_err}").contains("column-fill"));
    }
    // -----------------------------------------------------------------
    // parse_column_rule_width
    // -----------------------------------------------------------------
    #[test]
    fn column_rule_width_valid_and_invalid() {
        assert_eq!(
            parse_column_rule_width("5px").unwrap().inner,
            PixelValue::px(5.0)
        );
        assert_eq!(
            parse_column_rule_width("  0  ").unwrap().inner,
            PixelValue::px(0.0)
        );
        for bad in ["", "   ", "auto", "solid", "px", "\u{1F600}", "5px;5px", "5 px extra"] {
            assert!(
                parse_column_rule_width(bad).is_err(),
                "column-rule-width accepted {bad:?}"
            );
        }
    }
    #[test]
    fn column_rule_width_extremes_stay_finite() {
        for input in ["NaN", "inf", "-inf", "1e40px", "-1e40px", "1e-45px"] {
            let w = parse_column_rule_width(input).unwrap();
            assert!(
                w.inner.number.get().is_finite(),
                "{input:?} produced a non-finite column-rule-width"
            );
        }
        assert!(parse_column_rule_width(&"9".repeat(LONG)).unwrap().inner.number.get().is_finite());
        assert!(parse_column_rule_width(&"(".repeat(10_000)).is_err());
    }
    #[test]
    fn column_rule_width_default_is_medium_and_roundtrips() {
        let default = ColumnRuleWidth::default();
        assert_eq!(default.inner, PixelValue::const_px(3));
        assert_eq!(default.print_as_css_value(), "3px");
        assert_eq!(parse_column_rule_width("3px").unwrap(), default);
        for value in [
            ColumnRuleWidth::default(),
            ColumnRuleWidth { inner: PixelValue::px(0.0) },
            ColumnRuleWidth { inner: PixelValue::em(2.5) },
            ColumnRuleWidth { inner: PixelValue::percent(-25.0) },
        ] {
            let printed = value.print_as_css_value();
            assert_eq!(parse_column_rule_width(&printed).unwrap(), value);
        }
    }
    #[test]
    fn column_rule_width_error_roundtrips_through_owned() {
        for bad in ["", "auto", "px"] {
            let err = parse_column_rule_width(bad).unwrap_err();
            let owned = err.to_contained();
            assert_eq!(owned.to_shared(), err, "lossy round-trip for {bad:?}");
            assert_eq!(format!("{err:?}"), format!("{err}"));
            assert!(!format!("{err}").is_empty());
        }
    }
    // -----------------------------------------------------------------
    // parse_column_rule_style
    // -----------------------------------------------------------------
    #[test]
    fn column_rule_style_accepts_every_border_style_and_roundtrips() {
        for style in [
            BorderStyle::None,
            BorderStyle::Solid,
            BorderStyle::Double,
            BorderStyle::Dotted,
            BorderStyle::Dashed,
            BorderStyle::Hidden,
            BorderStyle::Groove,
            BorderStyle::Ridge,
            BorderStyle::Inset,
            BorderStyle::Outset,
        ] {
            let value = ColumnRuleStyle { inner: style };
            let printed = value.print_as_css_value();
            assert_eq!(
                parse_column_rule_style(&printed).unwrap(),
                value,
                "round-trip failed for {style:?} (printed as {printed:?})"
            );
        }
    }
    #[test]
    fn column_rule_style_rejects_garbage_without_panicking() {
        for bad in [
            "", "   ", "SOLID", "solidd", "solid solid", "3px", "\u{1F600}", "\0", "soli\u{0301}d",
        ] {
            assert!(
                parse_column_rule_style(bad).is_err(),
                "column-rule-style accepted {bad:?}"
            );
        }
        assert!(parse_column_rule_style(&"solid".repeat(LONG / 5)).is_err());
        assert!(parse_column_rule_style(&"(".repeat(10_000)).is_err());
        // Leading/trailing whitespace *is* trimmed.
        assert_eq!(
            parse_column_rule_style("  dotted \n").unwrap().inner,
            BorderStyle::Dotted
        );
    }
    #[test]
    fn column_rule_style_error_roundtrips_through_owned() {
        let err = parse_column_rule_style("bogus").unwrap_err();
        let owned = err.to_contained();
        assert_eq!(owned.to_shared(), err);
        assert_eq!(format!("{err:?}"), format!("{err}"));
        assert!(format!("{err}").contains("bogus"));
        let unicode_err = parse_column_rule_style("\u{1F600}").unwrap_err();
        let unicode_owned = unicode_err.to_contained();
        assert_eq!(unicode_owned.to_shared(), unicode_err);
    }
    // -----------------------------------------------------------------
    // parse_column_rule_color
    // -----------------------------------------------------------------
    #[test]
    fn column_rule_color_accepts_names_hex_and_functions() {
        assert_eq!(parse_column_rule_color("blue").unwrap().inner, ColorU::BLUE);
        // Named colors *are* case-insensitive here (unlike column-span/fill/count).
        assert_eq!(parse_column_rule_color("  BLUE \t").unwrap().inner, ColorU::BLUE);
        assert_eq!(parse_column_rule_color("#000f").unwrap().inner, ColorU::BLACK);
        assert_eq!(
            parse_column_rule_color("#ff000080").unwrap().inner,
            ColorU::rgba(255, 0, 0, 128)
        );
        assert_eq!(parse_column_rule_color("transparent").unwrap().inner.a, 0);
        assert_eq!(
            parse_column_rule_color("rgba(255, 0, 0, 1.0)").unwrap().inner,
            ColorU::RED
        );
    }
    #[test]
    fn column_rule_color_rejects_garbage_without_panicking() {
        for bad in [
            "",
            "   ",
            "#",
            "#f",
            "#ff",
            "#fffff",
            "#zzzzzz",
            "notacolor",
            "rgb(",
            "rgb(1,2",
            "rgb()",
            "rgb(300)",
            "hsl(",
            "\u{1F600}",
            "#\u{1F600}",
            "\u{130}", // dotted capital I: to_lowercase() expands to 2 chars
        ] {
            assert!(
                parse_column_rule_color(bad).is_err(),
                "column-rule-color accepted {bad:?}"
            );
        }
    }
    #[test]
    fn column_rule_color_survives_long_and_nested_input() {
        assert!(parse_column_rule_color(&"a".repeat(100_000)).is_err());
        assert!(parse_column_rule_color(&"#".repeat(100_000)).is_err());
        // Unbalanced and deeply nested parens must not recurse or hang.
        assert!(parse_column_rule_color(&"(".repeat(10_000)).is_err());
        assert!(parse_column_rule_color(&format!("rgb{}", "(".repeat(10_000))).is_err());
        let nested = format!("rgb{}{}", "(".repeat(10_000), ")".repeat(10_000));
        assert!(parse_column_rule_color(&nested).is_err());
    }
    #[test]
    fn column_rule_color_roundtrips_through_to_hash() {
        for color in [
            ColorU::BLACK,
            ColorU::WHITE,
            ColorU::RED,
            ColorU::BLUE,
            ColorU::TRANSPARENT,
            ColorU::rgba(1, 2, 3, 4),
            ColorU::rgba(255, 255, 255, 0),
            ColorU::rgba(0, 0, 0, 255),
        ] {
            let value = ColumnRuleColor { inner: color };
            let printed = value.print_as_css_value(); // "#rrggbbaa"
            assert_eq!(printed.len(), 9, "unexpected hash form: {printed:?}");
            assert_eq!(
                parse_column_rule_color(&printed).unwrap(),
                value,
                "round-trip failed for {color:?} (printed as {printed:?})"
            );
        }
    }
    #[test]
    fn column_rule_color_error_roundtrips_through_owned() {
        for bad in ["", "notacolor", "rgb(", "#zzzzzz"] {
            let err = parse_column_rule_color(bad).unwrap_err();
            let owned = err.to_contained();
            assert_eq!(owned.to_shared(), err, "lossy round-trip for {bad:?}");
            assert_eq!(format!("{err:?}"), format!("{err}"));
            assert!(!format!("{err}").is_empty());
        }
    }
    // -----------------------------------------------------------------
    // Type invariants: defaults, ordering, hashing, codegen
    // -----------------------------------------------------------------
    #[test]
    fn defaults_match_the_documented_css_initial_values() {
        assert_eq!(ColumnCount::default(), ColumnCount::Auto);
        assert_eq!(ColumnWidth::default(), ColumnWidth::Auto);
        assert_eq!(ColumnSpan::default(), ColumnSpan::None);
        assert_eq!(ColumnFill::default(), ColumnFill::Balance);
        assert_eq!(ColumnRuleStyle::default().inner, BorderStyle::None);
        // NOTE: per CSS this should be `currentcolor`; the type doc records the deviation.
        assert_eq!(ColumnRuleColor::default().inner, ColorU::BLACK);
        // Every default must print to something its own parser accepts.
        assert_eq!(
            parse_column_count(&ColumnCount::default().print_as_css_value()).unwrap(),
            ColumnCount::default()
        );
        assert_eq!(
            parse_column_width(&ColumnWidth::default().print_as_css_value()).unwrap(),
            ColumnWidth::default()
        );
        assert_eq!(
            parse_column_span(&ColumnSpan::default().print_as_css_value()).unwrap(),
            ColumnSpan::default()
        );
        assert_eq!(
            parse_column_fill(&ColumnFill::default().print_as_css_value()).unwrap(),
            ColumnFill::default()
        );
        assert_eq!(
            parse_column_rule_width(&ColumnRuleWidth::default().print_as_css_value()).unwrap(),
            ColumnRuleWidth::default()
        );
        assert_eq!(
            parse_column_rule_style(&ColumnRuleStyle::default().print_as_css_value()).unwrap(),
            ColumnRuleStyle::default()
        );
        assert_eq!(
            parse_column_rule_color(&ColumnRuleColor::default().print_as_css_value()).unwrap(),
            ColumnRuleColor::default()
        );
    }
    #[test]
    fn ord_and_hash_agree_with_eq() {
        use std::{
            collections::hash_map::DefaultHasher,
            hash::{Hash, Hasher},
        };
        fn hash_of<T: Hash>(t: &T) -> u64 {
            let mut h = DefaultHasher::new();
            t.hash(&mut h);
            h.finish()
        }
        // Keyword variants sort before their value-carrying counterparts.
        assert!(ColumnCount::Auto < ColumnCount::Integer(0));
        assert!(ColumnCount::Integer(1) < ColumnCount::Integer(u32::MAX));
        assert!(ColumnWidth::Auto < ColumnWidth::Length(PixelValue::px(0.0)));
        assert!(ColumnSpan::None < ColumnSpan::All);
        assert!(ColumnFill::Auto < ColumnFill::Balance);
        // Eq implies equal hashes (these types are used as prop-cache keys).
        assert_eq!(
            hash_of(&ColumnCount::Integer(7)),
            hash_of(&parse_column_count("7").unwrap())
        );
        assert_eq!(
            hash_of(&ColumnWidth::Length(PixelValue::px(0.0))),
            hash_of(&parse_column_width("-0px").unwrap())
        );
        assert_ne!(hash_of(&ColumnFill::Auto), hash_of(&ColumnFill::Balance));
    }
    #[test]
    fn format_as_rust_code_emits_constructible_snippets() {
        assert_eq!(ColumnCount::Auto.format_as_rust_code(0), "ColumnCount::Auto");
        assert_eq!(
            ColumnCount::Integer(u32::MAX).format_as_rust_code(0),
            "ColumnCount::Integer(4294967295)"
        );
        assert_eq!(ColumnWidth::Auto.format_as_rust_code(0), "ColumnWidth::Auto");
        assert_eq!(ColumnSpan::All.format_as_rust_code(0), "ColumnSpan::All");
        assert_eq!(ColumnSpan::None.format_as_rust_code(0), "ColumnSpan::None");
        assert_eq!(ColumnFill::Auto.format_as_rust_code(0), "ColumnFill::Auto");
        assert_eq!(ColumnFill::Balance.format_as_rust_code(0), "ColumnFill::Balance");
        // Extreme instances must format without panicking.
        let wide = ColumnWidth::Length(PixelValue::px(f32::MAX));
        assert!(wide.format_as_rust_code(0).starts_with("ColumnWidth::Length("));
        let rule = ColumnRuleWidth { inner: PixelValue::px(-0.5) };
        assert!(rule.format_as_rust_code(0).starts_with("ColumnRuleWidth {"));
        let style = ColumnRuleStyle { inner: BorderStyle::Dotted };
        assert!(style.format_as_rust_code(0).contains("Dotted"));
        let color = ColumnRuleColor { inner: ColorU::TRANSPARENT };
        assert!(color.format_as_rust_code(0).starts_with("ColumnRuleColor {"));
    }
}