Skip to main content

azul_css/props/layout/
table.rs

1//! CSS properties for table layout and styling.
2//!
3//! This module contains properties specific to CSS table formatting:
4//! - `table-layout`: Controls the algorithm used to layout table cells, rows, and columns
5//! - `border-collapse`: Specifies whether cell borders are collapsed into a single border or
6//!   separated
7//! - `border-spacing`: Sets the distance between borders of adjacent cells (separate borders only)
8//! - `caption-side`: Specifies the placement of a table caption
9//! - `empty-cells`: Specifies whether or not to display borders on empty cells in a table
10
11use alloc::string::{String, ToString};
12
13use crate::{
14    codegen::format::FormatAsRustCode,
15    props::{
16        basic::pixel::{CssPixelValueParseError, PixelValue},
17        formatter::PrintAsCssValue,
18    },
19};
20
21// table-layout
22
23/// Controls the algorithm used to lay out table cells, rows, and columns.
24///
25/// The `table-layout` property determines whether the browser should use:
26/// - **auto**: Column widths are determined by the content (slower but flexible)
27/// - **fixed**: Column widths are determined by the first row (faster and predictable)
28#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
29#[repr(C)]
30#[derive(Default)]
31pub enum LayoutTableLayout {
32    /// Use automatic table layout algorithm (content-based, default).
33    /// Column width is set by the widest unbreakable content in the cells.
34    #[default]
35    Auto,
36    /// Use fixed table layout algorithm (first-row-based).
37    /// Column width is set by the width property of the column or first-row cell.
38    /// Renders faster than auto.
39    Fixed,
40}
41
42impl PrintAsCssValue for LayoutTableLayout {
43    fn print_as_css_value(&self) -> String {
44        match self {
45            Self::Auto => "auto".to_string(),
46            Self::Fixed => "fixed".to_string(),
47        }
48    }
49}
50
51impl FormatAsRustCode for LayoutTableLayout {
52    fn format_as_rust_code(&self, _tabs: usize) -> String {
53        match self {
54            Self::Auto => "LayoutTableLayout::Auto".to_string(),
55            Self::Fixed => "LayoutTableLayout::Fixed".to_string(),
56        }
57    }
58}
59
60// border-collapse
61
62/// Specifies whether cell borders are collapsed into a single border or separated.
63///
64/// The `border-collapse` property determines the border rendering model:
65/// - **separate**: Each cell has its own border (default, uses border-spacing)
66/// - **collapse**: Adjacent cells share borders (ignores border-spacing)
67#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
68#[repr(C)]
69#[derive(Default)]
70pub enum StyleBorderCollapse {
71    /// Borders are separated (default). Each cell has its own border.
72    /// The `border-spacing` property defines the distance between borders.
73    #[default]
74    Separate,
75    /// Borders are collapsed. Adjacent cells share a single border.
76    /// Border conflict resolution rules apply when borders differ.
77    Collapse,
78}
79
80impl PrintAsCssValue for StyleBorderCollapse {
81    fn print_as_css_value(&self) -> String {
82        match self {
83            Self::Separate => "separate".to_string(),
84            Self::Collapse => "collapse".to_string(),
85        }
86    }
87}
88
89impl FormatAsRustCode for StyleBorderCollapse {
90    fn format_as_rust_code(&self, _tabs: usize) -> String {
91        match self {
92            Self::Separate => "StyleBorderCollapse::Separate".to_string(),
93            Self::Collapse => "StyleBorderCollapse::Collapse".to_string(),
94        }
95    }
96}
97
98// border-spacing
99
100/// Sets the distance between the borders of adjacent cells.
101///
102/// The `border-spacing` property is only applicable when `border-collapse` is set to `separate`.
103/// It can have one or two values:
104/// - One value: Sets both horizontal and vertical spacing
105/// - Two values: First is horizontal, second is vertical
106///
107/// This struct represents a single spacing value (either horizontal or vertical).
108#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
109#[repr(C)]
110pub struct LayoutBorderSpacing {
111    /// Horizontal spacing between cell borders
112    pub horizontal: PixelValue,
113    /// Vertical spacing between cell borders
114    pub vertical: PixelValue,
115}
116
117impl Default for LayoutBorderSpacing {
118    fn default() -> Self {
119        // Default border-spacing is 0 (no spacing)
120        Self {
121            horizontal: PixelValue::const_px(0),
122            vertical: PixelValue::const_px(0),
123        }
124    }
125}
126
127impl LayoutBorderSpacing {
128    /// Creates a new border spacing with the same value for horizontal and vertical
129    #[must_use]
130    pub const fn new(spacing: PixelValue) -> Self {
131        Self {
132            horizontal: spacing,
133            vertical: spacing,
134        }
135    }
136
137    /// Creates a new border spacing with different horizontal and vertical values
138    #[must_use]
139    pub const fn new_separate(horizontal: PixelValue, vertical: PixelValue) -> Self {
140        Self {
141            horizontal,
142            vertical,
143        }
144    }
145}
146
147impl PrintAsCssValue for LayoutBorderSpacing {
148    fn print_as_css_value(&self) -> String {
149        if self.horizontal == self.vertical {
150            // Single value: same for both dimensions
151            self.horizontal.to_string()
152        } else {
153            // Two values: horizontal vertical
154            format!("{} {}", self.horizontal, self.vertical)
155        }
156    }
157}
158
159impl FormatAsRustCode for LayoutBorderSpacing {
160    fn format_as_rust_code(&self, _tabs: usize) -> String {
161        use crate::codegen::format::format_pixel_value;
162        format!(
163            "LayoutBorderSpacing {{ horizontal: {}, vertical: {} }}",
164            format_pixel_value(&self.horizontal),
165            format_pixel_value(&self.vertical)
166        )
167    }
168}
169
170// caption-side
171
172/// Specifies the placement of a table caption.
173///
174/// The `caption-side` property positions the caption either above or below the table.
175#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
176#[repr(C)]
177#[derive(Default)]
178pub enum StyleCaptionSide {
179    /// Caption is placed above the table (default)
180    #[default]
181    Top,
182    /// Caption is placed below the table
183    Bottom,
184}
185
186impl PrintAsCssValue for StyleCaptionSide {
187    fn print_as_css_value(&self) -> String {
188        match self {
189            Self::Top => "top".to_string(),
190            Self::Bottom => "bottom".to_string(),
191        }
192    }
193}
194
195impl FormatAsRustCode for StyleCaptionSide {
196    fn format_as_rust_code(&self, _tabs: usize) -> String {
197        match self {
198            Self::Top => "StyleCaptionSide::Top".to_string(),
199            Self::Bottom => "StyleCaptionSide::Bottom".to_string(),
200        }
201    }
202}
203
204// empty-cells
205
206/// Specifies whether or not to display borders and background on empty cells.
207///
208/// The `empty-cells` property only applies when `border-collapse` is set to `separate`.
209/// A cell is considered empty if it contains no visible content.
210#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
211#[repr(C)]
212#[derive(Default)]
213pub enum StyleEmptyCells {
214    /// Show borders and background on empty cells (default)
215    #[default]
216    Show,
217    /// Hide borders and background on empty cells
218    Hide,
219}
220
221impl PrintAsCssValue for StyleEmptyCells {
222    fn print_as_css_value(&self) -> String {
223        match self {
224            Self::Show => "show".to_string(),
225            Self::Hide => "hide".to_string(),
226        }
227    }
228}
229
230impl FormatAsRustCode for StyleEmptyCells {
231    fn format_as_rust_code(&self, _tabs: usize) -> String {
232        match self {
233            Self::Show => "StyleEmptyCells::Show".to_string(),
234            Self::Hide => "StyleEmptyCells::Hide".to_string(),
235        }
236    }
237}
238
239// Parsing Functions
240
241/// Parse errors for table-layout property
242#[derive(Debug, Clone, PartialEq)]
243pub(crate) enum LayoutTableLayoutParseError<'a> {
244    InvalidKeyword(&'a str),
245}
246
247/// Parse a table-layout value from a string
248pub(crate) fn parse_table_layout(
249    input: &str,
250) -> Result<LayoutTableLayout, LayoutTableLayoutParseError<'_>> {
251    match input.trim() {
252        "auto" => Ok(LayoutTableLayout::Auto),
253        "fixed" => Ok(LayoutTableLayout::Fixed),
254        other => Err(LayoutTableLayoutParseError::InvalidKeyword(other)),
255    }
256}
257
258/// Parse errors for border-collapse property
259#[derive(Debug, Clone, PartialEq)]
260pub(crate) enum StyleBorderCollapseParseError<'a> {
261    InvalidKeyword(&'a str),
262}
263
264/// Parse a border-collapse value from a string
265pub(crate) fn parse_border_collapse(
266    input: &str,
267) -> Result<StyleBorderCollapse, StyleBorderCollapseParseError<'_>> {
268    match input.trim() {
269        "separate" => Ok(StyleBorderCollapse::Separate),
270        "collapse" => Ok(StyleBorderCollapse::Collapse),
271        other => Err(StyleBorderCollapseParseError::InvalidKeyword(other)),
272    }
273}
274
275/// Parse errors for border-spacing property
276#[derive(Debug, Clone, PartialEq)]
277pub(crate) enum LayoutBorderSpacingParseError<'a> {
278    PixelValue(CssPixelValueParseError<'a>),
279    InvalidFormat,
280}
281
282/// Parse a border-spacing value from a string
283/// Accepts: "5px" or "5px 10px"
284pub(crate) fn parse_border_spacing(
285    input: &str,
286) -> Result<LayoutBorderSpacing, LayoutBorderSpacingParseError<'_>> {
287    use crate::props::basic::parse_pixel_value;
288
289    let parts: Vec<&str> = input.split_whitespace().collect();
290
291    match parts.len() {
292        1 => {
293            // Single value: use for both horizontal and vertical
294            let value =
295                parse_pixel_value(parts[0]).map_err(LayoutBorderSpacingParseError::PixelValue)?;
296            Ok(LayoutBorderSpacing::new(value))
297        }
298        2 => {
299            // Two values: horizontal vertical
300            let horizontal =
301                parse_pixel_value(parts[0]).map_err(LayoutBorderSpacingParseError::PixelValue)?;
302            let vertical =
303                parse_pixel_value(parts[1]).map_err(LayoutBorderSpacingParseError::PixelValue)?;
304            Ok(LayoutBorderSpacing::new_separate(horizontal, vertical))
305        }
306        _ => Err(LayoutBorderSpacingParseError::InvalidFormat),
307    }
308}
309
310/// Parse errors for caption-side property
311#[derive(Debug, Clone, PartialEq)]
312pub(crate) enum StyleCaptionSideParseError<'a> {
313    InvalidKeyword(&'a str),
314}
315
316/// Parse a caption-side value from a string
317pub(crate) fn parse_caption_side(
318    input: &str,
319) -> Result<StyleCaptionSide, StyleCaptionSideParseError<'_>> {
320    match input.trim() {
321        "top" => Ok(StyleCaptionSide::Top),
322        "bottom" => Ok(StyleCaptionSide::Bottom),
323        other => Err(StyleCaptionSideParseError::InvalidKeyword(other)),
324    }
325}
326
327/// Parse errors for empty-cells property
328#[derive(Debug, Clone, PartialEq)]
329pub(crate) enum StyleEmptyCellsParseError<'a> {
330    InvalidKeyword(&'a str),
331}
332
333/// Parse an empty-cells value from a string
334pub(crate) fn parse_empty_cells(
335    input: &str,
336) -> Result<StyleEmptyCells, StyleEmptyCellsParseError<'_>> {
337    match input.trim() {
338        "show" => Ok(StyleEmptyCells::Show),
339        "hide" => Ok(StyleEmptyCells::Hide),
340        other => Err(StyleEmptyCellsParseError::InvalidKeyword(other)),
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn test_parse_table_layout() {
350        assert_eq!(parse_table_layout("auto").unwrap(), LayoutTableLayout::Auto);
351        assert_eq!(
352            parse_table_layout("fixed").unwrap(),
353            LayoutTableLayout::Fixed
354        );
355        assert!(parse_table_layout("invalid").is_err());
356    }
357
358    #[test]
359    fn test_parse_border_collapse() {
360        assert_eq!(
361            parse_border_collapse("separate").unwrap(),
362            StyleBorderCollapse::Separate
363        );
364        assert_eq!(
365            parse_border_collapse("collapse").unwrap(),
366            StyleBorderCollapse::Collapse
367        );
368        assert!(parse_border_collapse("invalid").is_err());
369    }
370
371    #[test]
372    fn test_parse_border_spacing() {
373        let spacing1 = parse_border_spacing("5px").unwrap();
374        assert_eq!(spacing1.horizontal, PixelValue::const_px(5));
375        assert_eq!(spacing1.vertical, PixelValue::const_px(5));
376
377        let spacing2 = parse_border_spacing("5px 10px").unwrap();
378        assert_eq!(spacing2.horizontal, PixelValue::const_px(5));
379        assert_eq!(spacing2.vertical, PixelValue::const_px(10));
380    }
381
382    #[test]
383    fn test_parse_caption_side() {
384        assert_eq!(parse_caption_side("top").unwrap(), StyleCaptionSide::Top);
385        assert_eq!(
386            parse_caption_side("bottom").unwrap(),
387            StyleCaptionSide::Bottom
388        );
389        assert!(parse_caption_side("invalid").is_err());
390    }
391
392    #[test]
393    fn test_parse_empty_cells() {
394        assert_eq!(parse_empty_cells("show").unwrap(), StyleEmptyCells::Show);
395        assert_eq!(parse_empty_cells("hide").unwrap(), StyleEmptyCells::Hide);
396        assert!(parse_empty_cells("invalid").is_err());
397    }
398}
399
400#[cfg(test)]
401#[allow(clippy::float_cmp)] // exact comparisons are the point: FloatValue is fixed-point
402mod autotest_generated {
403    use crate::props::basic::SizeMetric;
404
405    use super::*;
406
407    // `FloatValue` stores millipixels in an `isize` (value * 1000, truncated via
408    // `as`). `.number.number()` is that raw integer -- asserting on it keeps the
409    // numeric tests exact instead of comparing floats.
410    fn raw(p: PixelValue) -> isize {
411        p.number.number()
412    }
413
414    fn table_layout_ok(s: &str) -> bool {
415        parse_table_layout(s).is_ok()
416    }
417    fn border_collapse_ok(s: &str) -> bool {
418        parse_border_collapse(s).is_ok()
419    }
420    fn caption_side_ok(s: &str) -> bool {
421        parse_caption_side(s).is_ok()
422    }
423    fn empty_cells_ok(s: &str) -> bool {
424        parse_empty_cells(s).is_ok()
425    }
426
427    /// Every keyword parser, type-erased to `&str -> bool` (is it accepted?), so
428    /// the malformed-input cases can be asserted against all four at once.
429    type KeywordParser = (&'static str, fn(&str) -> bool);
430    const KEYWORD_PARSERS: &[KeywordParser] = &[
431        ("table-layout", table_layout_ok),
432        ("border-collapse", border_collapse_ok),
433        ("caption-side", caption_side_ok),
434        ("empty-cells", empty_cells_ok),
435    ];
436
437    fn assert_all_keyword_parsers_reject(input: &str, why: &str) {
438        for (prop, parse) in KEYWORD_PARSERS {
439            assert!(
440                !parse(input),
441                "{prop}: expected {input:?} to be rejected ({why}), but it parsed"
442            );
443        }
444    }
445
446    // ------------------------------------------------------------------------
447    // constructors
448    // ------------------------------------------------------------------------
449
450    #[test]
451    fn new_applies_the_same_spacing_to_both_axes() {
452        let s = LayoutBorderSpacing::new(PixelValue::const_px(7));
453        assert_eq!(s.horizontal, PixelValue::const_px(7));
454        assert_eq!(s.vertical, PixelValue::const_px(7));
455        assert_eq!(s.horizontal, s.vertical);
456    }
457
458    #[test]
459    fn new_separate_preserves_argument_order() {
460        // Asymmetric on purpose: a swapped assignment would still pass if both
461        // axes shared a metric *and* a value.
462        let s =
463            LayoutBorderSpacing::new_separate(PixelValue::const_px(3), PixelValue::percent(50.0));
464        assert_eq!(s.horizontal, PixelValue::const_px(3));
465        assert_eq!(s.vertical, PixelValue::percent(50.0));
466        assert_ne!(s.horizontal, s.vertical);
467    }
468
469    #[test]
470    fn constructors_do_not_panic_on_extreme_floats() {
471        let extremes = [
472            0.0f32,
473            -0.0,
474            1.0,
475            -1.0,
476            f32::EPSILON,
477            f32::MIN_POSITIVE,
478            -f32::MIN_POSITIVE,
479            f32::MAX,
480            f32::MIN,
481            f32::INFINITY,
482            f32::NEG_INFINITY,
483            f32::NAN,
484        ];
485
486        for v in extremes {
487            let same = LayoutBorderSpacing::new(PixelValue::px(v));
488            assert_eq!(same.horizontal, same.vertical, "new({v}) must be symmetric");
489
490            for w in extremes {
491                let sep = LayoutBorderSpacing::new_separate(PixelValue::px(v), PixelValue::em(w));
492                assert_eq!(sep.horizontal.metric, SizeMetric::Px);
493                assert_eq!(sep.vertical.metric, SizeMetric::Em);
494                // Whatever the input float was, the stored value is a plain isize,
495                // so it can never be NaN/inf once it lands in the struct.
496                assert!(sep.horizontal.number.get().is_finite());
497                assert!(sep.vertical.number.get().is_finite());
498            }
499        }
500    }
501
502    #[test]
503    fn nan_spacing_is_flattened_to_zero_and_stays_comparable() {
504        // `FloatValue::new` does `NaN * 1000.0 as isize`, and an `as` cast maps NaN
505        // to 0. So a NaN spacing silently becomes 0px rather than poisoning Eq/Ord
506        // (which are derived over the isize, not the float).
507        let nan = LayoutBorderSpacing::new(PixelValue::px(f32::NAN));
508        assert_eq!(raw(nan.horizontal), 0);
509        assert_eq!(raw(nan.vertical), 0);
510
511        // Reflexivity would fail here if equality were float-based.
512        assert_eq!(nan, nan);
513        assert_eq!(nan, LayoutBorderSpacing::new(PixelValue::px(f32::NAN)));
514        assert_eq!(nan, LayoutBorderSpacing::new(PixelValue::px(0.0)));
515        assert_eq!(nan, LayoutBorderSpacing::default());
516    }
517
518    #[test]
519    fn infinite_spacing_saturates_instead_of_wrapping() {
520        // `inf * 1000.0 as isize` saturates at the isize bounds; ditto any finite
521        // f32 whose *1000 scaling overflows (f32::MAX).
522        for input in [f32::INFINITY, f32::MAX, 1e38] {
523            let s = LayoutBorderSpacing::new(PixelValue::px(input));
524            assert_eq!(raw(s.horizontal), isize::MAX, "{input} must saturate high");
525            assert!(s.horizontal.number.get().is_finite());
526        }
527        for input in [f32::NEG_INFINITY, f32::MIN, -1e38] {
528            let s = LayoutBorderSpacing::new(PixelValue::px(input));
529            assert_eq!(raw(s.horizontal), isize::MIN, "{input} must saturate low");
530            assert!(s.horizontal.number.get().is_finite());
531        }
532    }
533
534    #[test]
535    fn equal_border_spacings_hash_equal() {
536        use std::{
537            collections::hash_map::DefaultHasher,
538            hash::{Hash, Hasher},
539        };
540
541        fn hash(s: LayoutBorderSpacing) -> u64 {
542            let mut h = DefaultHasher::new();
543            s.hash(&mut h);
544            h.finish()
545        }
546
547        // NaN and 0.0 collapse to the same stored value, so the Hash/Eq contract
548        // must hold across them too.
549        assert_eq!(
550            hash(LayoutBorderSpacing::new(PixelValue::px(f32::NAN))),
551            hash(LayoutBorderSpacing::default())
552        );
553        assert_eq!(
554            hash(LayoutBorderSpacing::new(PixelValue::const_px(4))),
555            hash(LayoutBorderSpacing::new_separate(
556                PixelValue::const_px(4),
557                PixelValue::const_px(4)
558            ))
559        );
560    }
561
562    #[test]
563    fn defaults_are_the_css_initial_values() {
564        assert_eq!(LayoutTableLayout::default(), LayoutTableLayout::Auto);
565        assert_eq!(
566            StyleBorderCollapse::default(),
567            StyleBorderCollapse::Separate
568        );
569        assert_eq!(StyleCaptionSide::default(), StyleCaptionSide::Top);
570        assert_eq!(StyleEmptyCells::default(), StyleEmptyCells::Show);
571
572        let d = LayoutBorderSpacing::default();
573        assert_eq!(d, LayoutBorderSpacing::new(PixelValue::const_px(0)));
574        assert_eq!(raw(d.horizontal), 0);
575        assert_eq!(raw(d.vertical), 0);
576        assert_eq!(d.horizontal.metric, SizeMetric::Px);
577    }
578
579    // ------------------------------------------------------------------------
580    // keyword parsers: table-layout / border-collapse / caption-side / empty-cells
581    // ------------------------------------------------------------------------
582
583    #[test]
584    fn keyword_parsers_accept_every_variant() {
585        // Positive controls -- exhaustive over the variants of each enum.
586        assert_eq!(parse_table_layout("auto").unwrap(), LayoutTableLayout::Auto);
587        assert_eq!(
588            parse_table_layout("fixed").unwrap(),
589            LayoutTableLayout::Fixed
590        );
591        assert_eq!(
592            parse_border_collapse("separate").unwrap(),
593            StyleBorderCollapse::Separate
594        );
595        assert_eq!(
596            parse_border_collapse("collapse").unwrap(),
597            StyleBorderCollapse::Collapse
598        );
599        assert_eq!(parse_caption_side("top").unwrap(), StyleCaptionSide::Top);
600        assert_eq!(
601            parse_caption_side("bottom").unwrap(),
602            StyleCaptionSide::Bottom
603        );
604        assert_eq!(parse_empty_cells("show").unwrap(), StyleEmptyCells::Show);
605        assert_eq!(parse_empty_cells("hide").unwrap(), StyleEmptyCells::Hide);
606    }
607
608    #[test]
609    fn keyword_parsers_reject_empty_input() {
610        assert_all_keyword_parsers_reject("", "empty input");
611        assert_eq!(
612            parse_table_layout(""),
613            Err(LayoutTableLayoutParseError::InvalidKeyword(""))
614        );
615        assert_eq!(
616            parse_border_collapse(""),
617            Err(StyleBorderCollapseParseError::InvalidKeyword(""))
618        );
619        assert_eq!(
620            parse_caption_side(""),
621            Err(StyleCaptionSideParseError::InvalidKeyword(""))
622        );
623        assert_eq!(
624            parse_empty_cells(""),
625            Err(StyleEmptyCellsParseError::InvalidKeyword(""))
626        );
627    }
628
629    #[test]
630    fn keyword_parsers_reject_whitespace_only_input() {
631        for input in ["   ", "\t", "\n", "\r\n", "\t\n \x0c", "\u{a0}", "\u{2028}"] {
632            assert_all_keyword_parsers_reject(input, "whitespace only");
633        }
634        // The trim happens before the match, so the error payload is the *trimmed*
635        // input -- i.e. the empty string, not the original blanks.
636        assert_eq!(
637            parse_table_layout("  \t\n "),
638            Err(LayoutTableLayoutParseError::InvalidKeyword(""))
639        );
640    }
641
642    #[test]
643    fn keyword_parsers_trim_surrounding_whitespace() {
644        assert_eq!(
645            parse_table_layout("   auto\t\n").unwrap(),
646            LayoutTableLayout::Auto
647        );
648        assert_eq!(
649            parse_border_collapse("\r\n collapse  ").unwrap(),
650            StyleBorderCollapse::Collapse
651        );
652        assert_eq!(
653            parse_caption_side("\t bottom \t").unwrap(),
654            StyleCaptionSide::Bottom
655        );
656        assert_eq!(
657            parse_empty_cells("\n hide \n").unwrap(),
658            StyleEmptyCells::Hide
659        );
660    }
661
662    #[test]
663    fn keyword_parsers_also_trim_non_css_unicode_whitespace() {
664        // Characterized leniency: `str::trim` strips everything with the Unicode
665        // White_Space property, but CSS whitespace is only space/tab/LF/CR/FF. So
666        // NBSP- and LINE-SEPARATOR-padded keywords are accepted here even though a
667        // conformant tokenizer would reject them.
668        assert_eq!(
669            parse_table_layout("\u{a0}auto\u{a0}").unwrap(),
670            LayoutTableLayout::Auto
671        );
672        assert_eq!(
673            parse_empty_cells("\u{2028}show\u{2029}").unwrap(),
674            StyleEmptyCells::Show
675        );
676    }
677
678    #[test]
679    fn keyword_parse_errors_carry_the_trimmed_input() {
680        // The error borrows from `input` (lifetime-tied) and reports the trimmed
681        // slice, not the raw one.
682        assert_eq!(
683            parse_table_layout("  bogus  "),
684            Err(LayoutTableLayoutParseError::InvalidKeyword("bogus"))
685        );
686        assert_eq!(
687            parse_border_collapse(" separate collapse "),
688            Err(StyleBorderCollapseParseError::InvalidKeyword(
689                "separate collapse"
690            ))
691        );
692        assert_eq!(
693            parse_caption_side("\ttop;\t"),
694            Err(StyleCaptionSideParseError::InvalidKeyword("top;"))
695        );
696        assert_eq!(
697            parse_empty_cells(" show hide "),
698            Err(StyleEmptyCellsParseError::InvalidKeyword("show hide"))
699        );
700    }
701
702    #[test]
703    fn keyword_parsers_are_case_sensitive() {
704        // Conformance gap (characterized, matching `parse_pixel_value`): CSS
705        // keywords are ASCII case-insensitive, so `table-layout: AUTO` is valid CSS
706        // -- but the match arms only cover the lowercase spelling.
707        for input in [
708            "AUTO", "Auto", "aUtO", "FIXED", "SEPARATE", "Collapse", "TOP", "Bottom", "SHOW",
709            "Hide",
710        ] {
711            assert_all_keyword_parsers_reject(input, "keyword matching is case-sensitive");
712        }
713    }
714
715    #[test]
716    fn keyword_parsers_reject_garbage() {
717        for input in [
718            "invalid",
719            "auto fixed",
720            "auto;",
721            "auto;garbage",
722            "auto/*c*/",
723            "/*auto*/",
724            "au to",
725            "a\0uto",
726            "\0",
727            "\u{7f}",
728            "-->",
729            "<!--",
730            "!important",
731            "\\61 uto",
732            "'auto'",
733            "\"auto\"",
734            "auto()",
735            "url(auto)",
736        ] {
737            assert_all_keyword_parsers_reject(input, "garbage");
738        }
739    }
740
741    #[test]
742    fn keyword_parsers_reject_boundary_numeric_strings() {
743        // None of these properties take a number; the numeric boundary cases must
744        // not be coerced into a keyword.
745        for input in [
746            "0",
747            "-0",
748            "0.0",
749            "1",
750            "-1",
751            "9223372036854775807",  // i64::MAX
752            "-9223372036854775808", // i64::MIN
753            "18446744073709551616", // u64::MAX + 1
754            "1e400",
755            "-1e400",
756            "3.4028235e38",
757            "1.17549435e-38",
758            "NaN",
759            "nan",
760            "inf",
761            "-inf",
762            "infinity",
763        ] {
764            assert_all_keyword_parsers_reject(input, "numeric input for a keyword property");
765        }
766    }
767
768    #[test]
769    fn keyword_parsers_reject_leading_and_trailing_junk() {
770        assert_all_keyword_parsers_reject("xauto", "leading junk");
771        assert_all_keyword_parsers_reject("autox", "trailing junk");
772        assert_all_keyword_parsers_reject("auto!", "trailing junk");
773        assert_all_keyword_parsers_reject("(fixed)", "wrapped in parens");
774        assert_all_keyword_parsers_reject("collapse,", "trailing comma");
775        // Trailing/leading *whitespace* is the one thing that is trimmed away.
776        assert_eq!(
777            parse_table_layout("auto ").unwrap(),
778            LayoutTableLayout::Auto
779        );
780    }
781
782    #[test]
783    fn keyword_parsers_survive_unicode_input() {
784        for input in [
785            "\u{1F600}",                        // emoji
786            "auto\u{0301}",                     // combining acute on the final char
787            "\u{FF41}\u{FF55}\u{FF54}\u{FF4F}", // fullwidth "auto"
788            "\u{202E}auto",                     // RTL override prefix
789            "\u{FEFF}auto",                     // BOM prefix (not Unicode whitespace)
790            "аuto",                             // leading CYRILLIC A homoglyph
791            "🇩🇪",                               // regional indicator pair
792            "e\u{0301}\u{0301}\u{0301}",
793            "\u{10FFFF}", // highest scalar value
794        ] {
795            assert_all_keyword_parsers_reject(input, "non-ASCII input");
796        }
797        // Slicing must stay on char boundaries: the error payload borrows the input.
798        assert_eq!(
799            parse_table_layout("  \u{1F600}  "),
800            Err(LayoutTableLayoutParseError::InvalidKeyword("\u{1F600}"))
801        );
802    }
803
804    #[test]
805    fn keyword_parsers_survive_extremely_long_input() {
806        let long = "a".repeat(1_000_000);
807        assert_all_keyword_parsers_reject(&long, "1M-char input");
808        assert_eq!(
809            parse_table_layout(&long),
810            Err(LayoutTableLayoutParseError::InvalidKeyword(long.as_str()))
811        );
812
813        // A million repetitions of a *valid* token is still not the token.
814        let repeated = "auto".repeat(250_000);
815        assert_all_keyword_parsers_reject(&repeated, "repeated valid token");
816
817        // Padding a valid keyword with a megabyte of whitespace still parses.
818        let padded = format!("{}auto{}", " ".repeat(500_000), "\n".repeat(500_000));
819        assert_eq!(
820            parse_table_layout(&padded).unwrap(),
821            LayoutTableLayout::Auto
822        );
823    }
824
825    #[test]
826    fn keyword_parsers_survive_deeply_nested_input() {
827        // These parsers are non-recursive, so 10k nesting levels must not blow the
828        // stack -- assert that explicitly so a future recursive-descent rewrite of
829        // the value parser trips here.
830        let nested = format!("{}auto{}", "(".repeat(10_000), ")".repeat(10_000));
831        assert_all_keyword_parsers_reject(&nested, "10k nested brackets");
832
833        let braces = "{".repeat(50_000);
834        assert_all_keyword_parsers_reject(&braces, "50k open braces");
835    }
836
837    // ------------------------------------------------------------------------
838    // border-spacing
839    // ------------------------------------------------------------------------
840
841    #[test]
842    fn border_spacing_single_value_applies_to_both_axes() {
843        let s = parse_border_spacing("5px").unwrap();
844        assert_eq!(s.horizontal, PixelValue::const_px(5));
845        assert_eq!(s.vertical, PixelValue::const_px(5));
846        assert_eq!(s, LayoutBorderSpacing::new(PixelValue::const_px(5)));
847    }
848
849    #[test]
850    fn border_spacing_two_values_are_horizontal_then_vertical() {
851        let s = parse_border_spacing("5px 10px").unwrap();
852        assert_eq!(s.horizontal, PixelValue::const_px(5));
853        assert_eq!(s.vertical, PixelValue::const_px(10));
854        // Order matters: the reversed input must not compare equal.
855        assert_ne!(s, parse_border_spacing("10px 5px").unwrap());
856    }
857
858    #[test]
859    fn border_spacing_accepts_mixed_metrics_per_axis() {
860        let s = parse_border_spacing("1em 50%").unwrap();
861        assert_eq!(s.horizontal.metric, SizeMetric::Em);
862        assert_eq!(raw(s.horizontal), 1000);
863        assert_eq!(s.vertical.metric, SizeMetric::Percent);
864        assert_eq!(raw(s.vertical), 50_000);
865
866        // "rem" must win over "em" in the suffix table.
867        assert_eq!(
868            parse_border_spacing("2rem").unwrap().horizontal.metric,
869            SizeMetric::Rem
870        );
871        // ...and "vmax"/"vmin" over "vw"/"vh".
872        assert_eq!(
873            parse_border_spacing("2vmax 3vmin")
874                .unwrap()
875                .horizontal
876                .metric,
877            SizeMetric::Vmax
878        );
879    }
880
881    #[test]
882    fn border_spacing_rejects_empty_and_whitespace_only_input() {
883        // `split_whitespace` yields zero parts, which falls through to the `_` arm.
884        for input in ["", "   ", "\t\n", "\r\n\x0c ", "\u{a0}", "\u{2028}\u{2029}"] {
885            assert_eq!(
886                parse_border_spacing(input),
887                Err(LayoutBorderSpacingParseError::InvalidFormat),
888                "expected InvalidFormat for {input:?}"
889            );
890        }
891    }
892
893    #[test]
894    fn border_spacing_rejects_more_than_two_components() {
895        for input in ["5px 10px 15px", "1px 2px 3px 4px", "0 0 0"] {
896            assert_eq!(
897                parse_border_spacing(input),
898                Err(LayoutBorderSpacingParseError::InvalidFormat),
899                "expected InvalidFormat for {input:?}"
900            );
901        }
902    }
903
904    #[test]
905    fn border_spacing_collapses_arbitrary_internal_whitespace() {
906        let expected =
907            LayoutBorderSpacing::new_separate(PixelValue::const_px(5), PixelValue::const_px(10));
908        for input in [
909            "5px 10px",
910            "  5px   10px  ",
911            "\t5px\n10px\r\n",
912            "5px\x0c10px",
913        ] {
914            assert_eq!(parse_border_spacing(input).unwrap(), expected, "{input:?}");
915        }
916    }
917
918    #[test]
919    fn border_spacing_splits_on_non_css_unicode_whitespace() {
920        // Characterized leniency (same root cause as the keyword trim):
921        // `split_whitespace` uses the Unicode White_Space property, so a
922        // non-breaking space separates the two components even though CSS would
923        // tokenize `5px\u{a0}10px` as a single (invalid) dimension token.
924        let s = parse_border_spacing("5px\u{a0}10px").unwrap();
925        assert_eq!(s.horizontal, PixelValue::const_px(5));
926        assert_eq!(s.vertical, PixelValue::const_px(10));
927    }
928
929    #[test]
930    fn border_spacing_propagates_pixel_value_errors() {
931        // Unit with no number.
932        assert!(matches!(
933            parse_border_spacing("px"),
934            Err(LayoutBorderSpacingParseError::PixelValue(
935                CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px)
936            ))
937        ));
938        // Garbage, unknown unit, trailing junk.
939        for input in ["abc", "5foo", "5px;", "#5px", "5 .. px"] {
940            assert!(
941                parse_border_spacing(input).is_err(),
942                "expected {input:?} to be rejected"
943            );
944        }
945        // A separated unit makes the *second* component the failing one.
946        assert!(matches!(
947            parse_border_spacing("5 px"),
948            Err(LayoutBorderSpacingParseError::PixelValue(
949                CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px)
950            ))
951        ));
952        // The error payload borrows the offending slice, not the whole input.
953        assert!(matches!(
954            parse_border_spacing("5px zzz"),
955            Err(LayoutBorderSpacingParseError::PixelValue(
956                CssPixelValueParseError::InvalidPixelValue("zzz")
957            ))
958        ));
959    }
960
961    #[test]
962    fn border_spacing_accepts_unitless_numbers() {
963        // Characterized gap: `parse_pixel_value` falls back to a bare f32 and
964        // assumes px, so `border-spacing: 5` is accepted as 5px. Per CSS, only a
965        // unitless *zero* is legal for a <length>.
966        assert_eq!(
967            parse_border_spacing("0").unwrap(),
968            LayoutBorderSpacing::new(PixelValue::const_px(0))
969        );
970        assert_eq!(
971            parse_border_spacing("5").unwrap(),
972            LayoutBorderSpacing::new(PixelValue::const_px(5))
973        );
974        // -0 must land on the same stored value as +0 (no signed-zero surprises).
975        assert_eq!(raw(parse_border_spacing("-0").unwrap().horizontal), 0);
976        assert_eq!(
977            parse_border_spacing("-0").unwrap(),
978            parse_border_spacing("0").unwrap()
979        );
980    }
981
982    #[test]
983    fn border_spacing_accepts_negative_values() {
984        // Characterized gap: `border-spacing` is a non-negative <length> in CSS,
985        // but nothing here clamps -- the negative value is stored as-is.
986        let s = parse_border_spacing("-5px -10px").unwrap();
987        assert_eq!(raw(s.horizontal), -5000);
988        assert_eq!(raw(s.vertical), -10_000);
989    }
990
991    #[test]
992    fn border_spacing_flattens_nan_to_zero() {
993        // "NaN" is a valid f32 literal, so this reaches `FloatValue::new(NaN)`,
994        // where `NaN * 1000.0 as isize` yields 0. The parse succeeds with 0px --
995        // it does not panic, and it does not store a NaN.
996        let s = parse_border_spacing("NaNpx").unwrap();
997        assert_eq!(raw(s.horizontal), 0);
998        assert_eq!(raw(s.vertical), 0);
999        assert!(s.horizontal.number.get().is_finite());
1000        assert_eq!(s, LayoutBorderSpacing::default());
1001
1002        let mixed = parse_border_spacing("NaNpx 4px").unwrap();
1003        assert_eq!(raw(mixed.horizontal), 0);
1004        assert_eq!(raw(mixed.vertical), 4000);
1005    }
1006
1007    #[test]
1008    fn border_spacing_saturates_infinite_and_overflowing_values() {
1009        // Both an explicit infinity and a finite-but-overflowing literal (whose
1010        // *1000 scaling overflows isize) must saturate rather than wrap.
1011        for input in ["infpx", "infinitypx", "1e40px", "3.5e38px"] {
1012            let s = parse_border_spacing(input).unwrap();
1013            assert_eq!(raw(s.horizontal), isize::MAX, "{input} must saturate high");
1014            assert!(
1015                s.horizontal.number.get().is_finite(),
1016                "{input} must be finite"
1017            );
1018        }
1019        for input in ["-infpx", "-1e40px", "-3.5e38px"] {
1020            let s = parse_border_spacing(input).unwrap();
1021            assert_eq!(raw(s.horizontal), isize::MIN, "{input} must saturate low");
1022            assert!(
1023                s.horizontal.number.get().is_finite(),
1024                "{input} must be finite"
1025            );
1026        }
1027        // Saturation is per-axis, and the high/low bounds do not collapse together.
1028        let s = parse_border_spacing("infpx -infpx").unwrap();
1029        assert_eq!(raw(s.horizontal), isize::MAX);
1030        assert_eq!(raw(s.vertical), isize::MIN);
1031        assert_ne!(s.horizontal, s.vertical);
1032    }
1033
1034    #[test]
1035    fn border_spacing_truncates_below_milli_precision() {
1036        // FloatValue is fixed-point with 3 decimals and truncates (does not round)
1037        // toward zero, so sub-millipixel input silently becomes 0.
1038        assert_eq!(raw(parse_border_spacing("0.0005px").unwrap().horizontal), 0);
1039        assert_eq!(
1040            raw(parse_border_spacing("0.9999px").unwrap().horizontal),
1041            999
1042        );
1043        assert_eq!(
1044            raw(parse_border_spacing("1.9999px").unwrap().horizontal),
1045            1999
1046        );
1047        // Denormal input must not panic or produce a non-finite stored value.
1048        let denormal = parse_border_spacing("1.17549435e-38px").unwrap();
1049        assert_eq!(raw(denormal.horizontal), 0);
1050    }
1051
1052    #[test]
1053    fn border_spacing_survives_extremely_long_input() {
1054        // One 1M-char token: rejected by the pixel parser, and the error borrows
1055        // the whole slice back out.
1056        let long_token = "z".repeat(1_000_000);
1057        assert!(parse_border_spacing(&long_token).is_err());
1058
1059        // 200k components: hits the `_ => InvalidFormat` arm without hanging.
1060        let many = "5px ".repeat(200_000);
1061        assert_eq!(
1062            parse_border_spacing(&many),
1063            Err(LayoutBorderSpacingParseError::InvalidFormat)
1064        );
1065
1066        // A megabyte of padding around a valid single value still parses.
1067        let padded = format!("{}5px{}", " ".repeat(500_000), " ".repeat(500_000));
1068        assert_eq!(
1069            parse_border_spacing(&padded).unwrap(),
1070            LayoutBorderSpacing::new(PixelValue::const_px(5))
1071        );
1072    }
1073
1074    #[test]
1075    fn border_spacing_survives_deeply_nested_input() {
1076        let nested = format!("{}5px{}", "(".repeat(10_000), ")".repeat(10_000));
1077        assert!(
1078            parse_border_spacing(&nested).is_err(),
1079            "10k nested brackets must be rejected, not stack-overflow"
1080        );
1081    }
1082
1083    #[test]
1084    fn border_spacing_unicode_input_does_not_panic() {
1085        for input in [
1086            "\u{1F600}",
1087            "5\u{1F600}px",
1088            "5px \u{1F600}",
1089            "5px",        // fullwidth digit
1090            "5\u{0301}px", // combining mark between number and unit
1091            "\u{FEFF}5px", // BOM prefix (not whitespace -> part of the token)
1092        ] {
1093            assert!(
1094                parse_border_spacing(input).is_err(),
1095                "expected {input:?} to be rejected"
1096            );
1097        }
1098    }
1099
1100    // ------------------------------------------------------------------------
1101    // round-trips: print_as_css_value -> parse_* must be the identity
1102    // ------------------------------------------------------------------------
1103
1104    #[test]
1105    fn keyword_enums_roundtrip_through_css() {
1106        for v in [LayoutTableLayout::Auto, LayoutTableLayout::Fixed] {
1107            assert_eq!(parse_table_layout(&v.print_as_css_value()).unwrap(), v);
1108        }
1109        for v in [StyleBorderCollapse::Separate, StyleBorderCollapse::Collapse] {
1110            assert_eq!(parse_border_collapse(&v.print_as_css_value()).unwrap(), v);
1111        }
1112        for v in [StyleCaptionSide::Top, StyleCaptionSide::Bottom] {
1113            assert_eq!(parse_caption_side(&v.print_as_css_value()).unwrap(), v);
1114        }
1115        for v in [StyleEmptyCells::Show, StyleEmptyCells::Hide] {
1116            assert_eq!(parse_empty_cells(&v.print_as_css_value()).unwrap(), v);
1117        }
1118        // Defaults round-trip too.
1119        assert_eq!(
1120            parse_table_layout(&LayoutTableLayout::default().print_as_css_value()).unwrap(),
1121            LayoutTableLayout::default()
1122        );
1123    }
1124
1125    #[test]
1126    fn keyword_enum_css_spellings_are_distinct() {
1127        // A copy-paste in `print_as_css_value` would make two variants print the
1128        // same string, which the round-trip above would silently accept for one of
1129        // them.
1130        assert_ne!(
1131            LayoutTableLayout::Auto.print_as_css_value(),
1132            LayoutTableLayout::Fixed.print_as_css_value()
1133        );
1134        assert_ne!(
1135            StyleBorderCollapse::Separate.print_as_css_value(),
1136            StyleBorderCollapse::Collapse.print_as_css_value()
1137        );
1138        assert_ne!(
1139            StyleCaptionSide::Top.print_as_css_value(),
1140            StyleCaptionSide::Bottom.print_as_css_value()
1141        );
1142        assert_ne!(
1143            StyleEmptyCells::Show.print_as_css_value(),
1144            StyleEmptyCells::Hide.print_as_css_value()
1145        );
1146    }
1147
1148    #[test]
1149    fn border_spacing_roundtrips_through_css() {
1150        let values = [
1151            PixelValue::const_px(0),
1152            PixelValue::const_px(5),
1153            PixelValue::const_px(-5),
1154            PixelValue::px(1.5),
1155            PixelValue::px(-0.125),
1156            PixelValue::em(2.0),
1157            PixelValue::rem(0.5),
1158            PixelValue::percent(50.0),
1159            PixelValue::from_metric(SizeMetric::Pt, 12.0),
1160            PixelValue::from_metric(SizeMetric::Vmin, 3.25),
1161        ];
1162
1163        for h in values {
1164            // Single-value form (horizontal == vertical).
1165            let same = LayoutBorderSpacing::new(h);
1166            assert_eq!(
1167                parse_border_spacing(&same.print_as_css_value()).unwrap(),
1168                same,
1169                "single-value round-trip failed for {h:?}"
1170            );
1171
1172            // Two-value form.
1173            for v in values {
1174                let sep = LayoutBorderSpacing::new_separate(h, v);
1175                assert_eq!(
1176                    parse_border_spacing(&sep.print_as_css_value()).unwrap(),
1177                    sep,
1178                    "two-value round-trip failed for {h:?} / {v:?}"
1179                );
1180            }
1181        }
1182    }
1183
1184    #[test]
1185    fn border_spacing_prints_one_value_only_when_both_axes_match() {
1186        assert_eq!(
1187            LayoutBorderSpacing::new(PixelValue::const_px(5)).print_as_css_value(),
1188            "5px"
1189        );
1190        assert_eq!(
1191            LayoutBorderSpacing::new_separate(PixelValue::const_px(5), PixelValue::const_px(10))
1192                .print_as_css_value(),
1193            "5px 10px"
1194        );
1195        // Same number, different unit -> still two values (the metric is part of Eq).
1196        assert_eq!(
1197            LayoutBorderSpacing::new_separate(PixelValue::const_px(0), PixelValue::percent(0.0))
1198                .print_as_css_value(),
1199            "0px 0%"
1200        );
1201        assert_eq!(
1202            LayoutBorderSpacing::default().print_as_css_value(),
1203            "0px",
1204            "the default must not print as a two-value form"
1205        );
1206    }
1207
1208    #[test]
1209    fn border_spacing_saturated_value_roundtrips_stably() {
1210        // The saturated bound prints as a lossy f32 (9223372000000000px), but
1211        // re-parsing it saturates right back to the same stored bound, so the
1212        // round-trip is still a fixed point.
1213        let saturated = parse_border_spacing("infpx").unwrap();
1214        let printed = saturated.print_as_css_value();
1215        assert_eq!(parse_border_spacing(&printed).unwrap(), saturated);
1216
1217        let low = parse_border_spacing("-infpx").unwrap();
1218        assert_eq!(
1219            parse_border_spacing(&low.print_as_css_value()).unwrap(),
1220            low
1221        );
1222    }
1223
1224    #[test]
1225    fn border_spacing_parse_print_is_idempotent_after_quantization() {
1226        // Inputs whose precision the fixed-point representation cannot hold must
1227        // reach a fixed point after a single round-trip, not drift on every pass.
1228        for input in ["0.0005px", "0.9999px", "0.1px", "1e40px", "NaNpx", "-0"] {
1229            let once = parse_border_spacing(input).unwrap();
1230            let printed = once.print_as_css_value();
1231            let twice = parse_border_spacing(&printed).unwrap();
1232            assert_eq!(once, twice, "not idempotent for {input:?}");
1233            assert_eq!(
1234                printed,
1235                twice.print_as_css_value(),
1236                "not stable for {input:?}"
1237            );
1238        }
1239    }
1240
1241    // ------------------------------------------------------------------------
1242    // FormatAsRustCode
1243    // ------------------------------------------------------------------------
1244
1245    #[test]
1246    fn format_as_rust_code_emits_the_variant_paths() {
1247        assert_eq!(
1248            LayoutTableLayout::Fixed.format_as_rust_code(0),
1249            "LayoutTableLayout::Fixed"
1250        );
1251        assert_eq!(
1252            StyleBorderCollapse::Collapse.format_as_rust_code(0),
1253            "StyleBorderCollapse::Collapse"
1254        );
1255        assert_eq!(
1256            StyleCaptionSide::Bottom.format_as_rust_code(0),
1257            "StyleCaptionSide::Bottom"
1258        );
1259        assert_eq!(
1260            StyleEmptyCells::Hide.format_as_rust_code(0),
1261            "StyleEmptyCells::Hide"
1262        );
1263
1264        // The indent argument is ignored -- it must not change the output.
1265        assert_eq!(
1266            LayoutTableLayout::Auto.format_as_rust_code(0),
1267            LayoutTableLayout::Auto.format_as_rust_code(usize::MAX)
1268        );
1269    }
1270
1271    #[test]
1272    fn format_as_rust_code_for_border_spacing_is_total() {
1273        assert_eq!(
1274            LayoutBorderSpacing::new(PixelValue::const_px(5)).format_as_rust_code(0),
1275            "LayoutBorderSpacing { horizontal: \
1276             PixelValue::const_from_metric_fractional(SizeMetric::Px, 5, 0), vertical: \
1277             PixelValue::const_from_metric_fractional(SizeMetric::Px, 5, 0) }"
1278        );
1279
1280        // Extreme / degenerate values must still format without panicking.
1281        for v in [
1282            f32::NAN,
1283            f32::INFINITY,
1284            f32::NEG_INFINITY,
1285            f32::MAX,
1286            f32::MIN,
1287            -2.5,
1288        ] {
1289            let code = LayoutBorderSpacing::new(PixelValue::px(v)).format_as_rust_code(0);
1290            assert!(code.starts_with("LayoutBorderSpacing {"), "{v}: {code}");
1291            assert!(code.ends_with('}'), "{v}: {code}");
1292        }
1293    }
1294}