1
//! CSS properties for `display` and `float`.
2

            
3
use alloc::string::{String, ToString};
4
use crate::corety::AzString;
5

            
6
use 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)]
13
pub 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

            
61
impl LayoutDisplay {
62
    /// Returns true if this display type establishes a block formatting context.
63
12461
    #[must_use] pub const fn creates_block_context(&self) -> bool {
64
104
        matches!(
65
12461
            self,
66
            Self::Block
67
                | Self::FlowRoot
68
                | Self::Flex
69
                | Self::Grid
70
                | Self::Table
71
                | Self::ListItem
72
        )
73
12461
    }
74

            
75
    /// Returns true if this display type establishes a flex formatting context.
76
164
    #[must_use] pub const fn creates_flex_context(&self) -> bool {
77
164
        matches!(self, Self::Flex | Self::InlineFlex)
78
164
    }
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
517
    #[must_use] pub const fn creates_table_context(&self) -> bool {
83
517
        matches!(self, Self::Table | Self::InlineTable)
84
517
    }
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
235555
    #[must_use] pub const fn is_layout_internal(&self) -> bool {
90
233926
        matches!(
91
235555
            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
235555
    }
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
158
    #[must_use] pub const fn is_inline_level(&self) -> bool {
108
123
        matches!(
109
158
            self,
110
            Self::Inline
111
                | Self::InlineBlock
112
                | Self::InlineFlex
113
                | Self::InlineTable
114
                | Self::InlineGrid
115
        )
116
158
    }
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
47
    #[must_use] pub const fn is_atomic_inline(&self) -> bool {
127
39
        matches!(
128
47
            self,
129
            Self::InlineBlock | Self::InlineFlex | Self::InlineTable | Self::InlineGrid
130
        )
131
47
    }
132
}
133

            
134
// +spec:display-property:cabaec - serialization uses short display keywords per CSSOM precedence rules
135
impl PrintAsCssValue for LayoutDisplay {
136
6256
    fn print_as_css_value(&self) -> String {
137
6256
        String::from(match self {
138
3
            Self::None => "none",
139
5038
            Self::Block => "block",
140
785
            Self::Inline => "inline",
141
6
            Self::InlineBlock => "inline-block",
142
127
            Self::Flex => "flex",
143
8
            Self::InlineFlex => "inline-flex",
144
9
            Self::Table => "table",
145
10
            Self::InlineTable => "inline-table",
146
11
            Self::TableRowGroup => "table-row-group",
147
12
            Self::TableHeaderGroup => "table-header-group",
148
13
            Self::TableFooterGroup => "table-footer-group",
149
14
            Self::TableRow => "table-row",
150
15
            Self::TableColumnGroup => "table-column-group",
151
16
            Self::TableColumn => "table-column",
152
17
            Self::TableCell => "table-cell",
153
18
            Self::TableCaption => "table-caption",
154
20
            Self::ListItem => "list-item",
155
21
            Self::RunIn => "run-in",
156
22
            Self::Marker => "marker",
157
19
            Self::FlowRoot => "flow-root",
158
23
            Self::Grid => "grid",
159
24
            Self::InlineGrid => "inline-grid",
160
25
            Self::Contents => "contents",
161
        })
162
6256
    }
163
}
164

            
165
/// Represents a `float` attribute
166
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
167
#[repr(C)]
168
pub enum LayoutFloat {
169
    Left,
170
    Right,
171
    #[default]
172
    None,
173
}
174

            
175
impl PrintAsCssValue for LayoutFloat {
176
6
    fn print_as_css_value(&self) -> String {
177
6
        String::from(match self {
178
2
            Self::Left => "left",
179
2
            Self::Right => "right",
180
2
            Self::None => "none",
181
        })
182
6
    }
183
}
184

            
185
// --- PARSERS ---
186

            
187
#[cfg(feature = "parser")]
188
#[derive(Clone, PartialEq, Eq)]
189
pub enum LayoutDisplayParseError<'a> {
190
    InvalidValue(&'a str),
191
}
192

            
193
#[cfg(feature = "parser")]
194
impl_debug_as_display!(LayoutDisplayParseError<'a>);
195

            
196
#[cfg(feature = "parser")]
197
impl_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)]
204
pub enum LayoutDisplayParseErrorOwned {
205
    InvalidValue(AzString),
206
}
207

            
208
#[cfg(feature = "parser")]
209
impl LayoutDisplayParseError<'_> {
210
10
    #[must_use] pub fn to_contained(&self) -> LayoutDisplayParseErrorOwned {
