1
//! CSS properties for positioning elements: `position`, `top`, `right`,
2
//! `bottom`, `left`, and `z-index`. Types defined here are consumed by the
3
//! layout solver to resolve positioned elements.
4

            
5
use alloc::string::{String, ToString};
6
use crate::corety::AzString;
7

            
8
#[cfg(feature = "parser")]
9
use crate::props::basic::pixel::parse_pixel_value;
10
use crate::props::{
11
    basic::pixel::{CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue},
12
    formatter::PrintAsCssValue,
13
    macros::PixelValueTaker,
14
};
15

            
16
// --- LayoutPosition ---
17

            
18
/// Represents a `position` attribute - default: `Static`
19
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
20
#[repr(C)]
21
#[derive(Default)]
22
pub enum LayoutPosition {
23
    #[default]
24
    Static,
25
    Relative,
26
    Absolute,
27
    Fixed,
28
    Sticky,
29
}
30

            
31
impl LayoutPosition {
32
8327
    #[must_use] pub fn is_positioned(&self) -> bool {
33
8327
        *self != Self::Static
34
8327
    }
35
}
36

            
37

            
38
impl PrintAsCssValue for LayoutPosition {
39
10
    fn print_as_css_value(&self) -> String {
40
10
        String::from(match self {
41
2
            Self::Static => "static",
42
2
            Self::Relative => "relative",
43
2
            Self::Absolute => "absolute",
44
2
            Self::Fixed => "fixed",
45
2
            Self::Sticky => "sticky",
46
        })
47
10
    }
48
}
49

            
50
impl_enum_fmt!(LayoutPosition, Static, Fixed, Absolute, Relative, Sticky);
51

            
52
// -- Parser for LayoutPosition
53

            
54
#[derive(Clone, PartialEq, Eq)]
55
pub enum LayoutPositionParseError<'a> {
56
    InvalidValue(&'a str),
57
}
58

            
59
impl_debug_as_display!(LayoutPositionParseError<'a>);
60
impl_display! { LayoutPositionParseError<'a>, {
61
    InvalidValue(val) => format!("Invalid position value: \"{}\"", val),
62
}}
63

            
64
#[derive(Debug, Clone, PartialEq, Eq)]
65
#[repr(C, u8)]
66
pub enum LayoutPositionParseErrorOwned {
67
    InvalidValue(AzString),
68
}
69

            
70
impl LayoutPositionParseError<'_> {
71
13
    #[must_use] pub fn to_contained(&self) -> LayoutPositionParseErrorOwned {
72
13
        match self {
73
13
            LayoutPositionParseError::InvalidValue(s) => {
74
13
                LayoutPositionParseErrorOwned::InvalidValue((*s).to_string().into())
75
            }
76
        }
77
13
    }
78
}
79

            
80
impl LayoutPositionParseErrorOwned {
81
13
    #[must_use] pub fn to_shared(&self) -> LayoutPositionParseError<'_> {
82
13
        match self {
83
13
            Self::InvalidValue(s) => {
84
13
                LayoutPositionParseError::InvalidValue(s.as_str())
85
            }
86
        }
87
13
    }
88
}
89

            
90
#[cfg(feature = "parser")]
91
/// # Errors
92
///
93
/// Returns an error if `input` is not a valid CSS `position` value.
94
22791
pub fn parse_layout_position(
95
22791
    input: &str,
96
22791
) -> Result<LayoutPosition, LayoutPositionParseError<'_>> {
97
22791
    let input = input.trim();
98
22791
    match input {
99
22791
        "static" => Ok(LayoutPosition::Static),
100
22739
        "relative" => Ok(LayoutPosition::Relative),
101
21154
        "absolute" => Ok(LayoutPosition::Absolute),
102
414
        "fixed" => Ok(LayoutPosition::Fixed),
103
281
        "sticky" => Ok(LayoutPosition::Sticky),
104
49
        _ => Err(LayoutPositionParseError::InvalidValue(input)),
105
    }
106
22791
}
107

            
108
// --- Offset Properties (top, right, bottom, left) ---
109

            
110
macro_rules! define_position_property {
111
    ($struct_name:ident) => {
112
        #[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
113
        #[repr(C)]
114
        pub struct $struct_name {
115
            pub inner: PixelValue,
116
        }
117

            
118
        impl ::core::fmt::Debug for $struct_name {
119
            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
120
                write!(f, "{}", self.inner)
121
            }
122
        }
123

            
124
        impl PixelValueTaker for $struct_name {
125
            fn from_pixel_value(inner: PixelValue) -> Self {
126
                Self { inner }
127
            }
128
        }
129

            
130
        impl_pixel_value!($struct_name);
131

            
132
        impl PrintAsCssValue for $struct_name {
133
66
            fn print_as_css_value(&self) -> String {
134
66
                format!("{}", self.inner)
135
66
            }
136
        }
137
    };
