Skip to main content

azul_css/props/layout/
display.rs

1//! CSS properties for `display` and `float`.
2
3use alloc::string::{String, ToString};
4use crate::corety::AzString;
5
6use crate::props::formatter::PrintAsCssValue;
7
8/// Represents a `display` CSS property value
9// +spec:display-property:472a62 - display property controls box generation types per CSS 2.2 §9.2
10// +spec:display-property:cf1820 - display type enum defining box generation qualities
11#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
12#[repr(C)]
13pub enum LayoutDisplay {
14    // Basic display types
15    None,
16    // +spec:display-property:7d945d - outer display defaults to block, inner defaults to flow
17    #[default]
18    Block,
19    Inline,
20    InlineBlock,
21
22    // Flex layout
23    Flex,
24    InlineFlex,
25
26    // +spec:display-property:03b26a - Table display types mapping document elements to CSS table model
27    // +spec:display-property:d40388 - layout-internal display types set both inner and outer display
28    // +spec:display-property:dcf7f5 - table display values (table, inline-table, table-row, etc.) per CSS 2.2 §17
29    // +spec:table-layout:7fdc60 - display property maps elements to table roles (CSS 2.2 §17.1)
30    // Table layout
31    // +spec:display-property:1554ad - Layout-internal display types for table layout
32    // +spec:table-layout:6cc828 - <display-internal> and <display-legacy> table display types
33    Table,
34    InlineTable,
35    TableRowGroup,
36    TableHeaderGroup,
37    TableFooterGroup,
38    TableRow,
39    TableColumnGroup,
40    TableColumn,
41    TableCell,
42    TableCaption,
43
44    FlowRoot,
45
46    // List layout
47    ListItem,
48
49    // Special displays
50    RunIn,
51    Marker,
52
53    // CSS3 additions
54    Grid,
55    InlineGrid,
56
57    // display:contents - element generates no box, children promoted to parent
58    Contents,
59}
60
61impl LayoutDisplay {
62    /// Returns true if this display type establishes a block formatting context.
63    #[must_use] pub const fn creates_block_context(&self) -> bool {
64        matches!(
65            self,
66            Self::Block
67                | Self::FlowRoot
68                | Self::Flex
69                | Self::Grid
70                | Self::Table
71                | Self::ListItem
72        )
73    }
74
75    /// Returns true if this display type establishes a flex formatting context.
76    #[must_use] pub const fn creates_flex_context(&self) -> bool {
77        matches!(self, Self::Flex | Self::InlineFlex)
78    }
79
80    // +spec:display-property:798b4f - table box establishes table formatting context (CSS 2.2 §17.4)
81    /// Returns true if this display type establishes a table formatting context.
82    #[must_use] pub const fn creates_table_context(&self) -> bool {
83        matches!(self, Self::Table | Self::InlineTable)
84    }
85
86    /// Returns true for layout-internal display types (CSS Display 3 §2.4):
87    /// table-row-group, table-header-group, table-footer-group, table-row,
88    /// table-column-group, table-column, table-cell, table-caption.
89    #[must_use] pub const fn is_layout_internal(&self) -> bool {
90        matches!(
91            self,
92            Self::TableRowGroup
93                | Self::TableHeaderGroup
94                | Self::TableFooterGroup
95                | Self::TableRow
96                | Self::TableColumnGroup
97                | Self::TableColumn
98                | Self::TableCell
99                | Self::TableCaption
100        )
101    }
102
103    // +spec:display-property:101f27 - inline-level boxes (InlineBlock, InlineFlex, etc.) vs inline boxes (Inline)
104    // +spec:display-property:18e77e - inner-only display keywords (flex, grid, table, flow-root) are not inline-level, defaulting outer display to block
105    // +spec:display-property:a43e48 - inline-table is inline-level per CSS 2.2 §17.4
106    /// Returns true if this display type generates an inline-level box.
107    #[must_use] pub const fn is_inline_level(&self) -> bool {
108        matches!(
109            self,
110            Self::Inline
111                | Self::InlineBlock
112                | Self::InlineFlex
113                | Self::InlineTable
114                | Self::InlineGrid
115        )
116    }
117
118    /// Returns true for an ATOMIC inline-level box (CSS Display 3 §2.2 / §5.1):
119    /// `inline-block`, `inline-flex`, `inline-table`, `inline-grid`. These are
120    /// inline-level but establish their own independent formatting context and
121    /// participate in their parent's IFC as a single opaque unit — unlike a
122    /// (non-atomic) inline box (`Inline`), whose content flows directly into the
123    /// IFC. `is_inline_level` covers both; this predicate is the distinction
124    /// between them (which the enum has variants for but previously had no way to
125    /// query).
126    #[must_use] pub const fn is_atomic_inline(&self) -> bool {
127        matches!(
128            self,
129            Self::InlineBlock | Self::InlineFlex | Self::InlineTable | Self::InlineGrid
130        )
131    }
132}
133
134// +spec:display-property:cabaec - serialization uses short display keywords per CSSOM precedence rules
135impl PrintAsCssValue for LayoutDisplay {
136    fn print_as_css_value(&self) -> String {
137        String::from(match self {
138            Self::None => "none",
139            Self::Block => "block",
140            Self::Inline => "inline",
141            Self::InlineBlock => "inline-block",
142            Self::Flex => "flex",
143            Self::InlineFlex => "inline-flex",
144            Self::Table => "table",
145            Self::InlineTable => "inline-table",
146            Self::TableRowGroup => "table-row-group",
147            Self::TableHeaderGroup => "table-header-group",
148            Self::TableFooterGroup => "table-footer-group",
149            Self::TableRow => "table-row",
150            Self::TableColumnGroup => "table-column-group",
151            Self::TableColumn => "table-column",
152            Self::TableCell => "table-cell",
153            Self::TableCaption => "table-caption",
154            Self::ListItem => "list-item",
155            Self::RunIn => "run-in",
156            Self::Marker => "marker",
157            Self::FlowRoot => "flow-root",
158            Self::Grid => "grid",
159            Self::InlineGrid => "inline-grid",
160            Self::Contents => "contents",
161        })
162    }
163}
164
165/// Represents a `float` attribute
166#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
167#[repr(C)]
168pub enum LayoutFloat {
169    Left,
170    Right,
171    #[default]
172    None,
173}
174
175impl PrintAsCssValue for LayoutFloat {
176    fn print_as_css_value(&self) -> String {
177        String::from(match self {
178            Self::Left => "left",
179            Self::Right => "right",
180            Self::None => "none",
181        })
182    }
183}
184
185// --- PARSERS ---
186
187#[cfg(feature = "parser")]
188#[derive(Clone, PartialEq, Eq)]
189pub enum LayoutDisplayParseError<'a> {
190    InvalidValue(&'a str),
191}
192
193#[cfg(feature = "parser")]
194impl_debug_as_display!(LayoutDisplayParseError<'a>);
195
196#[cfg(feature = "parser")]
197impl_display! { LayoutDisplayParseError<'a>, {
198    InvalidValue(val) => format!("Invalid display value: \"{}\"", val),
199}}
200
201#[cfg(feature = "parser")]
202#[derive(Debug, Clone, PartialEq, Eq)]
203#[repr(C, u8)]
204pub enum LayoutDisplayParseErrorOwned {
205    InvalidValue(AzString),
206}
207
208#[cfg(feature = "parser")]
209impl LayoutDisplayParseError<'_> {
210    #[must_use] pub fn to_contained(&self) -> LayoutDisplayParseErrorOwned {
211        match self {
212            Self::InvalidValue(s) => LayoutDisplayParseErrorOwned::InvalidValue((*s).to_string().into()),
213        }
214    }
215}
216
217#[cfg(feature = "parser")]
218impl LayoutDisplayParseErrorOwned {
219    #[must_use] pub fn to_shared(&self) -> LayoutDisplayParseError<'_> {
220        match self {
221            Self::InvalidValue(s) => LayoutDisplayParseError::InvalidValue(s.as_str()),
222        }
223    }
224}
225
226#[cfg(feature = "parser")]
227/// # Errors
228///
229/// Returns an error if `input` is not a valid CSS `display` value.
230pub fn parse_layout_display(
231    input: &str,
232) -> Result<LayoutDisplay, LayoutDisplayParseError<'_>> {
233    let input = input.trim();
234    match input {
235        "none" => Ok(LayoutDisplay::None),
236        "block" => Ok(LayoutDisplay::Block),
237        "inline" => Ok(LayoutDisplay::Inline),
238        // +spec:display-property:f704ef - legacy single-keyword inline-level display values (inline-block, inline-table, inline-flex, inline-grid)
239        "inline-block" => Ok(LayoutDisplay::InlineBlock),
240        "flex" => Ok(LayoutDisplay::Flex),
241        "inline-flex" => Ok(LayoutDisplay::InlineFlex),
242        "table" => Ok(LayoutDisplay::Table),
243        "inline-table" => Ok(LayoutDisplay::InlineTable),
244        "table-row-group" => Ok(LayoutDisplay::TableRowGroup),
245        "table-header-group" => Ok(LayoutDisplay::TableHeaderGroup),
246        "table-footer-group" => Ok(LayoutDisplay::TableFooterGroup),
247        "table-row" => Ok(LayoutDisplay::TableRow),
248        "table-column-group" => Ok(LayoutDisplay::TableColumnGroup),
249        "table-column" => Ok(LayoutDisplay::TableColumn),
250        "table-cell" => Ok(LayoutDisplay::TableCell),
251        "table-caption" => Ok(LayoutDisplay::TableCaption),
252        "list-item" => Ok(LayoutDisplay::ListItem),
253        "run-in" => Ok(LayoutDisplay::RunIn),
254        "marker" => Ok(LayoutDisplay::Marker),
255        "grid" => Ok(LayoutDisplay::Grid),
256        "inline-grid" => Ok(LayoutDisplay::InlineGrid),
257        "flow-root" => Ok(LayoutDisplay::FlowRoot),
258        "contents" => Ok(LayoutDisplay::Contents),
259        _ => Err(LayoutDisplayParseError::InvalidValue(input)),
260    }
261}
262
263#[cfg(feature = "parser")]
264#[derive(Clone, PartialEq, Eq)]
265pub enum LayoutFloatParseError<'a> {
266    InvalidValue(&'a str),
267}
268
269#[cfg(feature = "parser")]
270impl_debug_as_display!(LayoutFloatParseError<'a>);
271
272#[cfg(feature = "parser")]
273impl_display! { LayoutFloatParseError<'a>, {
274    InvalidValue(val) => format!("Invalid float value: \"{}\"", val),
275}}
276
277#[cfg(feature = "parser")]
278#[derive(Debug, Clone, PartialEq, Eq)]
279#[repr(C, u8)]
280pub enum LayoutFloatParseErrorOwned {
281    InvalidValue(AzString),
282}
283
284#[cfg(feature = "parser")]
285impl LayoutFloatParseError<'_> {
286    #[must_use] pub fn to_contained(&self) -> LayoutFloatParseErrorOwned {
287        match self {
288            Self::InvalidValue(s) => LayoutFloatParseErrorOwned::InvalidValue((*s).to_string().into()),
289        }
290    }
291}
292
293#[cfg(feature = "parser")]
294impl LayoutFloatParseErrorOwned {
295    #[must_use] pub fn to_shared(&self) -> LayoutFloatParseError<'_> {
296        match self {
297            Self::InvalidValue(s) => LayoutFloatParseError::InvalidValue(s.as_str()),
298        }
299    }
300}
301
302#[cfg(feature = "parser")]
303/// # Errors
304///
305/// Returns an error if `input` is not a valid CSS `float` value.
306pub fn parse_layout_float(input: &str) -> Result<LayoutFloat, LayoutFloatParseError<'_>> {
307    let input = input.trim();
308    match input {
309        "left" => Ok(LayoutFloat::Left),
310        "right" => Ok(LayoutFloat::Right),
311        "none" => Ok(LayoutFloat::None),
312        _ => Err(LayoutFloatParseError::InvalidValue(input)),
313    }
314}
315
316#[cfg(all(test, feature = "parser"))]
317mod tests {
318    use super::*;
319
320    #[test]
321    #[allow(clippy::cognitive_complexity)] // large but cohesive: single-purpose CSS parser/formatter/dispatch table (one branch per property/variant)
322    fn test_parse_layout_display() {
323        assert_eq!(parse_layout_display("block").unwrap(), LayoutDisplay::Block);
324        assert_eq!(
325            parse_layout_display("inline").unwrap(),
326            LayoutDisplay::Inline
327        );
328        assert_eq!(
329            parse_layout_display("inline-block").unwrap(),
330            LayoutDisplay::InlineBlock
331        );
332        assert_eq!(parse_layout_display("flex").unwrap(), LayoutDisplay::Flex);
333        assert_eq!(
334            parse_layout_display("inline-flex").unwrap(),
335            LayoutDisplay::InlineFlex
336        );
337        assert_eq!(parse_layout_display("grid").unwrap(), LayoutDisplay::Grid);
338        assert_eq!(
339            parse_layout_display("inline-grid").unwrap(),
340            LayoutDisplay::InlineGrid
341        );
342        assert_eq!(parse_layout_display("none").unwrap(), LayoutDisplay::None);
343        assert_eq!(
344            parse_layout_display("flow-root").unwrap(),
345            LayoutDisplay::FlowRoot
346        );
347        assert_eq!(
348            parse_layout_display("list-item").unwrap(),
349            LayoutDisplay::ListItem
350        );
351        // Note: 'inherit' and 'initial' are handled by the CSS cascade system,
352        // not as enum variants
353        assert!(parse_layout_display("inherit").is_err());
354        assert!(parse_layout_display("initial").is_err());
355
356        // Table values
357        assert_eq!(parse_layout_display("table").unwrap(), LayoutDisplay::Table);
358        assert_eq!(
359            parse_layout_display("inline-table").unwrap(),
360            LayoutDisplay::InlineTable
361        );
362        assert_eq!(
363            parse_layout_display("table-row").unwrap(),
364            LayoutDisplay::TableRow
365        );
366        assert_eq!(
367            parse_layout_display("table-cell").unwrap(),
368            LayoutDisplay::TableCell
369        );
370        assert_eq!(
371            parse_layout_display("table-caption").unwrap(),
372            LayoutDisplay::TableCaption
373        );
374        assert_eq!(
375            parse_layout_display("table-column-group").unwrap(),
376            LayoutDisplay::TableColumnGroup
377        );
378        assert_eq!(
379            parse_layout_display("table-header-group").unwrap(),
380            LayoutDisplay::TableHeaderGroup
381        );
382        assert_eq!(
383            parse_layout_display("table-footer-group").unwrap(),
384            LayoutDisplay::TableFooterGroup
385        );
386        assert_eq!(
387            parse_layout_display("table-row-group").unwrap(),
388            LayoutDisplay::TableRowGroup
389        );
390
391        // Whitespace
392        assert_eq!(
393            parse_layout_display("  inline-flex  ").unwrap(),
394            LayoutDisplay::InlineFlex
395        );
396
397        // Invalid values
398        assert!(parse_layout_display("invalid-value").is_err());
399        assert!(parse_layout_display("").is_err());
400        assert!(parse_layout_display("display").is_err());
401    }
402
403    #[test]
404    fn test_parse_layout_float() {
405        assert_eq!(parse_layout_float("left").unwrap(), LayoutFloat::Left);
406        assert_eq!(parse_layout_float("right").unwrap(), LayoutFloat::Right);
407        assert_eq!(parse_layout_float("none").unwrap(), LayoutFloat::None);
408
409        // Whitespace
410        assert_eq!(parse_layout_float("  right  ").unwrap(), LayoutFloat::Right);
411
412        // Invalid values
413        assert!(parse_layout_float("center").is_err());
414        assert!(parse_layout_float("").is_err());
415        assert!(parse_layout_float("float-left").is_err());
416    }
417}
418
419#[cfg(all(test, feature = "parser"))]
420mod autotest_generated {
421    use super::*;
422
423    /// Every `LayoutDisplay` variant. Kept honest by [`display_variant_index`],
424    /// whose exhaustive match stops compiling when a variant is added.
425    const ALL_DISPLAY: [LayoutDisplay; 23] = [
426        LayoutDisplay::None,
427        LayoutDisplay::Block,
428        LayoutDisplay::Inline,
429        LayoutDisplay::InlineBlock,
430        LayoutDisplay::Flex,
431        LayoutDisplay::InlineFlex,
432        LayoutDisplay::Table,
433        LayoutDisplay::InlineTable,
434        LayoutDisplay::TableRowGroup,
435        LayoutDisplay::TableHeaderGroup,
436        LayoutDisplay::TableFooterGroup,
437        LayoutDisplay::TableRow,
438        LayoutDisplay::TableColumnGroup,
439        LayoutDisplay::TableColumn,
440        LayoutDisplay::TableCell,
441        LayoutDisplay::TableCaption,
442        LayoutDisplay::FlowRoot,
443        LayoutDisplay::ListItem,
444        LayoutDisplay::RunIn,
445        LayoutDisplay::Marker,
446        LayoutDisplay::Grid,
447        LayoutDisplay::InlineGrid,
448        LayoutDisplay::Contents,
449    ];
450
451    const ALL_FLOAT: [LayoutFloat; 3] = [LayoutFloat::Left, LayoutFloat::Right, LayoutFloat::None];
452
453    /// Position of each variant inside `ALL_DISPLAY`. The match is deliberately
454    /// exhaustive (no `_` arm) so a new `LayoutDisplay` variant is a compile
455    /// error here rather than a silently untested variant below.
456    const fn display_variant_index(d: LayoutDisplay) -> usize {
457        match d {
458            LayoutDisplay::None => 0,
459            LayoutDisplay::Block => 1,
460            LayoutDisplay::Inline => 2,
461            LayoutDisplay::InlineBlock => 3,
462            LayoutDisplay::Flex => 4,
463            LayoutDisplay::InlineFlex => 5,
464            LayoutDisplay::Table => 6,
465            LayoutDisplay::InlineTable => 7,
466            LayoutDisplay::TableRowGroup => 8,
467            LayoutDisplay::TableHeaderGroup => 9,
468            LayoutDisplay::TableFooterGroup => 10,
469            LayoutDisplay::TableRow => 11,
470            LayoutDisplay::TableColumnGroup => 12,
471            LayoutDisplay::TableColumn => 13,
472            LayoutDisplay::TableCell => 14,
473            LayoutDisplay::TableCaption => 15,
474            LayoutDisplay::FlowRoot => 16,
475            LayoutDisplay::ListItem => 17,
476            LayoutDisplay::RunIn => 18,
477            LayoutDisplay::Marker => 19,
478            LayoutDisplay::Grid => 20,
479            LayoutDisplay::InlineGrid => 21,
480            LayoutDisplay::Contents => 22,
481        }
482    }
483
484    // -----------------------------------------------------------------
485    // Coverage guards
486    // -----------------------------------------------------------------
487
488    #[test]
489    fn all_display_lists_every_variant_exactly_once() {
490        for (i, d) in ALL_DISPLAY.iter().enumerate() {
491            assert_eq!(
492                display_variant_index(*d),
493                i,
494                "ALL_DISPLAY is out of sync at index {i} ({d:?})"
495            );
496        }
497    }
498
499    // -----------------------------------------------------------------
500    // creates_block_context / creates_flex_context / creates_table_context
501    // -----------------------------------------------------------------
502
503    #[test]
504    fn creates_block_context_matches_an_exact_variant_set() {
505        // NOTE (spec deviation, pinned as *current* behaviour): CSS 2.1 §9.4.1
506        // also gives inline-blocks, table-cells and table-captions a new block
507        // formatting context, and flex/grid containers establish flex/grid
508        // formatting contexts rather than block ones. This implementation uses
509        // the narrower set below.
510        const EXPECTED: [LayoutDisplay; 6] = [
511            LayoutDisplay::Block,
512            LayoutDisplay::FlowRoot,
513            LayoutDisplay::Flex,
514            LayoutDisplay::Grid,
515            LayoutDisplay::Table,
516            LayoutDisplay::ListItem,
517        ];
518        for d in ALL_DISPLAY {
519            assert_eq!(
520                d.creates_block_context(),
521                EXPECTED.contains(&d),
522                "creates_block_context({d:?})"
523            );
524        }
525    }
526
527    #[test]
528    fn creates_flex_context_matches_an_exact_variant_set() {
529        const EXPECTED: [LayoutDisplay; 2] = [LayoutDisplay::Flex, LayoutDisplay::InlineFlex];
530        for d in ALL_DISPLAY {
531            assert_eq!(
532                d.creates_flex_context(),
533                EXPECTED.contains(&d),
534                "creates_flex_context({d:?})"
535            );
536        }
537    }
538
539    #[test]
540    fn creates_table_context_matches_an_exact_variant_set() {
541        // Only the table *wrapper* boxes establish a table formatting context;
542        // the layout-internal boxes (rows, cells, ...) participate in one.
543        const EXPECTED: [LayoutDisplay; 2] = [LayoutDisplay::Table, LayoutDisplay::InlineTable];
544        for d in ALL_DISPLAY {
545            assert_eq!(
546                d.creates_table_context(),
547                EXPECTED.contains(&d),
548                "creates_table_context({d:?})"
549            );
550        }
551    }
552
553    // -----------------------------------------------------------------
554    // is_layout_internal / is_inline_level
555    // -----------------------------------------------------------------
556
557    #[test]
558    fn is_layout_internal_matches_css_display_3_table_internals() {
559        const EXPECTED: [LayoutDisplay; 8] = [
560            LayoutDisplay::TableRowGroup,
561            LayoutDisplay::TableHeaderGroup,
562            LayoutDisplay::TableFooterGroup,
563            LayoutDisplay::TableRow,
564            LayoutDisplay::TableColumnGroup,
565            LayoutDisplay::TableColumn,
566            LayoutDisplay::TableCell,
567            LayoutDisplay::TableCaption,
568        ];
569        for d in ALL_DISPLAY {
570            assert_eq!(
571                d.is_layout_internal(),
572                EXPECTED.contains(&d),
573                "is_layout_internal({d:?})"
574            );
575        }
576        // The table wrappers themselves are *not* layout-internal.
577        assert!(!LayoutDisplay::Table.is_layout_internal());
578        assert!(!LayoutDisplay::InlineTable.is_layout_internal());
579    }
580
581    #[test]
582    fn is_inline_level_matches_an_exact_variant_set() {
583        const EXPECTED: [LayoutDisplay; 5] = [
584            LayoutDisplay::Inline,
585            LayoutDisplay::InlineBlock,
586            LayoutDisplay::InlineFlex,
587            LayoutDisplay::InlineTable,
588            LayoutDisplay::InlineGrid,
589        ];
590        for d in ALL_DISPLAY {
591            assert_eq!(
592                d.is_inline_level(),
593                EXPECTED.contains(&d),
594                "is_inline_level({d:?})"
595            );
596        }
597        // Inner-only keywords default their outer display to block.
598        assert!(!LayoutDisplay::Flex.is_inline_level());
599        assert!(!LayoutDisplay::Grid.is_inline_level());
600        assert!(!LayoutDisplay::Table.is_inline_level());
601        assert!(!LayoutDisplay::FlowRoot.is_inline_level());
602    }
603
604    #[test]
605    fn is_atomic_inline_matches_an_exact_variant_set() {
606        // Atomic inline-level boxes = inline-level minus the (non-atomic) `Inline`
607        // box. Every atomic-inline is inline-level; `Inline` is inline-level but
608        // NOT atomic.
609        const EXPECTED: [LayoutDisplay; 4] = [
610            LayoutDisplay::InlineBlock,
611            LayoutDisplay::InlineFlex,
612            LayoutDisplay::InlineTable,
613            LayoutDisplay::InlineGrid,
614        ];
615        for d in ALL_DISPLAY {
616            assert_eq!(d.is_atomic_inline(), EXPECTED.contains(&d), "is_atomic_inline({d:?})");
617            // An atomic inline is always inline-level.
618            if d.is_atomic_inline() {
619                assert!(d.is_inline_level(), "atomic-inline must be inline-level ({d:?})");
620            }
621        }
622        assert!(!LayoutDisplay::Inline.is_atomic_inline());
623        assert!(LayoutDisplay::Inline.is_inline_level());
624    }
625
626    // -----------------------------------------------------------------
627    // Cross-predicate invariants
628    // -----------------------------------------------------------------
629
630    #[test]
631    fn flex_and_table_contexts_are_mutually_exclusive() {
632        for d in ALL_DISPLAY {
633            assert!(
634                !(d.creates_flex_context() && d.creates_table_context()),
635                "{d:?} claims to establish both a flex and a table formatting context"
636            );
637        }
638    }
639
640    #[test]
641    fn layout_internal_and_inline_level_are_mutually_exclusive() {
642        for d in ALL_DISPLAY {
643            assert!(
644                !(d.is_layout_internal() && d.is_inline_level()),
645                "{d:?} is both layout-internal and inline-level"
646            );
647        }
648    }
649
650    #[test]
651    fn boxless_displays_establish_and_generate_nothing() {
652        // `display: none` generates no box at all; `display: contents` generates
653        // no box for itself, so neither may establish any formatting context nor
654        // count as an inline-level or layout-internal box.
655        for d in [LayoutDisplay::None, LayoutDisplay::Contents] {
656            assert!(!d.creates_block_context(), "{d:?}");
657            assert!(!d.creates_flex_context(), "{d:?}");
658            assert!(!d.creates_table_context(), "{d:?}");
659            assert!(!d.is_layout_internal(), "{d:?}");
660            assert!(!d.is_inline_level(), "{d:?}");
661        }
662    }
663
664    #[test]
665    fn predicates_on_the_default_instance_do_not_panic() {
666        // `Default` is `block`: a block-level box establishing a block context.
667        let d = LayoutDisplay::default();
668        assert_eq!(d, LayoutDisplay::Block);
669        assert!(d.creates_block_context());
670        assert!(!d.creates_flex_context());
671        assert!(!d.creates_table_context());
672        assert!(!d.is_layout_internal());
673        assert!(!d.is_inline_level());
674
675        assert_eq!(LayoutFloat::default(), LayoutFloat::None);
676    }
677
678    #[test]
679    fn predicates_are_pure_and_repeatable() {
680        for d in ALL_DISPLAY {
681            let snapshot = (
682                d.creates_block_context(),
683                d.creates_flex_context(),
684                d.creates_table_context(),
685                d.is_layout_internal(),
686                d.is_inline_level(),
687            );
688            for _ in 0..4 {
689                assert_eq!(
690                    (
691                        d.creates_block_context(),
692                        d.creates_flex_context(),
693                        d.creates_table_context(),
694                        d.is_layout_internal(),
695                        d.is_inline_level(),
696                    ),
697                    snapshot,
698                    "predicates are not deterministic for {d:?}"
699                );
700            }
701        }
702    }
703
704    #[test]
705    fn predicates_are_usable_in_const_context() {
706        // All five predicates are `const fn`; regressing that is a breaking
707        // change for downstream `const` tables.
708        const BLOCK_CTX: bool = LayoutDisplay::FlowRoot.creates_block_context();
709        const FLEX_CTX: bool = LayoutDisplay::InlineFlex.creates_flex_context();
710        const TABLE_CTX: bool = LayoutDisplay::InlineTable.creates_table_context();
711        const INTERNAL: bool = LayoutDisplay::TableCell.is_layout_internal();
712        const INLINE: bool = LayoutDisplay::InlineGrid.is_inline_level();
713        const _: () = assert!(BLOCK_CTX && FLEX_CTX && TABLE_CTX && INTERNAL && INLINE);
714    }
715
716    // -----------------------------------------------------------------
717    // Round-trip: print_as_css_value <-> parse
718    // -----------------------------------------------------------------
719
720    #[test]
721    fn display_round_trips_through_its_css_serialization() {
722        for d in ALL_DISPLAY {
723            let printed = d.print_as_css_value();
724            assert_eq!(
725                parse_layout_display(&printed),
726                Ok(d),
727                "round-trip failed for {d:?} (printed as {printed:?})"
728            );
729        }
730    }
731
732    #[test]
733    fn float_round_trips_through_its_css_serialization() {
734        for f in ALL_FLOAT {
735            let printed = f.print_as_css_value();
736            assert_eq!(
737                parse_layout_float(&printed),
738                Ok(f),
739                "round-trip failed for {f:?} (printed as {printed:?})"
740            );
741        }
742    }
743
744    #[test]
745    fn display_serializations_are_unique_bare_idents() {
746        for (i, a) in ALL_DISPLAY.iter().enumerate() {
747            let printed = a.print_as_css_value();
748            assert!(!printed.is_empty(), "{a:?} serializes to the empty string");
749            assert_eq!(printed.trim(), printed, "{a:?} serializes with padding");
750            assert!(
751                printed
752                    .chars()
753                    .all(|c| c.is_ascii_lowercase() || c == '-'),
754                "{a:?} serializes to a non-ident {printed:?}"
755            );
756            // A duplicate keyword would make the round-trip above lossy.
757            for b in &ALL_DISPLAY[i + 1..] {
758                assert_ne!(
759                    printed,
760                    b.print_as_css_value(),
761                    "{a:?} and {b:?} share a CSS keyword"
762                );
763            }
764        }
765    }
766
767    #[test]
768    fn parse_then_print_is_idempotent() {
769        for keyword in [
770            "none",
771            "block",
772            "inline",
773            "inline-block",
774            "flex",
775            "inline-flex",
776            "table",
777            "inline-table",
778            "table-row-group",
779            "table-header-group",
780            "table-footer-group",
781            "table-row",
782            "table-column-group",
783            "table-column",
784            "table-cell",
785            "table-caption",
786            "list-item",
787            "run-in",
788            "marker",
789            "grid",
790            "inline-grid",
791            "flow-root",
792            "contents",
793        ] {
794            let parsed = parse_layout_display(keyword)
795                .unwrap_or_else(|e| panic!("{keyword:?} must parse, got {e}"));
796            assert_eq!(parsed.print_as_css_value(), keyword);
797        }
798        for keyword in ["left", "right", "none"] {
799            let parsed = parse_layout_float(keyword)
800                .unwrap_or_else(|e| panic!("{keyword:?} must parse, got {e}"));
801            assert_eq!(parsed.print_as_css_value(), keyword);
802        }
803    }
804
805    // -----------------------------------------------------------------
806    // parse_layout_display / parse_layout_float: malformed input
807    // -----------------------------------------------------------------
808
809    #[test]
810    fn parsers_reject_empty_and_whitespace_only_input() {
811        for blank in ["", " ", "   ", "\t", "\n", "\r\n", "\t\n\r ", "\u{c}", "\u{b}"] {
812            assert!(
813                parse_layout_display(blank).is_err(),
814                "display accepted blank {blank:?}"
815            );
816            assert!(
817                parse_layout_float(blank).is_err(),
818                "float accepted blank {blank:?}"
819            );
820        }
821        // The error carries the *trimmed* input, so a blank input reports "".
822        assert_eq!(
823            parse_layout_display("   "),
824            Err(LayoutDisplayParseError::InvalidValue(""))
825        );
826        assert_eq!(
827            parse_layout_float("\t\n"),
828            Err(LayoutFloatParseError::InvalidValue(""))
829        );
830    }
831
832    #[test]
833    fn display_parser_rejects_garbage_without_panicking() {
834        for bad in [
835            "invalid-value",
836            "display",
837            "display: block",
838            "block;",
839            "block ;",
840            "block!important",
841            "block block",
842            "inline block",
843            "inline_block",
844            "inline--block",
845            "-inline-block",
846            "inline-block-",
847            "-webkit-box",
848            "block flow",     // CSS Display 3 two-value syntax is not supported
849            "inline flow-root",
850            "table-column-groups",
851            "tablerow",
852            "\0",
853            "blo\0ck",
854            "block\0",
855            "inherit",
856            "initial",
857            "unset",
858            "revert",
859            "\\62 lock", // CSS ident escape
860            "/*block*/",
861            "\"block\"",
862        ] {
863            assert!(
864                parse_layout_display(bad).is_err(),
865                "display accepted garbage {bad:?}"
866            );
867        }
868    }
869
870    #[test]
871    fn float_parser_rejects_garbage_without_panicking() {
872        for bad in [
873            "center",
874            "float-left",
875            "left right",
876            "leftright",
877            "inline-start",
878            "inline-end",
879            "left;",
880            "left!important",
881            "\0",
882            "le\0ft",
883            "inherit",
884            "initial",
885            "footnote",
886        ] {
887            assert!(
888                parse_layout_float(bad).is_err(),
889                "float accepted garbage {bad:?}"
890            );
891        }
892    }
893
894    #[test]
895    fn parsers_are_ascii_case_sensitive() {
896        // NOTE (spec deviation, pinned as *current* behaviour): CSS keywords are
897        // ASCII case-insensitive, so `display: BLOCK` is valid CSS. These
898        // parsers match exactly; the requirement asserted here is only that they
899        // reject deterministically instead of panicking.
900        for bad in ["BLOCK", "Block", "bLoCk", "INLINE-FLEX", "Table-Cell"] {
901            assert!(
902                parse_layout_display(bad).is_err(),
903                "display unexpectedly accepted {bad:?}"
904            );
905        }
906        for bad in ["LEFT", "Left", "RIGHT", "None"] {
907            assert!(
908                parse_layout_float(bad).is_err(),
909                "float unexpectedly accepted {bad:?}"
910            );
911        }
912    }
913
914    #[test]
915    fn parsers_trim_leading_and_trailing_whitespace_but_not_junk() {
916        assert_eq!(
917            parse_layout_display("  inline-flex  "),
918            Ok(LayoutDisplay::InlineFlex)
919        );
920        assert_eq!(
921            parse_layout_display("\n\t table-cell \r\n"),
922            Ok(LayoutDisplay::TableCell)
923        );
924        assert_eq!(parse_layout_float("\n right \t"), Ok(LayoutFloat::Right));
925
926        // Interior whitespace and trailing junk are *not* forgiven.
927        assert!(parse_layout_display("inline - flex").is_err());
928        assert!(parse_layout_display("table-cell;garbage").is_err());
929        assert!(parse_layout_float("right;garbage").is_err());
930    }
931
932    #[test]
933    fn parsers_trim_unicode_whitespace_but_not_zero_width_characters() {
934        // `str::trim` uses the Unicode `White_Space` property, a strict superset
935        // of CSS whitespace (space, tab, LF, CR, FF). NBSP / ideographic space /
936        // line separator therefore pass as padding - pinned as current behaviour.
937        assert_eq!(
938            parse_layout_display("\u{a0}block\u{a0}"),
939            Ok(LayoutDisplay::Block)
940        );
941        assert_eq!(
942            parse_layout_display("\u{3000}flex\u{2028}"),
943            Ok(LayoutDisplay::Flex)
944        );
945        assert_eq!(parse_layout_float("\u{a0}left\u{a0}"), Ok(LayoutFloat::Left));
946
947        // U+200B ZERO WIDTH SPACE and U+FEFF are *not* `White_Space`, so they
948        // survive the trim and must make the value invalid.
949        assert!(parse_layout_display("\u{200b}block").is_err());
950        assert!(parse_layout_display("block\u{feff}").is_err());
951        assert!(parse_layout_float("\u{200b}left").is_err());
952    }
953
954    #[test]
955    fn parsers_survive_non_ascii_and_multibyte_input() {
956        for bad in [
957            "\u{1F600}",
958            "block\u{1F600}",
959            "\u{1F3F3}\u{FE0F}\u{200D}\u{1F308}", // ZWJ emoji sequence
960            "blocke\u{301}",                      // combining acute accent
961            "\u{202E}block",                      // RTL override
962            "block",                          // fullwidth latin
963            "блок",
964            "块",
965            "\u{0}\u{1}\u{2}",
966            "\u{10FFFF}",
967        ] {
968            assert!(
969                parse_layout_display(bad).is_err(),
970                "display accepted unicode garbage {bad:?}"
971            );
972            assert!(
973                parse_layout_float(bad).is_err(),
974                "float accepted unicode garbage {bad:?}"
975            );
976        }
977    }
978
979    #[test]
980    fn parsers_reject_boundary_numeric_strings() {
981        let numeric = [
982            "0".to_string(),
983            "-0".to_string(),
984            "+0".to_string(),
985            "1".to_string(),
986            "-1".to_string(),
987            i64::MAX.to_string(),
988            i64::MIN.to_string(),
989            u64::MAX.to_string(),
990            format!("{}", f64::MAX),
991            format!("{}", f64::MIN_POSITIVE),
992            "NaN".to_string(),
993            "nan".to_string(),
994            "inf".to_string(),
995            "-inf".to_string(),
996            "infinity".to_string(),
997            "1e400".to_string(),
998            "-1e-400".to_string(),
999            "0x7fffffffffffffff".to_string(),
1000            "99999999999999999999999999999999".to_string(),
1001        ];
1002        for n in &numeric {
1003            assert!(
1004                parse_layout_display(n).is_err(),
1005                "display accepted number {n:?}"
1006            );
1007            assert!(parse_layout_float(n).is_err(), "float accepted number {n:?}");
1008        }
1009    }
1010
1011    #[test]
1012    fn parsers_do_not_hang_on_extremely_long_input() {
1013        const LONG: usize = 1_000_000;
1014
1015        let junk = "x".repeat(LONG);
1016        assert!(parse_layout_display(&junk).is_err());
1017        assert!(parse_layout_float(&junk).is_err());
1018
1019        // A keyword that is *almost* right, one megabyte long.
1020        let near_miss = format!("block{}", "k".repeat(LONG));
1021        assert!(parse_layout_display(&near_miss).is_err());
1022
1023        // Whitespace-only, one megabyte long: trims to "" and must still be Err.
1024        let blanks = " ".repeat(LONG);
1025        assert!(parse_layout_display(&blanks).is_err());
1026        assert!(parse_layout_float(&blanks).is_err());
1027
1028        // A valid keyword buried in a megabyte of padding on each side.
1029        let padded = format!("{blanks}table-caption{blanks}");
1030        assert_eq!(
1031            parse_layout_display(&padded),
1032            Ok(LayoutDisplay::TableCaption)
1033        );
1034        let padded_float = format!("{blanks}left{blanks}");
1035        assert_eq!(parse_layout_float(&padded_float), Ok(LayoutFloat::Left));
1036    }
1037
1038    #[test]
1039    fn parsers_do_not_stack_overflow_on_deeply_nested_input() {
1040        const DEPTH: usize = 10_000;
1041        let nested = format!("{}{}", "(".repeat(DEPTH), ")".repeat(DEPTH));
1042        assert!(parse_layout_display(&nested).is_err());
1043        assert!(parse_layout_float(&nested).is_err());
1044
1045        let nested_fn = format!("{}block{}", "calc(".repeat(DEPTH), ")".repeat(DEPTH));
1046        assert!(parse_layout_display(&nested_fn).is_err());
1047    }
1048
1049    // -----------------------------------------------------------------
1050    // Parse errors: payload, formatting, to_contained / to_shared
1051    // -----------------------------------------------------------------
1052
1053    #[test]
1054    fn display_error_reports_the_trimmed_input() {
1055        assert_eq!(
1056            parse_layout_display("  bogus  "),
1057            Err(LayoutDisplayParseError::InvalidValue("bogus"))
1058        );
1059        // Unicode padding is trimmed off the reported value as well.
1060        assert_eq!(
1061            parse_layout_display("\u{a0}bogus\u{3000}"),
1062            Err(LayoutDisplayParseError::InvalidValue("bogus"))
1063        );
1064    }
1065
1066    #[test]
1067    fn float_error_reports_the_trimmed_input() {
1068        assert_eq!(
1069            parse_layout_float("  bogus  "),
1070            Err(LayoutFloatParseError::InvalidValue("bogus"))
1071        );
1072    }
1073
1074    #[test]
1075    fn parse_errors_display_and_debug_identically() {
1076        let d = parse_layout_display("bogus").unwrap_err();
1077        assert_eq!(format!("{d}"), "Invalid display value: \"bogus\"");
1078        assert_eq!(format!("{d:?}"), format!("{d}"));
1079
1080        let f = parse_layout_float("bogus").unwrap_err();
1081        assert_eq!(format!("{f}"), "Invalid float value: \"bogus\"");
1082        assert_eq!(format!("{f:?}"), format!("{f}"));
1083
1084        // An empty payload must still format without panicking.
1085        let empty = parse_layout_display("").unwrap_err();
1086        assert_eq!(format!("{empty}"), "Invalid display value: \"\"");
1087    }
1088
1089    #[test]
1090    fn display_error_to_contained_to_shared_round_trips() {
1091        let long = "x".repeat(4096);
1092        let payloads: [&str; 8] = [
1093            "",
1094            " ",
1095            "bogus",
1096            "\u{1F600}",
1097            "a\0b",
1098            "block block",
1099            "\"quoted\"",
1100            &long,
1101        ];
1102        for payload in payloads {
1103            let shared = LayoutDisplayParseError::InvalidValue(payload);
1104            let owned = shared.to_contained();
1105            assert_eq!(
1106                owned,
1107                LayoutDisplayParseErrorOwned::InvalidValue(payload.to_string().into()),
1108                "to_contained lost the payload {payload:?}"
1109            );
1110            assert_eq!(
1111                owned.to_shared(),
1112                shared,
1113                "to_shared did not restore the payload {payload:?}"
1114            );
1115            // Byte-for-byte, not just "equal enough".
1116            let LayoutDisplayParseErrorOwned::InvalidValue(s) = &owned;
1117            assert_eq!(s.as_str(), payload);
1118        }
1119    }
1120
1121    #[test]
1122    fn float_error_to_contained_to_shared_round_trips() {
1123        let long = "y".repeat(4096);
1124        let payloads: [&str; 6] = ["", " ", "bogus", "\u{1F600}", "a\0b", &long];
1125        for payload in payloads {
1126            let shared = LayoutFloatParseError::InvalidValue(payload);
1127            let owned = shared.to_contained();
1128            assert_eq!(
1129                owned,
1130                LayoutFloatParseErrorOwned::InvalidValue(payload.to_string().into()),
1131                "to_contained lost the payload {payload:?}"
1132            );
1133            assert_eq!(
1134                owned.to_shared(),
1135                shared,
1136                "to_shared did not restore the payload {payload:?}"
1137            );
1138            let LayoutFloatParseErrorOwned::InvalidValue(s) = &owned;
1139            assert_eq!(s.as_str(), payload);
1140        }
1141    }
1142
1143    #[test]
1144    fn error_conversions_survive_a_borrowed_owned_borrowed_cycle() {
1145        // The owned form must outlive the &str it came from; converting back
1146        // must not resurrect a dangling borrow.
1147        let owned = {
1148            let input = format!("{}-bogus", "very-long-".repeat(64));
1149            parse_layout_display(&input).unwrap_err().to_contained()
1150        };
1151        let shared = owned.to_shared();
1152        assert_eq!(shared.to_contained(), owned);
1153        assert!(format!("{shared}").starts_with("Invalid display value: \"very-long-"));
1154    }
1155
1156    // -----------------------------------------------------------------
1157    // Positive controls
1158    // -----------------------------------------------------------------
1159
1160    #[test]
1161    fn valid_minimal_inputs_parse() {
1162        assert_eq!(parse_layout_display("none"), Ok(LayoutDisplay::None));
1163        assert_eq!(parse_layout_display("contents"), Ok(LayoutDisplay::Contents));
1164        assert_eq!(parse_layout_display("run-in"), Ok(LayoutDisplay::RunIn));
1165        assert_eq!(parse_layout_display("marker"), Ok(LayoutDisplay::Marker));
1166        assert_eq!(parse_layout_float("none"), Ok(LayoutFloat::None));
1167        assert_eq!(parse_layout_float("left"), Ok(LayoutFloat::Left));
1168    }
1169}