211
10
        match self {
212
10
            Self::InvalidValue(s) => LayoutDisplayParseErrorOwned::InvalidValue((*s).to_string().into()),
213
        }
214
10
    }
215
}
216

            
217
#[cfg(feature = "parser")]
218
impl LayoutDisplayParseErrorOwned {
219
9
    #[must_use] pub fn to_shared(&self) -> LayoutDisplayParseError<'_> {
220
9
        match self {
221
9
            Self::InvalidValue(s) => LayoutDisplayParseError::InvalidValue(s.as_str()),
222
        }
223
9
    }
224
}
225

            
226
#[cfg(feature = "parser")]
227
/// # Errors
228
///
229
/// Returns an error if `input` is not a valid CSS `display` value.
230
30981
pub fn parse_layout_display(
231
30981
    input: &str,
232
30981
) -> Result<LayoutDisplay, LayoutDisplayParseError<'_>> {
233
30981
    let input = input.trim();
234
30981
    match input {
235
30981
        "none" => Ok(LayoutDisplay::None),
236
30893
        "block" => Ok(LayoutDisplay::Block),
237
22173
        "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
21330
        "inline-block" => Ok(LayoutDisplay::InlineBlock),
240
20931
        "flex" => Ok(LayoutDisplay::Flex),
241
1153
        "inline-flex" => Ok(LayoutDisplay::InlineFlex),
242
1112
        "table" => Ok(LayoutDisplay::Table),
243
977
        "inline-table" => Ok(LayoutDisplay::InlineTable),
244
950
        "table-row-group" => Ok(LayoutDisplay::TableRowGroup),
245
923
        "table-header-group" => Ok(LayoutDisplay::TableHeaderGroup),
246
896
        "table-footer-group" => Ok(LayoutDisplay::TableFooterGroup),
247
881
        "table-row" => Ok(LayoutDisplay::TableRow),
248
770
        "table-column-group" => Ok(LayoutDisplay::TableColumnGroup),
249
755
        "table-column" => Ok(LayoutDisplay::TableColumn),
250
717
        "table-cell" => Ok(LayoutDisplay::TableCell),
251
521
        "table-caption" => Ok(LayoutDisplay::TableCaption),
252
481
        "list-item" => Ok(LayoutDisplay::ListItem),
253
298
        "run-in" => Ok(LayoutDisplay::RunIn),
254
295
        "marker" => Ok(LayoutDisplay::Marker),
255
292
        "grid" => Ok(LayoutDisplay::Grid),
256
195
        "inline-grid" => Ok(LayoutDisplay::InlineGrid),
257
168
        "flow-root" => Ok(LayoutDisplay::FlowRoot),
258
129
        "contents" => Ok(LayoutDisplay::Contents),
259
90
        _ => Err(LayoutDisplayParseError::InvalidValue(input)),
260
    }
261
30981
}
262

            
263
#[cfg(feature = "parser")]
264
#[derive(Clone, PartialEq, Eq)]
265
pub enum LayoutFloatParseError<'a> {
266
    InvalidValue(&'a str),
267
}
268

            
269
#[cfg(feature = "parser")]
270
impl_debug_as_display!(LayoutFloatParseError<'a>);
271

            
272
#[cfg(feature = "parser")]
273
impl_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)]
280
pub enum LayoutFloatParseErrorOwned {
281
    InvalidValue(AzString),
282
}
283

            
284
#[cfg(feature = "parser")]
285
impl LayoutFloatParseError<'_> {
286
6
    #[must_use] pub fn to_contained(&self) -> LayoutFloatParseErrorOwned {