138
}
139

            
140
/// Represents the CSS `top` offset property for positioned elements.
141
define_position_property!(LayoutTop);
142
/// Represents the CSS `right` offset property for positioned elements.
143
define_position_property!(LayoutRight);
144
/// Represents the CSS `bottom` offset property for positioned elements.
145
define_position_property!(LayoutInsetBottom);
146
/// Represents the CSS `left` offset property for positioned elements.
147
define_position_property!(LayoutLeft);
148

            
149
// -- Parse error types and parsers for offset properties (top, right, bottom, left)
150

            
151
macro_rules! define_offset_parse_error {
152
    ($struct_name:ident, $error_name:ident, $error_owned_name:ident, $parse_fn:ident) => {
153
        #[derive(Clone, PartialEq, Eq)]
154
        pub enum $error_name<'a> {
155
            PixelValue(CssPixelValueParseError<'a>),
156
        }
157
        impl_debug_as_display!($error_name<'a>);
158
        impl_display! { $error_name<'a>, { PixelValue(e) => format!("{}", e), }}
159
        impl_from!(CssPixelValueParseError<'a>, $error_name::PixelValue);
160

            
161
        #[derive(Debug, Clone, PartialEq, Eq)]
162
        #[repr(C, u8)]
163
        pub enum $error_owned_name {
164
            PixelValue(CssPixelValueParseErrorOwned),
165
        }
166
        impl $error_name<'_> {
167
8
            #[must_use] pub fn to_contained(&self) -> $error_owned_name {
168
8
                match self {
169
8
                    $error_name::PixelValue(e) => {
170
8
                        $error_owned_name::PixelValue(e.to_contained())
171
                    }
172
                }
173
8
            }
174
        }
175
        impl $error_owned_name {
176
8
            #[must_use] pub fn to_shared(&self) -> $error_name<'_> {
177
8
                match self {
178
8
                    $error_owned_name::PixelValue(e) => {
179
8
                        $error_name::PixelValue(e.to_shared())
180
                    }
181
                }
182
8
            }
183
        }
184

            
185
        #[cfg(feature = "parser")]
186
        /// # Errors
187
        ///
188
        /// Returns an error if `input` is not a valid CSS value for this property.
189
42769
        pub fn $parse_fn(input: &str) -> Result<$struct_name, $error_name<'_>> {
190
42769
            parse_pixel_value(input)
191
42769
                .map(|v| $struct_name { inner: v })
192
42769
                .map_err(Into::into)
193
42769
        }
194
    };
195
}
196

            
197
define_offset_parse_error!(LayoutTop, LayoutTopParseError, LayoutTopParseErrorOwned, parse_layout_top);
198
define_offset_parse_error!(LayoutRight, LayoutRightParseError, LayoutRightParseErrorOwned, parse_layout_right);
199
define_offset_parse_error!(LayoutInsetBottom, LayoutInsetBottomParseError, LayoutInsetBottomParseErrorOwned, parse_layout_bottom);
200
define_offset_parse_error!(LayoutLeft, LayoutLeftParseError, LayoutLeftParseErrorOwned, parse_layout_left);
201

            
202
// --- LayoutZIndex ---
203

            
204
/// Represents a `z-index` attribute - controls stacking order of positioned elements
205
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
206
#[repr(C, u8)]
207
#[derive(Default)]
208
pub enum LayoutZIndex {
209
    #[default]
210
    Auto,
211
    Integer(i32),
212
}
213

            
214
// Formatting to Rust code
215
impl crate::codegen::format::FormatAsRustCode for LayoutZIndex {
216
2
    fn format_as_rust_code(&self, _tabs: usize) -> String {
217
2
        match self {
218
1
            Self::Auto => String::from("LayoutZIndex::Auto"),
219
1
            Self::Integer(val) => {
220
1
                format!("LayoutZIndex::Integer({val})")
221
            }
222
        }
223
2
    }
224
}
225

            
226

            
227
impl PrintAsCssValue for LayoutZIndex {
228
9
    fn print_as_css_value(&self) -> String {
229
9
        match self {
230
2
            Self::Auto => String::from("auto"),
231
7
            Self::Integer(val) => val.to_string(),
232
        }
233
9
    }
234
}
235

            
236
// -- Parser for LayoutZIndex
237

            
238
#[derive(Clone, PartialEq, Eq)]
239
pub enum LayoutZIndexParseError<'a> {
240
    InvalidValue(&'a str),
241
    ParseInt(::core::num::ParseIntError, &'a str),
242
}
243
impl_debug_as_display!(LayoutZIndexParseError<'a>);
244
impl_display! { LayoutZIndexParseError<'a>, {
245
    InvalidValue(val) => format!("Invalid z-index value: \"{}\"", val),
246
    ParseInt(e, s) => format!("Invalid z-index integer \"{}\": {}", s, e),
247
}}
248

            
249
/// Wrapper for `ParseIntError` that stores the error message and original
250
/// input as owned strings for FFI compatibility.
251
#[derive(Debug, Clone, PartialEq, Eq)]
252
#[repr(C)]
253
pub struct ParseIntErrorWithInput {
254
    /// The stringified parse error (e.g. "invalid digit found in string").
255
    pub error: AzString,
256
    /// The original input string that failed to parse.
257
    pub input: AzString,
258
}
259

            
260
#[derive(Debug, Clone, PartialEq, Eq)]
261
#[repr(C, u8)]
262
pub enum LayoutZIndexParseErrorOwned {
263
    InvalidValue(AzString),
264
    ParseInt(ParseIntErrorWithInput),
265
}
266

            
267
impl LayoutZIndexParseError<'_> {
268
10
    #[must_use] pub fn to_contained(&self) -> LayoutZIndexParseErrorOwned {
269
10
        match self {
270
6
            LayoutZIndexParseError::InvalidValue(s) => {
271
6
                LayoutZIndexParseErrorOwned::InvalidValue((*s).to_string().into())
272
            }
273
4
            LayoutZIndexParseError::ParseInt(e, s) => {
274
4
                LayoutZIndexParseErrorOwned::ParseInt(ParseIntErrorWithInput { error: e.to_string().into(), input: (*s).to_string().into() })
275
            }
276
        }
277
10
    }
