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