287
6
        match self {
288
6
            Self::InvalidValue(s) => LayoutFloatParseErrorOwned::InvalidValue((*s).to_string().into()),
289
        }
290
6
    }
291
}
292

            
293
#[cfg(feature = "parser")]
294
impl LayoutFloatParseErrorOwned {
295
6
    #[must_use] pub fn to_shared(&self) -> LayoutFloatParseError<'_> {
296
6
        match self {
297
6
            Self::InvalidValue(s) => LayoutFloatParseError::InvalidValue(s.as_str()),
298
        }
299
6
    }
300
}
301

            
302
#[cfg(feature = "parser")]
303
/// # Errors
304
///
305
/// Returns an error if `input` is not a valid CSS `float` value.
306
549
pub fn parse_layout_float(input: &str) -> Result<LayoutFloat, LayoutFloatParseError<'_>> {
307
549
    let input = input.trim();
308
549
    match input {
309
549
        "left" => Ok(LayoutFloat::Left),
310
219
        "right" => Ok(LayoutFloat::Right),
311
82
        "none" => Ok(LayoutFloat::None),
312
66
        _ => Err(LayoutFloatParseError::InvalidValue(input)),
313
    }
314
549
}
315

            
316
#[cfg(all(test, feature = "parser"))]
317
mod 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
1
    fn test_parse_layout_display() {
323
1
        assert_eq!(parse_layout_display("block").unwrap(), LayoutDisplay::Block);
324
1
        assert_eq!(
325
1
            parse_layout_display("inline").unwrap(),
326
            LayoutDisplay::Inline
327
        );
328
1
        assert_eq!(
329
1
            parse_layout_display("inline-block").unwrap(),
330
            LayoutDisplay::InlineBlock
331
        );
332
1
        assert_eq!(parse_layout_display("flex").unwrap(), LayoutDisplay::Flex);
333
1
        assert_eq!(
334
1
            parse_layout_display("inline-flex").unwrap(),
335
            LayoutDisplay::InlineFlex
336
        );
337
1
        assert_eq!(parse_layout_display("grid").unwrap(), LayoutDisplay::Grid);
338
1
        assert_eq!(
339
1
            parse_layout_display("inline-grid").unwrap(),
340
            LayoutDisplay::InlineGrid
341
        );
342
1
        assert_eq!(parse_layout_display("none").unwrap(), LayoutDisplay::None);
343
1
        assert_eq!(
344
1
            parse_layout_display("flow-root").unwrap(),
345
            LayoutDisplay::FlowRoot
346
        );
347
1
        assert_eq!(
348
1
            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
1
        assert!(parse_layout_display("inherit").is_err());
354
1
        assert!(parse_layout_display("initial").is_err());
355

            
356
        // Table values
357
1
        assert_eq!(parse_layout_display("table").unwrap(), LayoutDisplay::Table);
358
1
        assert_eq!(
359
1
            parse_layout_display("inline-table").unwrap(),
360
            LayoutDisplay::InlineTable
361
        );
362
1
        assert_eq!(
363
1
            parse_layout_display("table-row").unwrap(),
364
            LayoutDisplay::TableRow
365
        );
366
1
        assert_eq!(
367
1
            parse_layout_display("table-cell").unwrap(),
368
            LayoutDisplay::TableCell
369
        );
370
1
        assert_eq!(
371
1
            parse_layout_display("table-caption").unwrap(),
372
            LayoutDisplay::TableCaption
373
        );
374
1
        assert_eq!(
375
1
            parse_layout_display("table-column-group").unwrap(),
376
            LayoutDisplay::TableColumnGroup
377
        );
378
1
        assert_eq!(
379
1
            parse_layout_display("table-header-group").unwrap(),
380
            LayoutDisplay::TableHeaderGroup
381
        );
382
1
        assert_eq!(
383
1
            parse_layout_display("table-footer-group").unwrap(),
384
            LayoutDisplay::TableFooterGroup
385
        );
386
1
        assert_eq!(
387
1
            parse_layout_display("table-row-group").unwrap(),
388
            LayoutDisplay::TableRowGroup
389
        );
390

            
391
        // Whitespace
392
1
        assert_eq!(
393
1
            parse_layout_display("  inline-flex  ").unwrap(),
394
            LayoutDisplay::InlineFlex
395
        );
396

            
397
        // Invalid values
398
1
        assert!(parse_layout_display("invalid-value").is_err());
399
1
        assert!(parse_layout_display("").is_err());
400
1
        assert!(parse_layout_display("display").is_err());
401
1
    }
402

            
403
    #[test]
404
1
    fn test_parse_layout_float() {
405
1
        assert_eq!(parse_layout_float("left").unwrap(), LayoutFloat::Left);
406
1
        assert_eq!(parse_layout_float("right").unwrap(), LayoutFloat::Right);
407
1
        assert_eq!(parse_layout_float("none").unwrap(), LayoutFloat::None);
408

            
409
        // Whitespace
410
1
        assert_eq!(parse_layout_float("  right  ").unwrap(), LayoutFloat::Right);
411

            
412
        // Invalid values
413
1
        assert!(parse_layout_float("center").is_err());
414
1
        assert!(parse_layout_float("").is_err());
415
1
        assert!(parse_layout_float("float-left").is_err());
416
1
    }
417
}
418

            
419
#[cfg(all(test, feature = "parser"))]
420
mod 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(),
            "99999999999999999999999999999999".to_string(),
        ];
        for n in &numeric {
            assert!(
                parse_layout_display(n).is_err(),
                "display accepted number {n:?}"
            );
            assert!(parse_layout_float(n).is_err(), "float accepted number {n:?}");
        }
    }
    #[test]
    fn parsers_do_not_hang_on_extremely_long_input() {
        const LONG: usize = 1_000_000;
        let junk = "x".repeat(LONG);
        assert!(parse_layout_display(&junk).is_err());
        assert!(parse_layout_float(&junk).is_err());
        // A keyword that is *almost* right, one megabyte long.
        let near_miss = format!("block{}", "k".repeat(LONG));
        assert!(parse_layout_display(&near_miss).is_err());
        // Whitespace-only, one megabyte long: trims to "" and must still be Err.
        let blanks = " ".repeat(LONG);
        assert!(parse_layout_display(&blanks).is_err());
        assert!(parse_layout_float(&blanks).is_err());
        // A valid keyword buried in a megabyte of padding on each side.
        let padded = format!("{blanks}table-caption{blanks}");
        assert_eq!(
            parse_layout_display(&padded),
            Ok(LayoutDisplay::TableCaption)
        );
        let padded_float = format!("{blanks}left{blanks}");
        assert_eq!(parse_layout_float(&padded_float), Ok(LayoutFloat::Left));
    }
    #[test]
    fn parsers_do_not_stack_overflow_on_deeply_nested_input() {
        const DEPTH: usize = 10_000;
        let nested = format!("{}{}", "(".repeat(DEPTH), ")".repeat(DEPTH));
        assert!(parse_layout_display(&nested).is_err());
        assert!(parse_layout_float(&nested).is_err());
        let nested_fn = format!("{}block{}", "calc(".repeat(DEPTH), ")".repeat(DEPTH));
        assert!(parse_layout_display(&nested_fn).is_err());
    }
    // -----------------------------------------------------------------
    // Parse errors: payload, formatting, to_contained / to_shared
    // -----------------------------------------------------------------
    #[test]
    fn display_error_reports_the_trimmed_input() {
        assert_eq!(
            parse_layout_display("  bogus  "),
            Err(LayoutDisplayParseError::InvalidValue("bogus"))
        );
        // Unicode padding is trimmed off the reported value as well.
        assert_eq!(
            parse_layout_display("\u{a0}bogus\u{3000}"),
            Err(LayoutDisplayParseError::InvalidValue("bogus"))
        );
    }
    #[test]
    fn float_error_reports_the_trimmed_input() {
        assert_eq!(
            parse_layout_float("  bogus  "),
            Err(LayoutFloatParseError::InvalidValue("bogus"))
        );
    }
    #[test]
    fn parse_errors_display_and_debug_identically() {
        let d = parse_layout_display("bogus").unwrap_err();
        assert_eq!(format!("{d}"), "Invalid display value: \"bogus\"");
        assert_eq!(format!("{d:?}"), format!("{d}"));
        let f = parse_layout_float("bogus").unwrap_err();
        assert_eq!(format!("{f}"), "Invalid float value: \"bogus\"");
        assert_eq!(format!("{f:?}"), format!("{f}"));
        // An empty payload must still format without panicking.
        let empty = parse_layout_display("").unwrap_err();
        assert_eq!(format!("{empty}"), "Invalid display value: \"\"");
    }
    #[test]
    fn display_error_to_contained_to_shared_round_trips() {
        let long = "x".repeat(4096);
        let payloads: [&str; 8] = [
            "",
            " ",
            "bogus",
            "\u{1F600}",
            "a\0b",
            "block block",
            "\"quoted\"",
            &long,
        ];
        for payload in payloads {
            let shared = LayoutDisplayParseError::InvalidValue(payload);
            let owned = shared.to_contained();
            assert_eq!(
                owned,
                LayoutDisplayParseErrorOwned::InvalidValue(payload.to_string().into()),
                "to_contained lost the payload {payload:?}"
            );
            assert_eq!(
                owned.to_shared(),
                shared,
                "to_shared did not restore the payload {payload:?}"
            );
            // Byte-for-byte, not just "equal enough".
            let LayoutDisplayParseErrorOwned::InvalidValue(s) = &owned;
            assert_eq!(s.as_str(), payload);
        }
    }
    #[test]
    fn float_error_to_contained_to_shared_round_trips() {
        let long = "y".repeat(4096);
        let payloads: [&str; 6] = ["", " ", "bogus", "\u{1F600}", "a\0b", &long];
        for payload in payloads {
            let shared = LayoutFloatParseError::InvalidValue(payload);
            let owned = shared.to_contained();
            assert_eq!(
                owned,
                LayoutFloatParseErrorOwned::InvalidValue(payload.to_string().into()),
                "to_contained lost the payload {payload:?}"
            );
            assert_eq!(
                owned.to_shared(),
                shared,
                "to_shared did not restore the payload {payload:?}"
            );
            let LayoutFloatParseErrorOwned::InvalidValue(s) = &owned;
            assert_eq!(s.as_str(), payload);
        }
    }
    #[test]
    fn error_conversions_survive_a_borrowed_owned_borrowed_cycle() {
        // The owned form must outlive the &str it came from; converting back
        // must not resurrect a dangling borrow.
        let owned = {
            let input = format!("{}-bogus", "very-long-".repeat(64));
            parse_layout_display(&input).unwrap_err().to_contained()
        };
        let shared = owned.to_shared();
        assert_eq!(shared.to_contained(), owned);
        assert!(format!("{shared}").starts_with("Invalid display value: \"very-long-"));
    }
    // -----------------------------------------------------------------
    // Positive controls
    // -----------------------------------------------------------------
    #[test]
    fn valid_minimal_inputs_parse() {
        assert_eq!(parse_layout_display("none"), Ok(LayoutDisplay::None));
        assert_eq!(parse_layout_display("contents"), Ok(LayoutDisplay::Contents));
        assert_eq!(parse_layout_display("run-in"), Ok(LayoutDisplay::RunIn));
        assert_eq!(parse_layout_display("marker"), Ok(LayoutDisplay::Marker));
        assert_eq!(parse_layout_float("none"), Ok(LayoutFloat::None));
        assert_eq!(parse_layout_float("left"), Ok(LayoutFloat::Left));
    }
}