278
}
279

            
280
impl LayoutZIndexParseErrorOwned {
281
    /// Converts back to the borrowed error type.
282
    ///
283
    /// **Note:** This conversion is lossy for `ParseInt` — the original
284
    /// `core::num::ParseIntError` cannot be reconstructed from its string
285
    /// representation, so `ParseInt` is mapped to `InvalidValue` instead.
286
6
    #[must_use] pub fn to_shared(&self) -> LayoutZIndexParseError<'_> {
287
6
        match self {
288
4
            Self::InvalidValue(s) => {
289
4
                LayoutZIndexParseError::InvalidValue(s.as_str())
290
            }
291
2
            Self::ParseInt(e) => {
292
                // We can't reconstruct ParseIntError, so use InvalidValue
293
2
                LayoutZIndexParseError::InvalidValue(e.input.as_str())
294
            }
295
        }
296
6
    }
297
}
298

            
299
#[cfg(feature = "parser")]
300
/// # Errors
301
///
302
/// Returns an error if `input` is not a valid CSS `z-index` value.
303
186
pub fn parse_layout_z_index(
304
186
    input: &str,
305
186
) -> Result<LayoutZIndex, LayoutZIndexParseError<'_>> {
306
186
    let input = input.trim();
307
186
    if input == "auto" {
308
3
        return Ok(LayoutZIndex::Auto);
309
183
    }
310

            
311
183
    match input.parse::<i32>() {
312
137
        Ok(val) => Ok(LayoutZIndex::Integer(val)),
313
46
        Err(e) => Err(LayoutZIndexParseError::ParseInt(e, input)),
314
    }
