Skip to main content

azul_css/props/layout/
display.rs

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