315
186
}
316

            
317
#[cfg(all(test, feature = "parser"))]
318
mod tests {
319
    use super::*;
320

            
321
    #[test]
322
1
    fn test_parse_layout_position() {
323
1
        assert_eq!(
324
1
            parse_layout_position("static").unwrap(),
325
            LayoutPosition::Static
326
        );
327
1
        assert_eq!(
328
1
            parse_layout_position("relative").unwrap(),
329
            LayoutPosition::Relative
330
        );
331
1
        assert_eq!(
332
1
            parse_layout_position("absolute").unwrap(),
333
            LayoutPosition::Absolute
334
        );
335
1
        assert_eq!(
336
1
            parse_layout_position("fixed").unwrap(),
337
            LayoutPosition::Fixed
338
        );
339
1
        assert_eq!(
340
1
            parse_layout_position("sticky").unwrap(),
341
            LayoutPosition::Sticky
342
        );
343
1
    }
344

            
345
    #[test]
346
1
    fn test_parse_layout_position_whitespace() {
347
1
        assert_eq!(
348
1
            parse_layout_position("  absolute  ").unwrap(),
349
            LayoutPosition::Absolute
350
        );
351
1
    }
352

            
353
    #[test]
354
1
    fn test_parse_layout_position_invalid() {
355
1
        assert!(parse_layout_position("").is_err());
356
1
        assert!(parse_layout_position("absolutely").is_err());
357
1
    }
358

            
359
    #[test]
360
1
    fn test_parse_layout_z_index() {
361
1
        assert_eq!(parse_layout_z_index("auto").unwrap(), LayoutZIndex::Auto);
362
1
        assert_eq!(
363
1
            parse_layout_z_index("10").unwrap(),
364
            LayoutZIndex::Integer(10)
365
        );
366
1
        assert_eq!(parse_layout_z_index("0").unwrap(), LayoutZIndex::Integer(0));
367
1
        assert_eq!(
368
1
            parse_layout_z_index("-5").unwrap(),
369
            LayoutZIndex::Integer(-5)
370
        );
371
1
        assert_eq!(
372
1
            parse_layout_z_index("  999  ").unwrap(),
373
            LayoutZIndex::Integer(999)
374
        );
375
1
    }
376

            
377
    #[test]
378
1
    fn test_parse_layout_z_index_invalid() {
379
1
        assert!(parse_layout_z_index("10px").is_err());
380
1
        assert!(parse_layout_z_index("1.5").is_err());
381
1
        assert!(parse_layout_z_index("none").is_err());
382
1
        assert!(parse_layout_z_index("").is_err());
383
1
    }
384

            
385
    #[test]
386
1
    fn test_parse_offsets() {
387
1
        assert_eq!(
388
1
            parse_layout_top("10px").unwrap(),
389
1
            LayoutTop {
390
1
                inner: PixelValue::px(10.0)
391
1
            }
392
        );
393
1
        assert_eq!(
394
1
            parse_layout_right("5%").unwrap(),
395
1
            LayoutRight {
396
1
                inner: PixelValue::percent(5.0)
397
1
            }
398
        );
399
1
        assert_eq!(
400
1
            parse_layout_bottom("2.5em").unwrap(),
401
1
            LayoutInsetBottom {
402
1
                inner: PixelValue::em(2.5)
403
1
            }
404
        );
405
1
        assert_eq!(
406
1
            parse_layout_left("0").unwrap(),
407
1
            LayoutLeft {
408
1
                inner: PixelValue::px(0.0)
409
1
            }
410
        );
411
1
    }
412

            
413
    #[test]
414
1
    fn test_parse_offsets_invalid() {
415
        // The simple `parse_pixel_value` does not handle `auto`.
416
1
        assert!(parse_layout_top("auto").is_err());
417
1
        assert!(parse_layout_right("").is_err());
418
        // Liberal parsing accepts whitespace between number and unit
419
1
        assert!(parse_layout_bottom("10 px").is_ok());
420
1
        assert!(parse_layout_left("ten pixels").is_err());
421
1
    }
422
}
423

            
424
#[cfg(test)]
425
mod autotest_generated {
426
    use alloc::{string::String, vec::Vec};
427

            
428
    use super::*;
429
    use crate::{codegen::format::FormatAsRustCode, props::basic::length::SizeMetric};
430

            
431
    /// Every `LayoutPosition` variant, so tests stay exhaustive if one is added.
432
    const ALL_POSITIONS: [LayoutPosition; 5] = [
433
        LayoutPosition::Static,
434
        LayoutPosition::Relative,
435
        LayoutPosition::Absolute,
436
        LayoutPosition::Fixed,
437
        LayoutPosition::Sticky,
438
    ];
439

            
440
    /// Every `SizeMetric` variant *except* `Vmin`, which cannot survive a
441
    /// `parse_pixel_value` round-trip — see `vmin_suffix_is_shadowed_by_in_bug`.
442
    const ROUNDTRIPPABLE_METRICS: [SizeMetric; 11] = [
443
        SizeMetric::Px,
444
        SizeMetric::Pt,
445
        SizeMetric::Em,
446
        SizeMetric::Rem,
447
        SizeMetric::In,
448
        SizeMetric::Cm,
449
        SizeMetric::Mm,
450
        SizeMetric::Percent,
451
        SizeMetric::Vw,
452
        SizeMetric::Vh,
453
        SizeMetric::Vmax,
454
    ];
455

            
456
    // ---------------------------------------------------------------------
457
    // LayoutPosition::is_positioned (predicate)
458
    // ---------------------------------------------------------------------
459

            
460
    #[test]
461
    fn is_positioned_basic_true_false() {
462
        assert!(!LayoutPosition::Static.is_positioned());
463
        assert!(LayoutPosition::Absolute.is_positioned());
464
    }
465

            
466
    #[test]
467
    fn is_positioned_holds_for_every_variant() {
468
        // The invariant the layout solver relies on: positioned <=> not static.
469
        for p in ALL_POSITIONS {
470
            assert_eq!(p.is_positioned(), p != LayoutPosition::Static, "{p:?}");
471
        }
472
    }
473

            
474
    #[test]
475
    fn is_positioned_default_is_static_and_unpositioned() {
476
        assert_eq!(LayoutPosition::default(), LayoutPosition::Static);
477
        assert!(!LayoutPosition::default().is_positioned());
478
    }
479

            
480
    #[test]
481
    fn is_positioned_is_pure() {
482
        // Takes &self; repeated calls must not mutate or drift.
483
        let p = LayoutPosition::Sticky;
484
        assert_eq!(p.is_positioned(), p.is_positioned());
485
        assert!(p.is_positioned());
486
    }
487

            
488
    // ---------------------------------------------------------------------
489
    // parse_layout_position (parser)
490
    // ---------------------------------------------------------------------
491

            
492
    #[cfg(feature = "parser")]
493
    #[test]
494
    fn parse_position_valid_minimal() {
495
        assert_eq!(parse_layout_position("static"), Ok(LayoutPosition::Static));
496
        assert_eq!(
497
            parse_layout_position("relative"),
498
            Ok(LayoutPosition::Relative)
499
        );
500
        assert_eq!(
501
            parse_layout_position("absolute"),
502
            Ok(LayoutPosition::Absolute)
503
        );
504
        assert_eq!(parse_layout_position("fixed"), Ok(LayoutPosition::Fixed));
505
        assert_eq!(parse_layout_position("sticky"), Ok(LayoutPosition::Sticky));
506
    }
507

            
508
    #[cfg(feature = "parser")]
509
    #[test]
510
    fn parse_position_empty_and_whitespace_only() {
511
        // Empty and whitespace-only both trim down to "" and must report "".
512
        for input in ["", "   ", "\t\n", "\r\n\t  \x0c", " \u{0b} "] {
513
            let err = parse_layout_position(input)
514
                .expect_err("whitespace-only input must not parse");
515
            assert_eq!(err, LayoutPositionParseError::InvalidValue(""));
516
        }
517
    }
518

            
519
    #[cfg(feature = "parser")]
520
    #[test]
521
    fn parse_position_garbage_never_panics() {
522
        for input in [
523
            "absolutely",
524
            "STATIC",
525
            "position: absolute",
526
            "!@#$%^&*()",
527
            "\0\0\0",
528
            "-1",
529
            "0",
530
            "null",
531
            "static;",
532
            "static static",
533
            "\u{7f}\u{1}",
534
        ] {
535
            assert!(
536
                parse_layout_position(input).is_err(),
537
                "expected Err for {input:?}"
538
            );
539
        }
540
    }
541

            
542
    #[cfg(feature = "parser")]
543
    #[test]
544
    fn parse_position_is_ascii_case_sensitive() {
545
        // NOTE: pins *current* behaviour. Per CSS Syntax, keywords are ASCII
546
        // case-insensitive, so these arguably ought to be Ok(..) — see report.
547
        for input in ["Static", "STATIC", "sTaTiC", "Absolute", "FIXED"] {
548
            assert!(
549
                parse_layout_position(input).is_err(),
550
                "expected Err for {input:?}"
551
            );
552
        }
553
    }
554

            
555
    #[cfg(feature = "parser")]
556
    #[test]
557
    fn parse_position_trims_but_rejects_inner_junk() {
558
        // Surrounding whitespace is trimmed...
559
        assert_eq!(
560
            parse_layout_position("  \t absolute \n "),
561
            Ok(LayoutPosition::Absolute)
562
        );
563
        // ...but anything else attached to the keyword is rejected.
564
        for input in ["absolute;", "absolute garbage", ";absolute", "absolute,"] {
565
            assert!(
566
                parse_layout_position(input).is_err(),
567
                "expected Err for {input:?}"
568
            );
569
        }
570
    }
571

            
572
    #[cfg(feature = "parser")]
573
    #[test]
574
    fn parse_position_error_borrows_the_trimmed_input() {
575
        let err = parse_layout_position("   bogus   ").unwrap_err();
576
        assert_eq!(err, LayoutPositionParseError::InvalidValue("bogus"));
577
        // Display must show the trimmed slice, not the padded original.
578
        assert_eq!(err.to_string(), "Invalid position value: \"bogus\"");
579
    }
580

            
581
    #[cfg(feature = "parser")]
582
    #[test]
583
    fn parse_position_unicode_never_panics() {
584
        for input in [
585
            "\u{1F600}",
586
            "static\u{0301}",   // combining acute accent
587
            "\u{202E}static",   // RTL override
588
            "абсолютный",
589
            "\u{FEFF}static",   // BOM is not ASCII whitespace -> must not parse
590
            "𝔰𝔱𝔞𝔱𝔦𝔠",
591
        ] {
592
            assert!(
593
                parse_layout_position(input).is_err(),
594
                "expected Err for {input:?}"
595
            );
596
        }
597
    }
598

            
599
    #[cfg(feature = "parser")]
600
    #[test]
601
    fn parse_position_extremely_long_input() {
602
        let huge = "static".repeat(200_000); // ~1.2M bytes
603
        assert!(parse_layout_position(&huge).is_err());
604

            
605
        let huge_ws = String::from(" ").repeat(1_000_000);
606
        assert_eq!(
607
            parse_layout_position(&huge_ws),
608
            Err(LayoutPositionParseError::InvalidValue(""))
609
        );
610
    }
611

            
612
    #[cfg(feature = "parser")]
613
    #[test]
614
    fn parse_position_deeply_nested_does_not_stack_overflow() {
615
        let nested = "[".repeat(10_000);
616
        assert!(parse_layout_position(&nested).is_err());
617
    }
618

            
619
    #[cfg(feature = "parser")]
620
    #[test]
621
    fn parse_position_round_trip_encode_decode() {
622
        for p in ALL_POSITIONS {
623
            let css = p.print_as_css_value();
624
            assert_eq!(parse_layout_position(&css), Ok(p), "round-trip of {p:?}");
625
        }
626
    }
627

            
628
    #[cfg(feature = "parser")]
629
    #[test]
630
    fn parse_position_round_trip_decode_encode() {
631
        for css in ["static", "relative", "absolute", "fixed", "sticky"] {
632
            let parsed = parse_layout_position(css).unwrap();
633
            assert_eq!(parsed.print_as_css_value(), css);
634
        }
635
    }
636

            
637
    #[test]
638
    fn position_format_as_rust_code_names_the_variant() {
639
        assert_eq!(
640
            LayoutPosition::Static.format_as_rust_code(0),
641
            "LayoutPosition::Static"
642
        );
643
        assert_eq!(
644
            LayoutPosition::Sticky.format_as_rust_code(usize::MAX),
645
            "LayoutPosition::Sticky"
646
        );
647
    }
648

            
649
    // ---------------------------------------------------------------------
650
    // LayoutPositionParseError <-> Owned (getters)
651
    // ---------------------------------------------------------------------
652

            
653
    #[test]
654
    fn position_error_to_contained_basic_access() {
655
        let shared = LayoutPositionParseError::InvalidValue("bogus");
656
        assert_eq!(
657
            shared.to_contained(),
658
            LayoutPositionParseErrorOwned::InvalidValue("bogus".to_string().into())
659
        );
660
    }
661

            
662
    #[test]
663
    fn position_error_owned_to_shared_basic_access() {
664
        let owned = LayoutPositionParseErrorOwned::InvalidValue("bogus".to_string().into());
665
        assert_eq!(
666
            owned.to_shared(),
667
            LayoutPositionParseError::InvalidValue("bogus")
668
        );
669
    }
670

            
671
    #[test]
672
    fn position_error_round_trip_is_lossless_on_edge_payloads() {
673
        // Empty, unicode, NUL and a 100k-byte payload must all survive intact.
674
        let long = "x".repeat(100_000);
675
        for payload in [
676
            "",
677
            " ",
678
            "\0",
679
            "\u{1F600}\u{0301}",
680
            "quote\"and\\backslash",
681
            long.as_str(),
682
        ] {
683
            let shared = LayoutPositionParseError::InvalidValue(payload);
684
            let owned = shared.to_contained();
685
            assert_eq!(owned.to_shared(), shared, "payload {payload:?} was mangled");
686
            // ...and the owned form is a fixed point of the two conversions.
687
            assert_eq!(owned.to_shared().to_contained(), owned);
688
        }
689
    }
690

            
691
    // ---------------------------------------------------------------------
692
    // parse_layout_z_index (parser)
693
    // ---------------------------------------------------------------------
694

            
695
    #[cfg(feature = "parser")]
696
    #[test]
697
    fn parse_z_index_valid_minimal() {
698
        assert_eq!(parse_layout_z_index("auto"), Ok(LayoutZIndex::Auto));
699
        assert_eq!(parse_layout_z_index("1"), Ok(LayoutZIndex::Integer(1)));
700
    }
701

            
702
    #[cfg(feature = "parser")]
703
    #[test]
704
    fn parse_z_index_i32_boundaries_saturate_into_err_not_wraparound() {
705
        // Exactly at the i32 limits: must parse.
706
        assert_eq!(
707
            parse_layout_z_index("2147483647"),
708
            Ok(LayoutZIndex::Integer(i32::MAX))
709
        );
710
        assert_eq!(
711
            parse_layout_z_index("-2147483648"),
712
            Ok(LayoutZIndex::Integer(i32::MIN))
713
        );
714

            
715
        // One past the limits: must be a clean Err, never a wrapped value.
716
        for overflowing in [
717
            "2147483648",
718
            "-2147483649",
719
            "9223372036854775807",  // i64::MAX
720
            "-9223372036854775808", // i64::MIN
721
            "340282366920938463463374607431768211456",
722
        ] {
723
            let err = parse_layout_z_index(overflowing)
724
                .expect_err("out-of-range integer must not parse");
725
            assert!(
726
                matches!(err, LayoutZIndexParseError::ParseInt(_, s) if s == overflowing),
727
                "expected ParseInt({overflowing:?}), got {err:?}"
728
            );
729
        }
730
    }
731

            
732
    #[cfg(feature = "parser")]
733
    #[test]
734
    fn parse_z_index_zero_sign_and_padding_forms() {
735
        assert_eq!(parse_layout_z_index("0"), Ok(LayoutZIndex::Integer(0)));
736
        assert_eq!(parse_layout_z_index("-0"), Ok(LayoutZIndex::Integer(0)));
737
        assert_eq!(parse_layout_z_index("+0"), Ok(LayoutZIndex::Integer(0)));
738
        assert_eq!(parse_layout_z_index("+7"), Ok(LayoutZIndex::Integer(7)));
739
        assert_eq!(parse_layout_z_index("007"), Ok(LayoutZIndex::Integer(7)));
740
        assert_eq!(
741
            parse_layout_z_index("  \t -42 \n "),
742
            Ok(LayoutZIndex::Integer(-42))
743
        );
744
    }
745

            
746
    #[cfg(feature = "parser")]
747
    #[test]
748
    fn parse_z_index_rejects_floats_and_float_literals() {
749
        // z-index is an <integer>; nothing float-shaped may sneak through.
750
        for input in [
751
            "1.5", "1.0", "0.0", "-0.0", "1e3", "1E3", "1e-3", "NaN", "nan", "inf", "-inf",
752
            "infinity",
753
        ] {
754
            assert!(
755
                parse_layout_z_index(input).is_err(),
756
                "expected Err for {input:?}"
757
            );
758
        }
759
    }
760

            
761
    #[cfg(feature = "parser")]
762
    #[test]
763
    fn parse_z_index_garbage_never_panics() {
764
        for input in [
765
            "", "   ", "\t\n", "auto auto", "AUTO", "Auto", "none", "10px", "1_000", "1,000",
766
            "- 5", "5-", "--5", "++5", "0x1F", "0b1", "١٢٣", // Arabic-Indic digits
767
            "123",                                          // fullwidth digits
768
            "\u{1F600}", "\0", "5\0",
769
        ] {
770
            assert!(
771
                parse_layout_z_index(input).is_err(),
772
                "expected Err for {input:?}"
773
            );
774
        }
775
    }
776

            
777
    #[cfg(feature = "parser")]
778
    #[test]
779
    fn parse_z_index_extremely_long_input_terminates() {
780
        let huge = "9".repeat(1_000_000);
781
        assert!(parse_layout_z_index(&huge).is_err());
782

            
783
        let nested = "[".repeat(10_000);
784
        assert!(parse_layout_z_index(&nested).is_err());
785
    }
786

            
787
    #[cfg(feature = "parser")]
788
    #[test]
789
    fn parse_z_index_error_borrows_the_trimmed_input() {
790
        let err = parse_layout_z_index("  abc  ").unwrap_err();
791
        assert!(
792
            matches!(&err, LayoutZIndexParseError::ParseInt(_, s) if *s == "abc"),
793
            "got {err:?}"
794
        );
795
        assert!(err.to_string().starts_with("Invalid z-index integer \"abc\""));
796
    }
797

            
798
    #[cfg(feature = "parser")]
799
    #[test]
800
    fn parse_z_index_round_trip_encode_decode() {
801
        let values = [
802
            LayoutZIndex::Auto,
803
            LayoutZIndex::Integer(0),
804
            LayoutZIndex::Integer(1),
805
            LayoutZIndex::Integer(-1),
806
            LayoutZIndex::Integer(12_345),
807
            LayoutZIndex::Integer(-99_999),
808
            LayoutZIndex::Integer(i32::MAX),
809
            LayoutZIndex::Integer(i32::MIN),
810
        ];
811
        for z in values {
812
            let css = z.print_as_css_value();
813
            assert_eq!(parse_layout_z_index(&css), Ok(z), "round-trip of {z:?}");
814
        }
815
    }
816

            
817
    #[test]
818
    fn z_index_default_is_auto() {
819
        assert_eq!(LayoutZIndex::default(), LayoutZIndex::Auto);
820
        assert_eq!(LayoutZIndex::default().print_as_css_value(), "auto");
821
    }
822

            
823
    #[test]
824
    fn z_index_format_as_rust_code_survives_i32_min() {
825
        assert_eq!(LayoutZIndex::Auto.format_as_rust_code(0), "LayoutZIndex::Auto");
826
        assert_eq!(
827
            LayoutZIndex::Integer(i32::MIN).format_as_rust_code(0),
828
            "LayoutZIndex::Integer(-2147483648)"
829
        );
830
    }
831

            
832
    // ---------------------------------------------------------------------
833
    // LayoutZIndexParseError <-> Owned (getters)
834
    // ---------------------------------------------------------------------
835

            
836
    #[test]
837
    fn z_index_error_invalid_value_round_trips_losslessly() {
838
        for payload in ["", "auto ", "\u{1F600}", "\0"] {
839
            let shared = LayoutZIndexParseError::InvalidValue(payload);
840
            let owned = shared.to_contained();
841
            assert_eq!(
842
                owned,
843
                LayoutZIndexParseErrorOwned::InvalidValue(payload.to_string().into())
844
            );
845
            assert_eq!(owned.to_shared(), shared);
846
        }
847
    }
848

            
849
    #[test]
850
    fn z_index_error_to_contained_captures_parse_int_message_and_input() {
851
        let int_err = "abc".parse::<i32>().unwrap_err();
852
        let shared = LayoutZIndexParseError::ParseInt(int_err.clone(), "abc");
853

            
854
        match shared.to_contained() {
855
            LayoutZIndexParseErrorOwned::ParseInt(e) => {
856
                assert_eq!(e.input.as_str(), "abc");
857
                assert!(!e.error.as_str().is_empty());
858
                assert_eq!(e.error.as_str(), int_err.to_string());
859
            }
860
            other => panic!("expected ParseInt, got {other:?}"),
861
        }
862
    }
863

            
864
    #[test]
865
    fn z_index_error_overflow_and_invalid_digit_are_distinct_before_to_shared() {
866
        let invalid_digit = "abc".parse::<i32>().unwrap_err();
867
        let overflow = "2147483648".parse::<i32>().unwrap_err();
868

            
869
        let a = LayoutZIndexParseError::ParseInt(invalid_digit, "abc").to_contained();
870
        let b = LayoutZIndexParseError::ParseInt(overflow, "2147483648").to_contained();
871
        assert_ne!(a, b, "the two ParseIntError kinds must not collapse");
872
    }
873

            
874
    #[test]
875
    fn z_index_error_to_shared_is_lossy_for_parse_int_as_documented() {
876
        let int_err = "abc".parse::<i32>().unwrap_err();
877
        let owned = LayoutZIndexParseError::ParseInt(int_err, "abc").to_contained();
878

            
879
        // Documented: ParseIntError cannot be rebuilt, so ParseInt -> InvalidValue.
880
        let shared = owned.to_shared();
881
        assert_eq!(shared, LayoutZIndexParseError::InvalidValue("abc"));
882

            
883
        // ...which means the owned form is NOT a fixed point of the two
884
        // conversions. This asserts the lossiness is exactly as documented,
885
        // and will fail loudly if someone silently changes the mapping.
886
        assert_ne!(shared.to_contained(), owned);
887
        assert_eq!(
888
            shared.to_contained(),
889
            LayoutZIndexParseErrorOwned::InvalidValue("abc".to_string().into())
890
        );
891
    }
892

            
893
    #[test]
894
    fn z_index_error_to_shared_on_empty_parse_int_payload_does_not_panic() {
895
        let owned = LayoutZIndexParseErrorOwned::ParseInt(ParseIntErrorWithInput {
896
            error: String::new().into(),
897
            input: String::new().into(),
898
        });
899
        assert_eq!(owned.to_shared(), LayoutZIndexParseError::InvalidValue(""));
900
    }
901

            
902
    // ---------------------------------------------------------------------
903
    // Offset properties: top / right / bottom / left
904
    // ---------------------------------------------------------------------
905

            
906
    #[cfg(feature = "parser")]
907
    #[test]
908
    fn offsets_nan_input_saturates_to_zero_instead_of_propagating_nan() {
909
        // f32::from_str accepts "NaN"; PixelValue stores fixed-point isize, and
910
        // `NaN as isize` == 0. Assert the NaN is absorbed, not carried into layout.
911
        for input in ["NaN", "nan", "-NaN", "NaNpx", "nan%"] {
912
            let v = parse_layout_top(input)
913
                .unwrap_or_else(|e| panic!("{input:?} should parse, got {e:?}"));
914
            let n = v.inner.number.get();
915
            assert!(!n.is_nan(), "{input:?} leaked a NaN into PixelValue");
916
            assert_eq!(n, 0.0, "{input:?} should saturate to 0");
917
        }
918
    }
919

            
920
    #[cfg(feature = "parser")]
921
    #[test]
922
    fn offsets_infinite_input_saturates_to_finite_bounds() {
923
        // f32::from_str maps overflow to +/-inf; the fixed-point cast then
924
        // saturates at isize::MIN/MAX. Nothing infinite may reach the solver.
925
        for (input, positive) in [
926
            ("inf", true),
927
            ("infinity", true),
928
            ("-inf", false),
929
            ("1e40px", true),
930
            ("-1e40px", false),
931
            ("99999999999999999999999999999999999999999999px", true),
932
        ] {
933
            let v = parse_layout_right(input)
934
                .unwrap_or_else(|e| panic!("{input:?} should parse, got {e:?}"));
935
            let n = v.inner.number.get();
936
            assert!(n.is_finite(), "{input:?} leaked a non-finite PixelValue");
937
            assert_eq!(n > 0.0, positive, "{input:?} lost its sign");
938
        }
939
    }
940

            
941
    #[cfg(feature = "parser")]
942
    #[test]
943
    fn offsets_subnormal_input_flushes_to_zero() {
944
        // Below the 1/1000 fixed-point resolution everything truncates to 0.
945
        for input in ["1e-40px", "-1e-40px", "0.0001px", "0.0009px"] {
946
            let v = parse_layout_bottom(input)
947
                .unwrap_or_else(|e| panic!("{input:?} should parse, got {e:?}"));
948
            assert_eq!(v.inner.number.get(), 0.0, "{input:?} should truncate to 0");
949
        }
950
    }
951

            
952
    #[cfg(feature = "parser")]
953
    #[test]
954
    fn offsets_reject_empty_garbage_and_unicode() {
955
        for input in [
956
            "",
957
            "   ",
958
            "auto",
959
            "px",
960
            "%",
961
            "10 20px",
962
            "ten pixels",
963
            "10pxx",
964
            "10 px extra",
965
            "\u{1F600}",
966
            "١٠px", // Arabic-Indic digits
967
            "10\u{00B5}m",
968
        ] {
969
            assert!(
970
                parse_layout_left(input).is_err(),
971
                "expected Err for {input:?}"
972
            );
973
        }
974
    }
975

            
976
    #[cfg(feature = "parser")]
977
    #[test]
978
    fn offsets_extremely_long_input_terminates() {
979
        let huge = "9".repeat(1_000_000);
980
        // Parses to a saturated-but-finite value; must not hang or panic.
981
        let v = parse_layout_top(&huge).expect("a million 9s is a valid f32 literal");
982
        assert!(v.inner.number.get().is_finite());
983

            
984
        let junk = "z".repeat(1_000_000);
985
        assert!(parse_layout_top(&junk).is_err());
986

            
987
        let nested = "[".repeat(10_000);
988
        assert!(parse_layout_top(&nested).is_err());
989
    }
990

            
991
    #[cfg(feature = "parser")]
992
    #[test]
993
    fn offsets_round_trip_encode_decode_across_metrics() {
994
        // Values chosen to be exact at the 1/1000 fixed-point resolution.
995
        for metric in ROUNDTRIPPABLE_METRICS {
996
            for raw in [0.0_f32, 1.0, -1.0, 10.5, -7.25, 1024.0] {
997
                let expected = LayoutTop {
998
                    inner: PixelValue::from_metric(metric, raw),
999
                };
                let css = expected.print_as_css_value();
                let parsed = parse_layout_top(&css)
                    .unwrap_or_else(|e| panic!("{css:?} failed to re-parse: {e:?}"));
                assert_eq!(parsed, expected, "round-trip of {css:?}");
            }
        }
    }
    #[cfg(feature = "parser")]
    #[test]
    fn offsets_all_four_sides_agree_on_the_same_input() {
        // The four parsers are macro-generated; they must not diverge.
        let t = parse_layout_top("12.5%").unwrap();
        let r = parse_layout_right("12.5%").unwrap();
        let b = parse_layout_bottom("12.5%").unwrap();
        let l = parse_layout_left("12.5%").unwrap();
        assert_eq!(t.inner, r.inner);
        assert_eq!(r.inner, b.inner);
        assert_eq!(b.inner, l.inner);
        assert_eq!(t.inner, PixelValue::percent(12.5));
    }
    #[cfg(feature = "parser")]
    #[test]
    fn offsets_zero_is_bare_number_defaulting_to_px() {
        assert_eq!(parse_layout_left("0").unwrap(), LayoutLeft::zero());
        assert_eq!(LayoutTop::zero().inner, PixelValue::px(0.0));
        assert_eq!(LayoutTop::default().inner, PixelValue::zero());
    }
    #[cfg(feature = "parser")]
    #[test]
    fn offset_error_round_trip_is_lossless_for_every_variant() {
        // Drive each CssPixelValueParseError variant through the real parser,
        // then assert to_contained/to_shared preserves it.
        let inputs = ["", "px", "abcpx", "not-a-length"];
        let mut seen: Vec<String> = Vec::new();
        for input in inputs {
            let err = parse_layout_top(input).unwrap_err();
            let owned = err.to_contained();
            assert_eq!(owned.to_shared(), err, "error for {input:?} was mangled");
            assert_eq!(owned.to_shared().to_contained(), owned);
            seen.push(err.to_string());
        }
        // The four inputs must exercise four *distinct* error messages,
        // otherwise this test is silently only covering one variant.
        seen.sort();
        seen.dedup();
        assert_eq!(seen.len(), 4, "expected 4 distinct pixel-value errors");
    }
    /// KNOWN BUG (pinned, not endorsed): `vmin` lengths are unparseable.
    ///
    /// `parse_pixel_value`'s suffix table tries `("in", In)` *before*
    /// `("vmin", Vmin)`, so `"10vmin"` hits `strip_suffix("in")` first, leaving
    /// `"10vm"` to be parsed as an f32 — which fails. Every `vmin` length in a
    /// stylesheet is therefore silently dropped, on `top`/`right`/`bottom`/
    /// `left` and on every other property that goes through `parse_pixel_value`.
    ///
    /// This test asserts the *current* (broken) behaviour so the suite stays
    /// honest; flip it to `is_ok()` / an equality assert once the suffix table
    /// in `css/src/props/basic/pixel.rs` is reordered.
    #[cfg(feature = "parser")]
    #[test]
    fn vmin_suffix_is_shadowed_by_in_bug() {
        // FIXED (was a characterization of the bug): "vmin" used to be shadowed by an
        // earlier "in" suffix and rejected. It now parses on every longhand.
        assert!(parse_layout_top("10vmin").is_ok());
        assert!(parse_layout_right("0vmin").is_ok());
        assert!(parse_layout_bottom("2.5vmin").is_ok());
        assert!(parse_layout_left("100vmin").is_ok());
        // The neighbouring viewport units keep working too.
        assert!(parse_layout_top("10vmax").is_ok());
        assert!(parse_layout_top("10vw").is_ok());
        assert!(parse_layout_top("10vh").is_ok());
    }
}