1
//! CSS properties for CSS Grid layout.
2

            
3
use alloc::{
4
    boxed::Box,
5
    string::{String, ToString},
6
    vec::Vec,
7
};
8

            
9
use crate::{
10
    corety::AzString,
11
    codegen::format::FormatAsRustCode,
12
    impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_eq, impl_vec_hash, impl_vec_mut,
13
    impl_vec_ord, impl_vec_partialeq, impl_vec_partialord,
14
    props::{basic::pixel::PixelValue, formatter::PrintAsCssValue},
15
};
16

            
17
// --- grid-template-columns / grid-template-rows ---
18

            
19
/// Wrapper for minmax(min, max) to satisfy repr(C) (enum variants can only have 1 field)
20
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
21
#[repr(C)]
22
pub struct GridMinMax {
23
    pub min: Box<GridTrackSizing>,
24
    pub max: Box<GridTrackSizing>,
25
}
26

            
27
impl core::fmt::Debug for GridMinMax {
28
1
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
29
1
        write!(
30
1
            f,
31
1
            "minmax({}, {})",
32
1
            self.min.print_as_css_value(),
33
1
            self.max.print_as_css_value()
34
        )
35
1
    }
36
}
37

            
38
/// Represents a single track sizing function for grid
39
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
40
#[repr(C, u8)]
41
#[derive(Default)]
42
pub enum GridTrackSizing {
43
    /// Fixed pixel/percent size
44
    Fixed(PixelValue),
45
    /// fr units (value multiplied by `FR_SCALING_FACTOR` to allow fractional
46
    /// values while satisfying Eq/Ord/Hash — e.g. `1fr` = `Fr(100)`, `0.5fr` = `Fr(50)`)
47
    Fr(i32),
48
    /// min-content
49
    MinContent,
50
    /// max-content
51
    MaxContent,
52
    /// auto
53
    #[default]
54
    Auto,
55
    /// minmax(min, max) - uses `GridMinMax` which contains Box<GridTrackSizing> for each bound
56
    MinMax(GridMinMax),
57
    /// fit-content(size)
58
    FitContent(PixelValue),
59
}
60

            
61
impl_option!(
62
    GridTrackSizing,
63
    OptionGridTrackSizing,
64
    copy = false,
65
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
66
);
67

            
68
impl core::fmt::Debug for GridTrackSizing {
69
1
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
70
1
        write!(f, "{}", self.print_as_css_value())
71
1
    }
72
}
73

            
74

            
75
impl PrintAsCssValue for GridTrackSizing {
76
76
    fn print_as_css_value(&self) -> String {
77
76
        match self {
78
42
            Self::Fixed(px) => px.print_as_css_value(),
79
22
            Self::Fr(f) => format!("{f}fr"),
80
1
            Self::MinContent => "min-content".to_string(),
81
3
            Self::MaxContent => "max-content".to_string(),
82
4
            Self::Auto => "auto".to_string(),
83
3
            Self::MinMax(minmax) => {
84
3
                format!(
85
3
                    "minmax({}, {})",
86
3
                    minmax.min.print_as_css_value(),
87
3
                    minmax.max.print_as_css_value()
88
                )
89
            }
90
1
            Self::FitContent(size) => {
91
1
                format!("fit-content({})", size.print_as_css_value())
92
            }
93
        }
94
76
    }
95
}
96

            
97
// C-compatible Vec for GridTrackSizing
98
impl_vec!(GridTrackSizing, GridTrackSizingVec, GridTrackSizingVecDestructor, GridTrackSizingVecDestructorType, GridTrackSizingVecSlice, OptionGridTrackSizing);
99
impl_vec_clone!(
100
    GridTrackSizing,
101
    GridTrackSizingVec,
102
    GridTrackSizingVecDestructor
103
);
104
impl_vec_debug!(GridTrackSizing, GridTrackSizingVec);
105
impl_vec_partialeq!(GridTrackSizing, GridTrackSizingVec);
106
impl_vec_eq!(GridTrackSizing, GridTrackSizingVec);
107
impl_vec_partialord!(GridTrackSizing, GridTrackSizingVec);
108
impl_vec_ord!(GridTrackSizing, GridTrackSizingVec);
109
impl_vec_hash!(GridTrackSizing, GridTrackSizingVec);
110
impl_vec_mut!(GridTrackSizing, GridTrackSizingVec);
111

            
112
/// Represents `grid-template-columns` or `grid-template-rows`
113
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
114
#[repr(C)]
115
pub struct GridTemplate {
116
    pub tracks: GridTrackSizingVec,
117
}
118

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

            
125
impl Default for GridTemplate {
126
157
    fn default() -> Self {
127
157
        Self {
128
157
            tracks: GridTrackSizingVec::from_vec(Vec::new()),
129
157
        }
130
157
    }
131
}
132

            
133
impl PrintAsCssValue for GridTemplate {
134
21
    fn print_as_css_value(&self) -> String {
135
21
        let tracks_slice = self.tracks.as_ref();
136
21
        if tracks_slice.is_empty() {
137
2
            "none".to_string()
138
        } else {
139
19
            tracks_slice
140
19
                .iter()
141
19
                .map(PrintAsCssValue::print_as_css_value)
142
19
                .collect::<Vec<_>>()
143
19
                .join(" ")
144
        }
145
21
    }
146
}
147

            
148
// --- grid-auto-columns / grid-auto-rows ---
149

            
150
/// Represents `grid-auto-columns` or `grid-auto-rows`
151
/// Structurally identical to `GridTemplate` but semantically different
152
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
153
#[repr(C)]
154
pub struct GridAutoTracks {
155
    pub tracks: GridTrackSizingVec,
156
}
157

            
158
impl core::fmt::Debug for GridAutoTracks {
159
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
160
        write!(f, "{}", self.print_as_css_value())
161
    }
162
}
163

            
164
impl Default for GridAutoTracks {
165
133
    fn default() -> Self {
166
133
        Self {
167
133
            tracks: GridTrackSizingVec::from_vec(Vec::new()),
168
133
        }
169
133
    }
170
}
171

            
172
impl PrintAsCssValue for GridAutoTracks {
173
2
    fn print_as_css_value(&self) -> String {
174
2
        let tracks_slice = self.tracks.as_ref();
175
2
        if tracks_slice.is_empty() {
176
2
            "auto".to_string()
177
        } else {
178
            tracks_slice
179
                .iter()
180
                .map(PrintAsCssValue::print_as_css_value)
181
                .collect::<Vec<_>>()
182
                .join(" ")
183
        }
184
2
    }
185
}
186

            
187
impl From<GridTemplate> for GridAutoTracks {
188
1
    fn from(template: GridTemplate) -> Self {
189
1
        Self {
190
1
            tracks: template.tracks,
191
1
        }
192
1
    }
193
}
194

            
195
// --- grid-row / grid-column (grid line placement) ---
196

            
197
/// Named grid line with optional span count (FFI-safe wrapper)
198
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
199
#[repr(C)]
200
pub struct NamedGridLine {
201
    pub grid_line_name: AzString,
202
    /// Span count, 0 means no span specified
203
    pub span_count: i32,
204
}
205

            
206
impl NamedGridLine {
207
460
    #[must_use] pub fn create(name: AzString, span: Option<i32>) -> Self {
208
460
        Self {
209
460
            grid_line_name: name,
210
460
            span_count: span.unwrap_or(0),
211
460
        }
212
460
    }
213

            
214
12
    #[must_use] pub const fn span(&self) -> Option<i32> {
215
12
        if self.span_count == 0 {
216
3
            None
217
        } else {
218
9
            Some(self.span_count)
219
        }
220
12
    }
221
}
222
#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
223
/// Represents a grid line position (start or end)
224
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
225
#[repr(C, u8)]
226
#[derive(Default)]
227
pub enum GridLine {
228
    /// auto
229
    #[default]
230
    Auto,
231
    /// Line number (1-based, negative for counting from end)
232
    Line(i32),
233
    /// Named line with optional span count
234
    Named(NamedGridLine),
235
    /// span N
236
    Span(i32),
237
}
238

            
239
impl core::fmt::Debug for GridLine {
240
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
241
        write!(f, "{}", self.print_as_css_value())
242
    }
243
}
244

            
245

            
246
impl PrintAsCssValue for GridLine {
247
49
    fn print_as_css_value(&self) -> String {
248
49
        match self {
249
3
            Self::Auto => "auto".to_string(),
250
10
            Self::Line(n) => n.to_string(),
251
32
            Self::Named(named) => {
252
32
                if named.span_count == 0 {
253
30
                    named.grid_line_name.as_str().to_string()
254
                } else {
255
2
                    format!("{} {}", named.grid_line_name.as_str(), named.span_count)
256
                }
257
            }
258
4
            Self::Span(n) => format!("span {n}"),
259
        }
260
49
    }
261
}
262

            
263
/// Represents `grid-row` or `grid-column` (start / end)
264
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
265
#[repr(C)]
266
pub struct GridPlacement {
267
    pub grid_start: GridLine,
268
    pub grid_end: GridLine,
269
}
270

            
271
impl core::fmt::Debug for GridPlacement {
272
15
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
273
15
        write!(f, "{}", self.print_as_css_value())
274
15
    }
275
}
276

            
277
impl Default for GridPlacement {
278
5
    fn default() -> Self {
279
5
        Self {
280
5
            grid_start: GridLine::Auto,
281
5
            grid_end: GridLine::Auto,
282
5
        }
283
5
    }
284
}
285

            
286
impl PrintAsCssValue for GridPlacement {
287
25
    fn print_as_css_value(&self) -> String {
288
25
        if self.grid_end == GridLine::Auto {
289
5
            self.grid_start.print_as_css_value()
290
        } else {
291
20
            format!(
292
20
                "{} / {}",
293
20
                self.grid_start.print_as_css_value(),
294
20
                self.grid_end.print_as_css_value()
295
            )
296
        }
297
25
    }
298
}
299

            
300
#[cfg(feature = "parser")]
301
#[derive(Clone, PartialEq, Eq)]
302
pub enum GridParseError<'a> {
303
    InvalidValue(&'a str),
304
}
305

            
306
#[cfg(feature = "parser")]
307
impl_debug_as_display!(GridParseError<'a>);
308
#[cfg(feature = "parser")]
309
impl_display! { GridParseError<'a>, {
310
    InvalidValue(e) => format!("Invalid grid value: \"{}\"", e),
311
}}
312

            
313
#[cfg(feature = "parser")]
314
#[derive(Debug, Clone, PartialEq, Eq)]
315
#[repr(C, u8)]
316
pub enum GridParseErrorOwned {
317
    InvalidValue(AzString),
318
}
319

            
320
#[cfg(feature = "parser")]
321
impl GridParseError<'_> {
322
8
    #[must_use] pub fn to_contained(&self) -> GridParseErrorOwned {
323
8
        match self {
324
8
            GridParseError::InvalidValue(s) => GridParseErrorOwned::InvalidValue((*s).to_string().into()),
325
        }
326
8
    }
327
}
328

            
329
#[cfg(feature = "parser")]
330
impl GridParseErrorOwned {
331
8
    #[must_use] pub fn to_shared(&self) -> GridParseError<'_> {
332
8
        match self {
333
8
            Self::InvalidValue(s) => GridParseError::InvalidValue(s.as_str()),
334
        }
335
8
    }
336
}
337

            
338
#[cfg(feature = "parser")]
339
221
fn split_respecting_parens(input: &str) -> Result<Vec<String>, ()> {
340
221
    let mut parts = Vec::new();
341
221
    let mut current = String::new();
342
221
    let mut paren_depth: i32 = 0;
343

            
344
2463565
    for ch in input.chars() {
345
100335
        match ch {
346
30135
            '(' => { paren_depth += 1; current.push(ch); }
347
30137
            ')' => { paren_depth -= 1; if paren_depth < 0 { return Err(()); } current.push(ch); }
348
100335
            ' ' if paren_depth == 0 => {
349
100204
                if !current.trim().is_empty() {
350
100197
                    parts.push(current.trim().to_string());
351
100197
                    current.clear();
352
100197
                }
353
            }
354
2303089
            _ => current.push(ch),
355
        }
356
    }
357
214
    if !current.trim().is_empty() {
358
204
        parts.push(current.trim().to_string());
359
204
    }
360
214
    Ok(parts)
361
221
}
362

            
363
#[cfg(feature = "parser")]
364
/// # Errors
365
///
366
/// Returns an error if `input` is not a valid CSS `grid-template` value.
367
177
pub fn parse_grid_template(input: &str) -> Result<GridTemplate, GridParseError<'_>> {
368
    use crate::props::basic::pixel::parse_pixel_value;
369

            
370
177
    let input = input.trim();
371

            
372
177
    if input == "none" {
373
4
        return Ok(GridTemplate::default());
374
173
    }
375

            
376
173
    let parts = split_respecting_parens(input)
377
173
        .map_err(|()| GridParseError::InvalidValue(input))?;
378

            
379
170
    let mut tracks = Vec::new();
380
50495
    for part in &parts {
381
50346
        parse_grid_track_or_repeat(part, &mut tracks)
382
50346
            .map_err(|()| GridParseError::InvalidValue(input))?;
383
    }
384

            
385
149
    Ok(GridTemplate {
386
149
        tracks: GridTrackSizingVec::from_vec(tracks),
387
149
    })
388
177
}
389

            
390
/// Parse a single grid track token, which may be `repeat(N, track)` or a plain track.
391
/// For `repeat(N, track_list)`, the tracks are expanded inline.
392
#[cfg(feature = "parser")]
393
50373
fn parse_grid_track_or_repeat(input: &str, tracks: &mut Vec<GridTrackSizing>) -> Result<(), ()> {
394
    // Maximum repeat count accepted in `repeat(N, …)` to bound expansion.
395
    const MAX_GRID_REPEAT_COUNT: usize = 10_000;
396
50373
    let input = input.trim();
397

            
398
    // Handle repeat(N, track_list)
399
50373
    if input.starts_with("repeat(") && input.ends_with(')') {
400
41
        let content = &input[7..input.len() - 1];
401
        // Find the first comma that separates the count from the track list
402
41
        let comma_pos = content.find(',').ok_or(())?;
403
39
        let count_str = content[..comma_pos].trim();
404
39
        let track_list_str = content[comma_pos + 1..].trim();
405

            
406
39
        let count: usize = count_str.parse().map_err(|_| ())?;
407
32
        if count == 0 || count > MAX_GRID_REPEAT_COUNT {
408
3
            return Err(());
409
29
        }
410

            
411
        // Parse the track list (may contain multiple space-separated tracks)
412
29
        let parts = split_respecting_parens(track_list_str)?;
413
29
        let repeat_tracks: Vec<GridTrackSizing> = parts
414
29
            .iter()
415
38
            .map(|p| parse_grid_track_owned(p))
416
29
            .collect::<Result<Vec<_>, _>>()?;
417

            
418
        // Expand: repeat N times
419
20060
        for _ in 0..count {
420
20060
            tracks.extend(repeat_tracks.iter().cloned());
421
20060
        }
422
26
        return Ok(());
423
50332
    }
424

            
425
    // Plain single track
426
50332
    tracks.push(parse_grid_track_owned(input)?);
427
50304
    Ok(())
428
50373
}
429

            
430
#[cfg(feature = "parser")]
431
50667
fn parse_grid_track_owned(input: &str) -> Result<GridTrackSizing, ()> {
432
    use crate::props::basic::pixel::parse_pixel_value;
433

            
434
50667
    let input = input.trim();
435

            
436
50667
    if input == "auto" {
437
12
        return Ok(GridTrackSizing::Auto);
438
50655
    }
439

            
440
50655
    if input == "min-content" {
441
5
        return Ok(GridTrackSizing::MinContent);
442
50650
    }
443

            
444
50650
    if input == "max-content" {
445
6
        return Ok(GridTrackSizing::MaxContent);
446
50644
    }
447

            
448
50644
    if let Some(num_str) = input.strip_suffix("fr") {
449
        /// Fr values are stored as integers scaled by this factor (e.g. `1fr` = 100, `0.5fr` = 50).
450
        const FR_SCALING_FACTOR: f32 = 100.0;
451
50234
        let num_str = num_str.trim();
452
50234
        if let Ok(num) = num_str.parse::<f32>() {
453
50225
            let scaled = num * FR_SCALING_FACTOR;
454
50225
            if scaled.is_nan() || scaled < crate::cast::i32_to_f32(i32::MIN) || scaled > crate::cast::i32_to_f32(i32::MAX) {
455
15
                return Err(());
456
50210
            }
457
50210
            return Ok(GridTrackSizing::Fr(crate::cast::f32_to_i32(scaled)));
458
9
        }
459
9
        return Err(());
460
410
    }
461

            
462
410
    if input.starts_with("minmax(") && input.ends_with(')') {
463
109
        let content = &input[7..input.len() - 1];
464
109
        let parts: Vec<&str> = content.split(',').collect();
465
109
        if parts.len() == 2 {
466
104
            let min = parse_grid_track_owned(parts[0].trim())?;
467
104
            let max = parse_grid_track_owned(parts[1].trim())?;
468
104
            return Ok(GridTrackSizing::MinMax(GridMinMax {
469
104
                min: Box::new(min),
470
104
                max: Box::new(max),
471
104
            }));
472
5
        }
473
5
        return Err(());
474
301
    }
475

            
476
301
    if input.starts_with("fit-content(") && input.ends_with(')') {
477
7
        let size_str = &input[12..input.len() - 1].trim();
478
7
        if let Ok(size) = parse_pixel_value(size_str) {
479
5
            return Ok(GridTrackSizing::FitContent(size));
480
2
        }
481
2
        return Err(());
482
294
    }
483

            
484
    // Try to parse as pixel value
485
294
    if let Ok(px) = parse_pixel_value(input) {
486
247
        return Ok(GridTrackSizing::Fixed(px));
487
47
    }
488

            
489
47
    Err(())
490
50667
}
491

            
492
#[cfg(feature = "parser")]
493
/// # Errors
494
///
495
/// Returns an error if `input` is not a valid CSS `grid-placement` value.
496
44
pub fn parse_grid_placement(input: &str) -> Result<GridPlacement, GridParseError<'_>> {
497
44
    let input = input.trim();
498

            
499
44
    if input == "auto" {
500
4
        return Ok(GridPlacement::default());
501
40
    }
502

            
503
    // Split by "/"
504
40
    let parts: Vec<&str> = input.split('/').map(str::trim).collect();
505

            
506
38
    let grid_start =
507
40
        parse_grid_line_owned(parts[0]).map_err(|()| GridParseError::InvalidValue(input))?;
508
38
    let grid_end = if parts.len() > 1 {
509
20
        parse_grid_line_owned(parts[1]).map_err(|()| GridParseError::InvalidValue(input))?
510
    } else {
511
18
        GridLine::Auto
512
    };
513

            
514
37
    Ok(GridPlacement {
515
37
        grid_start,
516
37
        grid_end,
517
37
    })
518
44
}
519

            
520
// --- grid-auto-flow ---
521

            
522
/// Represents the `grid-auto-flow` property
523
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
524
#[repr(C)]
525
#[derive(Default)]
526
pub enum LayoutGridAutoFlow {
527
    #[default]
528
    Row,
529
    Column,
530
    RowDense,
531
    ColumnDense,
532
}
533

            
534

            
535
impl PrintAsCssValue for LayoutGridAutoFlow {
536
4
    fn print_as_css_value(&self) -> String {
537
4
        match self {
538
1
            Self::Row => "row".to_string(),
539
1
            Self::Column => "column".to_string(),
540
1
            Self::RowDense => "row dense".to_string(),
541
1
            Self::ColumnDense => "column dense".to_string(),
542
        }
543
4
    }
544
}
545

            
546
#[cfg(feature = "parser")]
547
#[derive(Clone, PartialEq, Eq)]
548
pub enum GridAutoFlowParseError<'a> {
549
    InvalidValue(&'a str),
550
}
551

            
552
#[cfg(feature = "parser")]
553
impl_debug_as_display!(GridAutoFlowParseError<'a>);
554
#[cfg(feature = "parser")]
555
impl_display! { GridAutoFlowParseError<'a>, {
556
    InvalidValue(e) => format!("Invalid grid-auto-flow value: \"{}\"", e),
557
}}
558

            
559
#[cfg(feature = "parser")]
560
#[derive(Debug, Clone, PartialEq, Eq)]
561
#[repr(C, u8)]
562
pub enum GridAutoFlowParseErrorOwned {
563
    InvalidValue(AzString),
564
}
565

            
566
#[cfg(feature = "parser")]
567
impl GridAutoFlowParseError<'_> {
568
6
    #[must_use] pub fn to_contained(&self) -> GridAutoFlowParseErrorOwned {
569
6
        match self {
570
6
            GridAutoFlowParseError::InvalidValue(s) => {
571
6
                GridAutoFlowParseErrorOwned::InvalidValue((*s).to_string().into())
572
            }
573
        }
574
6
    }
575
}
576

            
577
#[cfg(feature = "parser")]
578
impl GridAutoFlowParseErrorOwned {
579
6
    #[must_use] pub fn to_shared(&self) -> GridAutoFlowParseError<'_> {
580
6
        match self {
581
6
            Self::InvalidValue(s) => {
582
6
                GridAutoFlowParseError::InvalidValue(s.as_str())
583
            }
584
        }
585
6
    }
586
}
587

            
588
#[cfg(feature = "parser")]
589
/// # Errors
590
///
591
/// Returns an error if `input` is not a valid CSS `grid-auto-flow` value.
592
26
pub fn parse_layout_grid_auto_flow(
593
26
    input: &str,
594
26
) -> Result<LayoutGridAutoFlow, GridAutoFlowParseError<'_>> {
595
26
    match input.trim() {
596
26
        "row" => Ok(LayoutGridAutoFlow::Row),
597
23
        "column" => Ok(LayoutGridAutoFlow::Column),
598
21
        "row dense" | "dense" => Ok(LayoutGridAutoFlow::RowDense),
599
18
        "column dense" => Ok(LayoutGridAutoFlow::ColumnDense),
600
16
        _ => Err(GridAutoFlowParseError::InvalidValue(input)),
601
    }
602
26
}
603

            
604
// --- justify-self / justify-items ---
605

            
606
/// Represents `justify-self` for grid items
607
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
608
#[repr(C)]
609
#[derive(Default)]
610
pub enum LayoutJustifySelf {
611
    #[default]
612
    Auto,
613
    Start,
614
    End,
615
    Center,
616
    Stretch,
617
}
618

            
619

            
620
impl PrintAsCssValue for LayoutJustifySelf {
621
5
    fn print_as_css_value(&self) -> String {
622
5
        match self {
623
1
            Self::Auto => "auto".to_string(),
624
1
            Self::Start => "start".to_string(),
625
1
            Self::End => "end".to_string(),
626
1
            Self::Center => "center".to_string(),
627
1
            Self::Stretch => "stretch".to_string(),
628
        }
629
5
    }
630
}
631

            
632
#[cfg(feature = "parser")]
633
#[derive(Clone, PartialEq, Eq)]
634
pub enum JustifySelfParseError<'a> {
635
    InvalidValue(&'a str),
636
}
637

            
638
#[cfg(feature = "parser")]
639
#[derive(Debug, Clone, PartialEq, Eq)]
640
#[repr(C, u8)]
641
pub enum JustifySelfParseErrorOwned {
642
    InvalidValue(AzString),
643
}
644

            
645
#[cfg(feature = "parser")]
646
impl JustifySelfParseError<'_> {
647
5
    #[must_use] pub fn to_contained(&self) -> JustifySelfParseErrorOwned {
648
5
        match self {
649
5
            JustifySelfParseError::InvalidValue(s) => {
650
5
                JustifySelfParseErrorOwned::InvalidValue((*s).to_string().into())
651
            }
652
        }
653
5
    }
654
}
655

            
656
#[cfg(feature = "parser")]
657
impl JustifySelfParseErrorOwned {
658
5
    #[must_use] pub fn to_shared(&self) -> JustifySelfParseError<'_> {
659
5
        match self {
660
5
            Self::InvalidValue(s) => {
661
5
                JustifySelfParseError::InvalidValue(s.as_str())
662
            }
663
        }
664
5
    }
665
}
666

            
667
#[cfg(feature = "parser")]
668
impl_debug_as_display!(JustifySelfParseError<'a>);
669
#[cfg(feature = "parser")]
670
impl_display! { JustifySelfParseError<'a>, {
671
    InvalidValue(e) => format!("Invalid justify-self value: \"{}\"", e),
672
}}
673

            
674
#[cfg(feature = "parser")]
675
/// # Errors
676
///
677
/// Returns an error if `input` is not a valid CSS `justify-self` value.
678
29
pub fn parse_layout_justify_self(
679
29
    input: &str,
680
29
) -> Result<LayoutJustifySelf, JustifySelfParseError<'_>> {
681
29
    match input.trim() {
682
29
        "auto" => Ok(LayoutJustifySelf::Auto),
683
26
        "start" | "flex-start" => Ok(LayoutJustifySelf::Start),
684
22
        "end" | "flex-end" => Ok(LayoutJustifySelf::End),
685
19
        "center" => Ok(LayoutJustifySelf::Center),
686
16
        "stretch" => Ok(LayoutJustifySelf::Stretch),
687
14
        _ => Err(JustifySelfParseError::InvalidValue(input)),
688
    }
689
29
}
690

            
691
/// Represents `justify-items` for grid containers
692
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
693
#[repr(C)]
694
#[derive(Default)]
695
pub enum LayoutJustifyItems {
696
    Start,
697
    End,
698
    Center,
699
    #[default]
700
    Stretch,
701
}
702

            
703

            
704
impl PrintAsCssValue for LayoutJustifyItems {
705
4
    fn print_as_css_value(&self) -> String {
706
4
        match self {
707
1
            Self::Start => "start".to_string(),
708
1
            Self::End => "end".to_string(),
709
1
            Self::Center => "center".to_string(),
710
1
            Self::Stretch => "stretch".to_string(),
711
        }
712
4
    }
713
}
714

            
715
#[cfg(feature = "parser")]
716
#[derive(Clone, PartialEq, Eq)]
717
pub enum JustifyItemsParseError<'a> {
718
    InvalidValue(&'a str),
719
}
720

            
721
#[cfg(feature = "parser")]
722
#[derive(Debug, Clone, PartialEq, Eq)]
723
#[repr(C, u8)]
724
pub enum JustifyItemsParseErrorOwned {
725
    InvalidValue(AzString),
726
}
727

            
728
#[cfg(feature = "parser")]
729
impl JustifyItemsParseError<'_> {
730
5
    #[must_use] pub fn to_contained(&self) -> JustifyItemsParseErrorOwned {
731
5
        match self {
732
5
            JustifyItemsParseError::InvalidValue(s) => {
733
5
                JustifyItemsParseErrorOwned::InvalidValue((*s).to_string().into())
734
            }
735
        }
736
5
    }
737
}
738

            
739
#[cfg(feature = "parser")]
740
impl JustifyItemsParseErrorOwned {
741
5
    #[must_use] pub fn to_shared(&self) -> JustifyItemsParseError<'_> {
742
5
        match self {
743
5
            Self::InvalidValue(s) => {
744
5
                JustifyItemsParseError::InvalidValue(s.as_str())
745
            }
746
        }
747
5
    }
748
}
749

            
750
#[cfg(feature = "parser")]
751
impl_debug_as_display!(JustifyItemsParseError<'a>);
752
#[cfg(feature = "parser")]
753
impl_display! { JustifyItemsParseError<'a>, {
754
    InvalidValue(e) => format!("Invalid justify-items value: \"{}\"", e),
755
}}
756

            
757
#[cfg(feature = "parser")]
758
/// # Errors
759
///
760
/// Returns an error if `input` is not a valid CSS `justify-items` value.
761
18
pub fn parse_layout_justify_items(
762
18
    input: &str,
763
18
) -> Result<LayoutJustifyItems, JustifyItemsParseError<'_>> {
764
18
    match input.trim() {
765
18
        "start" => Ok(LayoutJustifyItems::Start),
766
16
        "end" => Ok(LayoutJustifyItems::End),
767
14
        "center" => Ok(LayoutJustifyItems::Center),
768
12
        "stretch" => Ok(LayoutJustifyItems::Stretch),
769
10
        _ => Err(JustifyItemsParseError::InvalidValue(input)),
770
    }
771
18
}
772

            
773
// --- gap (single value type) ---
774

            
775
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
776
#[repr(C)]
777
pub struct LayoutGap {
778
    pub inner: PixelValue,
779
}
780

            
781
impl core::fmt::Debug for LayoutGap {
782
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
783
        write!(f, "{}", self.inner)
784
    }
785
}
786

            
787
impl PrintAsCssValue for LayoutGap {
788
8
    fn print_as_css_value(&self) -> String {
789
8
        self.inner.print_as_css_value()
790
8
    }
791
}
792

            
793
// Implement FormatAsRustCode for the new types so they can be emitted by the
794
// code generator.
795
impl FormatAsRustCode for LayoutGridAutoFlow {
796
    fn format_as_rust_code(&self, _tabs: usize) -> String {
797
        format!(
798
            "LayoutGridAutoFlow::{}",
799
            match self {
800
                Self::Row => "Row",
801
                Self::Column => "Column",
802
                Self::RowDense => "RowDense",
803
                Self::ColumnDense => "ColumnDense",
804
            }
805
        )
806
    }
807
}
808

            
809
impl FormatAsRustCode for LayoutJustifySelf {
810
    fn format_as_rust_code(&self, _tabs: usize) -> String {
811
        format!(
812
            "LayoutJustifySelf::{}",
813
            match self {
814
                Self::Auto => "Auto",
815
                Self::Start => "Start",
816
                Self::End => "End",
817
                Self::Center => "Center",
818
                Self::Stretch => "Stretch",
819
            }
820
        )
821
    }
822
}
823

            
824
impl FormatAsRustCode for LayoutJustifyItems {
825
    fn format_as_rust_code(&self, _tabs: usize) -> String {
826
        format!(
827
            "LayoutJustifyItems::{}",
828
            match self {
829
                Self::Start => "Start",
830
                Self::End => "End",
831
                Self::Center => "Center",
832
                Self::Stretch => "Stretch",
833
            }
834
        )
835
    }
836
}
837

            
838
impl FormatAsRustCode for LayoutGap {
839
    fn format_as_rust_code(&self, _tabs: usize) -> String {
840
        use crate::codegen::format::format_pixel_value;
841
        format!("LayoutGap {{ inner: {} }}", format_pixel_value(&self.inner))
842
    }
843
}
844

            
845
impl FormatAsRustCode for GridTrackSizing {
846
    // `tabs` is required by the FormatAsRustCode trait signature; this variant only
847
    // threads it through to nested MinMax children, never reading it locally.
848
    #[allow(clippy::only_used_in_recursion)]
849
    fn format_as_rust_code(&self, tabs: usize) -> String {
850
        use crate::codegen::format::format_pixel_value;
851
        match self {
852
            Self::Fixed(pv) => {
853
                format!("GridTrackSizing::Fixed({})", format_pixel_value(pv))
854
            }
855
            Self::Fr(f) => format!("GridTrackSizing::Fr({f})"),
856
            Self::MinContent => "GridTrackSizing::MinContent".to_string(),
857
            Self::MaxContent => "GridTrackSizing::MaxContent".to_string(),
858
            Self::Auto => "GridTrackSizing::Auto".to_string(),
859
            Self::MinMax(minmax) => {
860
                format!(
861
                    "GridTrackSizing::MinMax(GridMinMax {{ min: Box::new({}), max: Box::new({}) }})",
862
                    minmax.min.format_as_rust_code(tabs),
863
                    minmax.max.format_as_rust_code(tabs)
864
                )
865
            }
866
            Self::FitContent(pv) => {
867
                format!("GridTrackSizing::FitContent({})", format_pixel_value(pv))
868
            }
869
        }
870
    }
871
}
872

            
873
impl FormatAsRustCode for GridAutoTracks {
874
    fn format_as_rust_code(&self, tabs: usize) -> String {
875
        let tracks: Vec<String> = self
876
            .tracks
877
            .as_ref()
878
            .iter()
879
            .map(|t| t.format_as_rust_code(tabs))
880
            .collect();
881
        format!(
882
            "GridAutoTracks {{ tracks: GridTrackSizingVec::from_vec(vec![{}]) }}",
883
            tracks.join(", ")
884
        )
885
    }
886
}
887

            
888
impl FormatAsRustCode for GridTemplateAreas {
889
    fn format_as_rust_code(&self, _tabs: usize) -> String {
890
        format!("GridTemplateAreas {{ areas: GridAreaDefinitionVec::from_vec(vec!{:?}) }}", self.areas.as_ref())
891
    }
892
}
893

            
894
#[cfg(feature = "parser")]
895
/// # Errors
896
///
897
/// Returns an error if `input` is not a valid CSS `gap` value.
898
81
pub fn parse_layout_gap(
899
81
    input: &str,
900
81
) -> Result<LayoutGap, crate::props::basic::pixel::CssPixelValueParseError<'_>> {
901
81
    crate::props::basic::pixel::parse_pixel_value(input).map(|p| LayoutGap { inner: p })
902
81
}
903

            
904
#[cfg(feature = "parser")]
905
/// # Errors
906
///
907
/// Returns an error if `input` is not a valid CSS `grid-line-owned` value.
908
500
pub fn parse_grid_line_owned(input: &str) -> Result<GridLine, ()> {
909
500
    let input = input.trim();
910

            
911
500
    if input == "auto" {
912
3
        return Ok(GridLine::Auto);
913
497
    }
914

            
915
497
    if let Some(num_str) = input.strip_prefix("span ") {
916
19
        let num_str = num_str.trim();
917
19
        if let Ok(num) = num_str.parse::<i32>() {
918
13
            return Ok(GridLine::Span(num));
919
6
        }
920
6
        return Err(());
921
478
    }
922

            
923
    // Try to parse as line number
924
478
    if let Ok(num) = input.parse::<i32>() {
925
38
        return Ok(GridLine::Line(num));
926
440
    }
927

            
928
    // Otherwise treat as named line
929
440
    Ok(GridLine::Named(NamedGridLine::create(
930
440
        input.to_string().into(),
931
440
        None,
932
440
    )))
933
500
}
934

            
935
#[cfg(all(test, feature = "parser"))]
936
mod tests {
937
    use super::*;
938

            
939
    // Grid template tests
940
    #[test]
941
1
    fn test_parse_grid_template_none() {
942
1
        let result = parse_grid_template("none").unwrap();
943
1
        assert_eq!(result.tracks.len(), 0);
944
1
    }
945

            
946
    #[test]
947
1
    fn test_parse_grid_template_single_px() {
948
1
        let result = parse_grid_template("100px").unwrap();
949
1
        assert_eq!(result.tracks.len(), 1);
950
1
        assert!(matches!(
951
1
            result.tracks.as_ref()[0],
952
            GridTrackSizing::Fixed(_)
953
        ));
954
1
    }
955

            
956
    #[test]
957
1
    fn test_parse_grid_template_multiple_tracks() {
958
1
        let result = parse_grid_template("100px 200px 1fr").unwrap();
959
1
        assert_eq!(result.tracks.len(), 3);
960
1
    }
961

            
962
    #[test]
963
1
    fn test_parse_grid_template_fr_units() {
964
1
        let result = parse_grid_template("1fr 2fr 1fr").unwrap();
965
1
        assert_eq!(result.tracks.len(), 3);
966
1
        assert!(matches!(
967
1
            result.tracks.as_ref()[0],
968
            GridTrackSizing::Fr(100)
969
        ));
970
1
        assert!(matches!(
971
1
            result.tracks.as_ref()[1],
972
            GridTrackSizing::Fr(200)
973
        ));
974
1
    }
975

            
976
    #[test]
977
1
    fn test_parse_grid_template_fractional_fr() {
978
1
        let result = parse_grid_template("0.5fr 1.5fr").unwrap();
979
1
        assert_eq!(result.tracks.len(), 2);
980
1
        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::Fr(50)));
981
1
        assert!(matches!(
982
1
            result.tracks.as_ref()[1],
983
            GridTrackSizing::Fr(150)
984
        ));
985
1
    }
986

            
987
    #[test]
988
1
    fn test_parse_grid_template_auto() {
989
1
        let result = parse_grid_template("auto 100px auto").unwrap();
990
1
        assert_eq!(result.tracks.len(), 3);
991
1
        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::Auto));
992
1
        assert!(matches!(result.tracks.as_ref()[2], GridTrackSizing::Auto));
993
1
    }
994

            
995
    #[test]
996
1
    fn test_parse_grid_template_min_max_content() {
997
1
        let result = parse_grid_template("min-content max-content auto").unwrap();
998
1
        assert_eq!(result.tracks.len(), 3);
999
1
        assert!(matches!(
1
            result.tracks.as_ref()[0],
            GridTrackSizing::MinContent
        ));
1
        assert!(matches!(
1
            result.tracks.as_ref()[1],
            GridTrackSizing::MaxContent
        ));
1
    }
    #[test]
1
    fn test_parse_grid_template_minmax() {
1
        let result = parse_grid_template("minmax(100px, 1fr)").unwrap();
1
        assert_eq!(result.tracks.len(), 1);
1
        assert!(matches!(
1
            result.tracks.as_ref()[0],
            GridTrackSizing::MinMax(_)
        ));
1
    }
    #[test]
1
    fn test_parse_grid_template_minmax_complex() {
1
        let result = parse_grid_template("minmax(min-content, max-content)").unwrap();
1
        assert_eq!(result.tracks.len(), 1);
1
    }
    #[test]
1
    fn test_parse_grid_template_fit_content() {
1
        let result = parse_grid_template("fit-content(200px)").unwrap();
1
        assert_eq!(result.tracks.len(), 1);
1
        assert!(matches!(
1
            result.tracks.as_ref()[0],
            GridTrackSizing::FitContent(_)
        ));
1
    }
    #[test]
1
    fn test_parse_grid_template_mixed() {
1
        let result = parse_grid_template("100px minmax(100px, 1fr) auto 2fr").unwrap();
1
        assert_eq!(result.tracks.len(), 4);
1
    }
    #[test]
1
    fn test_parse_grid_template_percent() {
1
        let result = parse_grid_template("25% 50% 25%").unwrap();
1
        assert_eq!(result.tracks.len(), 3);
1
    }
    #[test]
1
    fn test_parse_grid_template_em_units() {
1
        let result = parse_grid_template("10em 20em 1fr").unwrap();
1
        assert_eq!(result.tracks.len(), 3);
1
    }
    // Grid placement tests
    #[test]
1
    fn test_parse_grid_placement_auto() {
1
        let result = parse_grid_placement("auto").unwrap();
1
        assert!(matches!(result.grid_start, GridLine::Auto));
1
        assert!(matches!(result.grid_end, GridLine::Auto));
1
    }
    #[test]
1
    fn test_parse_grid_placement_line_number() {
1
        let result = parse_grid_placement("1").unwrap();
1
        assert!(matches!(result.grid_start, GridLine::Line(1)));
1
        assert!(matches!(result.grid_end, GridLine::Auto));
1
    }
    #[test]
1
    fn test_parse_grid_placement_negative_line() {
1
        let result = parse_grid_placement("-1").unwrap();
1
        assert!(matches!(result.grid_start, GridLine::Line(-1)));
1
    }
    #[test]
1
    fn test_parse_grid_placement_span() {
1
        let result = parse_grid_placement("span 2").unwrap();
1
        assert!(matches!(result.grid_start, GridLine::Span(2)));
1
    }
    #[test]
1
    fn test_parse_grid_placement_start_end() {
1
        let result = parse_grid_placement("1 / 3").unwrap();
1
        assert!(matches!(result.grid_start, GridLine::Line(1)));
1
        assert!(matches!(result.grid_end, GridLine::Line(3)));
1
    }
    #[test]
1
    fn test_parse_grid_placement_span_end() {
1
        let result = parse_grid_placement("1 / span 2").unwrap();
1
        assert!(matches!(result.grid_start, GridLine::Line(1)));
1
        assert!(matches!(result.grid_end, GridLine::Span(2)));
1
    }
    #[test]
1
    fn test_parse_grid_placement_named_line() {
1
        let result = parse_grid_placement("header-start").unwrap();
1
        assert!(matches!(result.grid_start, GridLine::Named(_)));
1
    }
    #[test]
1
    fn test_parse_grid_placement_named_start_end() {
1
        let result = parse_grid_placement("header-start / header-end").unwrap();
1
        assert!(matches!(result.grid_start, GridLine::Named(_)));
1
        assert!(matches!(result.grid_end, GridLine::Named(_)));
1
    }
    // Edge cases
    #[test]
1
    fn test_parse_grid_template_whitespace() {
1
        let result = parse_grid_template("  100px   200px  ").unwrap();
1
        assert_eq!(result.tracks.len(), 2);
1
    }
    #[test]
1
    fn test_parse_grid_placement_whitespace() {
1
        let result = parse_grid_placement("  1  /  3  ").unwrap();
1
        assert!(matches!(result.grid_start, GridLine::Line(1)));
1
        assert!(matches!(result.grid_end, GridLine::Line(3)));
1
    }
    #[test]
1
    fn test_parse_grid_template_zero_fr() {
1
        let result = parse_grid_template("0fr").unwrap();
1
        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::Fr(0)));
1
    }
    #[test]
1
    fn test_parse_grid_placement_zero_line() {
1
        let result = parse_grid_placement("0").unwrap();
1
        assert!(matches!(result.grid_start, GridLine::Line(0)));
1
    }
    // repeat() tests
    #[test]
1
    fn test_parse_grid_template_repeat_fr() {
1
        let result = parse_grid_template("repeat(3, 1fr)").unwrap();
1
        assert_eq!(result.tracks.len(), 3);
1
        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::Fr(100)));
1
        assert!(matches!(result.tracks.as_ref()[1], GridTrackSizing::Fr(100)));
1
        assert!(matches!(result.tracks.as_ref()[2], GridTrackSizing::Fr(100)));
1
    }
    #[test]
1
    fn test_parse_grid_template_repeat_px() {
1
        let result = parse_grid_template("repeat(2, 100px)").unwrap();
1
        assert_eq!(result.tracks.len(), 2);
1
        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::Fixed(_)));
1
        assert!(matches!(result.tracks.as_ref()[1], GridTrackSizing::Fixed(_)));
1
    }
    #[test]
1
    fn test_parse_grid_template_repeat_multiple_tracks() {
        // repeat(2, 100px 1fr) should expand to [100px, 1fr, 100px, 1fr]
1
        let result = parse_grid_template("repeat(2, 100px 1fr)").unwrap();
1
        assert_eq!(result.tracks.len(), 4);
1
        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::Fixed(_)));
1
        assert!(matches!(result.tracks.as_ref()[1], GridTrackSizing::Fr(100)));
1
        assert!(matches!(result.tracks.as_ref()[2], GridTrackSizing::Fixed(_)));
1
        assert!(matches!(result.tracks.as_ref()[3], GridTrackSizing::Fr(100)));
1
    }
    #[test]
1
    fn test_parse_grid_template_repeat_with_other_tracks() {
        // "100px repeat(2, 1fr) auto" should produce [100px, 1fr, 1fr, auto]
1
        let result = parse_grid_template("100px repeat(2, 1fr) auto").unwrap();
1
        assert_eq!(result.tracks.len(), 4);
1
        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::Fixed(_)));
1
        assert!(matches!(result.tracks.as_ref()[1], GridTrackSizing::Fr(100)));
1
        assert!(matches!(result.tracks.as_ref()[2], GridTrackSizing::Fr(100)));
1
        assert!(matches!(result.tracks.as_ref()[3], GridTrackSizing::Auto));
1
    }
    #[test]
1
    fn test_parse_grid_template_repeat_minmax() {
1
        let result = parse_grid_template("repeat(3, minmax(100px, 1fr))").unwrap();
1
        assert_eq!(result.tracks.len(), 3);
1
        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::MinMax(_)));
1
        assert!(matches!(result.tracks.as_ref()[1], GridTrackSizing::MinMax(_)));
1
        assert!(matches!(result.tracks.as_ref()[2], GridTrackSizing::MinMax(_)));
1
    }
}
// --- grid-template-areas ---
/// A single named grid area with its row/column bounds (1-based grid line numbers).
/// This matches taffy's `GridTemplateArea<String>`.
#[repr(C)]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GridAreaDefinition {
    pub name: AzString,
    pub row_start: u16,
    pub row_end: u16,
    pub column_start: u16,
    pub column_end: u16,
}
impl_option!(
    GridAreaDefinition,
    OptionGridAreaDefinition,
    copy = false,
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl_vec!(GridAreaDefinition, GridAreaDefinitionVec, GridAreaDefinitionVecDestructor, GridAreaDefinitionVecDestructorType, GridAreaDefinitionVecSlice, OptionGridAreaDefinition);
impl_vec_clone!(
    GridAreaDefinition,
    GridAreaDefinitionVec,
    GridAreaDefinitionVecDestructor
);
impl_vec_debug!(GridAreaDefinition, GridAreaDefinitionVec);
impl_vec_partialeq!(GridAreaDefinition, GridAreaDefinitionVec);
impl_vec_eq!(GridAreaDefinition, GridAreaDefinitionVec);
impl_vec_partialord!(GridAreaDefinition, GridAreaDefinitionVec);
impl_vec_ord!(GridAreaDefinition, GridAreaDefinitionVec);
impl_vec_hash!(GridAreaDefinition, GridAreaDefinitionVec);
impl_vec_mut!(GridAreaDefinition, GridAreaDefinitionVec);
/// Represents the parsed value of `grid-template-areas`.
///
/// Example CSS:
/// ```css
/// grid-template-areas:
///     "header header header"
///     "sidebar main aside"
///     "footer footer footer";
/// ```
#[repr(C)]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GridTemplateAreas {
    pub areas: GridAreaDefinitionVec,
}
impl Default for GridTemplateAreas {
9
    fn default() -> Self {
9
        Self { areas: GridAreaDefinitionVec::from_vec(Vec::new()) }
9
    }
}
impl PrintAsCssValue for GridTemplateAreas {
9
    fn print_as_css_value(&self) -> String {
9
        let areas_slice = self.areas.as_ref();
9
        if areas_slice.is_empty() {
2
            return "none".to_string();
7
        }
        // Reconstruct the row strings from the area definitions
7
        let max_row = areas_slice.iter().map(|a| a.row_end).max().unwrap_or(1);
7
        let max_col = areas_slice.iter().map(|a| a.column_end).max().unwrap_or(1);
7
        let num_rows = (max_row - 1) as usize;
7
        let num_cols = (max_col - 1) as usize;
7
        let mut grid: Vec<Vec<String>> = vec![vec![".".to_string(); num_cols]; num_rows];
18
        for area in areas_slice {
11
            let row_start = area.row_start as usize - 1;
11
            let row_end = area.row_end as usize - 1;
11
            let col_start = area.column_start as usize - 1;
11
            let col_end = area.column_end as usize - 1;
13
            for row in grid.iter_mut().take(row_end).skip(row_start) {
21
                for cell in row.iter_mut().take(col_end).skip(col_start) {
21
                    *cell = area.name.as_str().to_string();
21
                }
            }
        }
7
        grid.iter()
10
            .map(|row| format!("\"{}\"", row.join(" ")))
7
            .collect::<Vec<_>>()
7
            .join(" ")
9
    }
}
/// Parse `grid-template-areas` CSS value.
///
/// Accepts quoted row strings like:
///   `"header header header" "sidebar main aside" "footer footer footer"`
///
/// Returns a `GridTemplateAreas` with deduplicated named areas and their
/// computed row/column line boundaries (1-based, as taffy expects).
#[cfg(feature = "parser")]
/// # Errors
///
/// Returns an error if `input` is not a valid CSS `grid-template-areas` value.
69
pub fn parse_grid_template_areas(input: &str) -> Result<GridTemplateAreas, ()> {
    use alloc::collections::BTreeMap;
69
    let input = input.trim();
69
    if input == "none" {
3
        return Ok(GridTemplateAreas::default());
66
    }
    // Extract quoted strings: each one is a row
66
    let mut rows: Vec<Vec<String>> = Vec::new();
66
    let mut i = 0;
66
    let bytes = input.as_bytes();
132262
    while i < bytes.len() {
132201
        if bytes[i] == b'"' || bytes[i] == b'\'' {
65731
            let quote = bytes[i];
65731
            i += 1;
65731
            let start = i;
273012
            while i < bytes.len() && bytes[i] != quote {
207281
                i += 1;
207281
            }
65731
            if i >= bytes.len() {
3
                return Err(());
65728
            }
65728
            let row_str = &input[start..i];
65728
            let cells: Vec<String> = row_str.split_whitespace().map(std::string::ToString::to_string).collect();
65728
            if cells.is_empty() {
2
                return Err(());
65726
            }
65726
            rows.push(cells);
66470
        }
        // advance past the closing quote (quoted branch) or the current char (else)
132196
        i += 1;
    }
61
    if rows.is_empty() {
5
        return Err(());
56
    }
    // Validate: all rows must have the same number of columns
56
    let num_cols = rows[0].len();
65779
    for row in &rows {
65725
        if row.len() != num_cols {
2
            return Err(());
65723
        }
    }
    // Build area map: name -> (min_row, max_row, min_col, max_col) in 0-based indices
54
    let mut area_map: BTreeMap<String, (usize, usize, usize, usize)> = BTreeMap::new();
65721
    for (row_idx, row) in rows.iter().enumerate() {
135917
        for (col_idx, cell) in row.iter().enumerate() {
135917
            if cell == "." {
12
                continue; // skip null cell tokens
135905
            }
135905
            let entry = area_map.entry(cell.clone()).or_insert((row_idx, row_idx, col_idx, col_idx));
135905
            entry.0 = entry.0.min(row_idx);
135905
            entry.1 = entry.1.max(row_idx);
135905
            entry.2 = entry.2.min(col_idx);
135905
            entry.3 = entry.3.max(col_idx);
        }
    }
    // Convert to 1-based grid line numbers (taffy convention)
54
    let mut areas = Vec::new();
234
    for (name, (min_row, max_row, min_col, max_col)) in area_map {
180
        areas.push(GridAreaDefinition {
180
            name: name.into(),
180
            row_start: u16::try_from(min_row + 1).unwrap_or(u16::MAX),
180
            row_end: u16::try_from(max_row + 2).unwrap_or(u16::MAX), // end line is one past the last cell
180
            column_start: u16::try_from(min_col + 1).unwrap_or(u16::MAX),
180
            column_end: u16::try_from(max_col + 2).unwrap_or(u16::MAX),
180
        });
180
    }
54
    Ok(GridTemplateAreas { areas: GridAreaDefinitionVec::from_vec(areas) })
69
}
#[cfg(all(test, feature = "parser"))]
mod autotest_generated {
    use super::*;
    // Every assertion below pins the *observed* behaviour of the code as it stands.
    // Where that behaviour deviates from the CSS Grid spec or loses information, the
    // test is named `..._is_lax` / `..._is_lossy` / `..._saturates` and carries a
    // BUG/DEVIATION comment. Those comments are the deliverable: they mark the places
    // where a fix would have to change the assertion, not the code under test.
    // ---------------------------------------------------------------------
    // NamedGridLine::create / NamedGridLine::span
    // ---------------------------------------------------------------------
    #[test]
    fn named_grid_line_span_roundtrips_every_nonzero_i32() {
        for span in [1_i32, -1, 7, -7, i32::MAX, i32::MIN, i32::MIN + 1] {
            let line = NamedGridLine::create("area".to_string().into(), Some(span));
            assert_eq!(line.span_count, span);
            assert_eq!(line.span(), Some(span), "span {span} must survive create()");
        }
    }
    #[test]
    fn named_grid_line_span_of_some_zero_is_lossy() {
        // BUG (encoding collision): `span_count == 0` is the sentinel for "no span",
        // so an explicitly requested `Some(0)` is indistinguishable from `None` on
        // the way out. `create(_, Some(0)).span()` should arguably be `Some(0)` or
        // `create` should reject 0; today it silently becomes `None`.
        let explicit_zero = NamedGridLine::create("a".to_string().into(), Some(0));
        let absent = NamedGridLine::create("a".to_string().into(), None);
        assert_eq!(explicit_zero.span(), None);
        assert_eq!(absent.span(), None);
        assert_eq!(explicit_zero, absent, "Some(0) and None collapse to the same value");
    }
    #[test]
    fn named_grid_line_create_accepts_empty_and_unicode_names() {
        let empty = NamedGridLine::create(String::new().into(), None);
        assert_eq!(empty.grid_line_name.as_str(), "");
        assert_eq!(empty.span(), None);
        let emoji = NamedGridLine::create("\u{1F600}\u{0301}".to_string().into(), Some(3));
        assert_eq!(emoji.grid_line_name.as_str(), "\u{1F600}\u{0301}");
        assert_eq!(emoji.span(), Some(3));
    }
    #[test]
    fn named_grid_line_span_on_an_extreme_instance_does_not_panic() {
        let huge_name = "x".repeat(100_000);
        let line = NamedGridLine::create(huge_name.clone().into(), Some(i32::MIN));
        assert_eq!(line.span(), Some(i32::MIN));
        assert_eq!(line.grid_line_name.as_str().len(), huge_name.len());
    }
    // ---------------------------------------------------------------------
    // split_respecting_parens (private)
    // ---------------------------------------------------------------------
    #[test]
    fn split_respecting_parens_empty_and_whitespace_yield_ok_empty_not_err() {
        // DEVIATION: the adversarial expectation is Err/None for empty input, but this
        // helper reports "no tokens" as `Ok(vec![])`. That is what makes
        // `parse_grid_template("")` succeed (see the parse_grid_template tests below).
        assert_eq!(split_respecting_parens(""), Ok(Vec::new()));
        assert_eq!(split_respecting_parens("   "), Ok(Vec::new()));
        assert_eq!(split_respecting_parens(" \t\n "), Ok(Vec::new()));
    }
    #[test]
    fn split_respecting_parens_valid_minimal_and_nested_calls() {
        assert_eq!(
            split_respecting_parens("100px 1fr"),
            Ok(vec!["100px".to_string(), "1fr".to_string()])
        );
        // The whole point of the helper: spaces inside parens are NOT separators.
        assert_eq!(
            split_respecting_parens("repeat(2, 100px 1fr) auto"),
            Ok(vec!["repeat(2, 100px 1fr)".to_string(), "auto".to_string()])
        );
        assert_eq!(
            split_respecting_parens("a(b c)d e"),
            Ok(vec!["a(b c)d".to_string(), "e".to_string()])
        );
    }
    #[test]
    fn split_respecting_parens_rejects_unbalanced_close_paren() {
        assert_eq!(split_respecting_parens(")"), Err(()));
        assert_eq!(split_respecting_parens("a)b"), Err(()));
        assert_eq!(split_respecting_parens("(a))"), Err(()));
        assert_eq!(split_respecting_parens(")("), Err(()));
    }
    #[test]
    fn split_respecting_parens_accepts_unbalanced_open_paren_is_lax() {
        // Asymmetry: a stray ')' is an error, a stray '(' is not — the depth counter is
        // never checked at end-of-input. The malformed token is handed downstream, where
        // the track parser happens to reject it, so nothing unsound escapes.
        assert_eq!(split_respecting_parens("((("), Ok(vec!["(((".to_string()]));
        assert_eq!(
            split_respecting_parens("repeat(2, 1fr"),
            Ok(vec!["repeat(2, 1fr".to_string()])
        );
        assert!(parse_grid_template("repeat(2, 1fr").is_err());
    }
    #[test]
    fn split_respecting_parens_does_not_treat_tab_or_newline_as_a_separator() {
        // BUG (CSS whitespace): only U+0020 splits tokens. CSS treats \t, \n, \r and \f
        // as whitespace too, so a multi-line `grid-template-columns` declaration is
        // mis-tokenised into one giant token.
        assert_eq!(
            split_respecting_parens("100px\t200px"),
            Ok(vec!["100px\t200px".to_string()])
        );
        assert_eq!(
            split_respecting_parens("100px\n200px"),
            Ok(vec!["100px\n200px".to_string()])
        );
        // ...and the consequence, one layer up:
        assert!(parse_grid_template("100px\t200px").is_err());
        assert!(parse_grid_template("100px\n200px").is_err());
        // Whereas the space-separated form is fine.
        assert_eq!(parse_grid_template("100px 200px").unwrap().tracks.len(), 2);
    }
    #[test]
    fn split_respecting_parens_handles_multibyte_unicode() {
        // char-based iteration, so no byte-boundary slicing hazard.
        assert_eq!(
            split_respecting_parens("\u{1F600} e\u{0301}"),
            Ok(vec!["\u{1F600}".to_string(), "e\u{0301}".to_string()])
        );
        assert_eq!(
            split_respecting_parens("\u{1F600}(\u{4E2D} \u{6587})"),
            Ok(vec!["\u{1F600}(\u{4E2D} \u{6587})".to_string()])
        );
    }
    #[test]
    fn split_respecting_parens_survives_a_million_chars_and_deep_nesting() {
        // 1M-char single token: linear scan, must not hang.
        let long = "a".repeat(1_000_000);
        let parts = split_respecting_parens(&long).unwrap();
        assert_eq!(parts.len(), 1);
        assert_eq!(parts[0].len(), 1_000_000);
        // 50k space-separated tokens.
        let many = "1fr ".repeat(50_000);
        assert_eq!(split_respecting_parens(&many).unwrap().len(), 50_000);
        // 10k nested parens: the scanner is iterative, so no stack overflow, and the
        // balanced nest is returned as a single (garbage) token that parsing rejects.
        let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
        assert_eq!(split_respecting_parens(&nested).unwrap().len(), 1);
        assert!(parse_grid_template(&nested).is_err());
    }
    // ---------------------------------------------------------------------
    // parse_grid_track_owned (private)
    // ---------------------------------------------------------------------
    #[test]
    fn parse_grid_track_owned_empty_and_whitespace_are_err() {
        assert_eq!(parse_grid_track_owned(""), Err(()));
        assert_eq!(parse_grid_track_owned("   "), Err(()));
        assert_eq!(parse_grid_track_owned("\t\n"), Err(()));
    }
    #[test]
    fn parse_grid_track_owned_valid_minimal_keywords() {
        assert_eq!(parse_grid_track_owned("auto"), Ok(GridTrackSizing::Auto));
        assert_eq!(parse_grid_track_owned("min-content"), Ok(GridTrackSizing::MinContent));
        assert_eq!(parse_grid_track_owned("max-content"), Ok(GridTrackSizing::MaxContent));
        assert_eq!(
            parse_grid_track_owned("  auto  "),
            Ok(GridTrackSizing::Auto),
            "leading/trailing whitespace is trimmed"
        );
        // DEVIATION: CSS keywords are ASCII case-insensitive; this parser is not.
        assert_eq!(parse_grid_track_owned("AUTO"), Err(()));
        assert_eq!(parse_grid_track_owned("Min-Content"), Err(()));
    }
    #[test]
    fn parse_grid_track_owned_fr_is_scaled_by_100_and_truncates() {
        assert_eq!(parse_grid_track_owned("1fr"), Ok(GridTrackSizing::Fr(100)));
        assert_eq!(parse_grid_track_owned("0fr"), Ok(GridTrackSizing::Fr(0)));
        assert_eq!(parse_grid_track_owned("-0fr"), Ok(GridTrackSizing::Fr(0)));
        assert_eq!(parse_grid_track_owned("+1fr"), Ok(GridTrackSizing::Fr(100)));
        assert_eq!(parse_grid_track_owned("0.5fr"), Ok(GridTrackSizing::Fr(50)));
        // Truncation, not rounding: anything below 0.01fr collapses to 0fr, which
        // taffy reads as "take no free space at all".
        assert_eq!(parse_grid_track_owned("0.005fr"), Ok(GridTrackSizing::Fr(0)));
        assert_eq!(parse_grid_track_owned("1.999fr"), Ok(GridTrackSizing::Fr(199)));
        // DEVIATION: CSS forbids negative <flex> values; this accepts them.
        assert_eq!(parse_grid_track_owned("-1fr"), Ok(GridTrackSizing::Fr(-100)));
    }
    #[test]
    fn parse_grid_track_owned_fr_rejects_nan_inf_and_out_of_range() {
        // The guard in parse_grid_track_owned checks is_nan() and the i32 bounds
        // *before* casting, so none of these can produce a garbage saturated Fr.
        for bad in [
            "NaNfr", "nanfr", "inffr", "-inffr", "infinityfr", "1e8fr", "-1e8fr",
            "1e30fr", "-1e30fr", "340282350000000000000000000000000000000fr",
        ] {
            assert_eq!(parse_grid_track_owned(bad), Err(()), "{bad:?} must be rejected");
        }
        // Largest values that still fit: 1e7fr * 100 == 1e9, exactly representable in f32.
        assert_eq!(parse_grid_track_owned("1e7fr"), Ok(GridTrackSizing::Fr(1_000_000_000)));
        assert_eq!(parse_grid_track_owned("-1e7fr"), Ok(GridTrackSizing::Fr(-1_000_000_000)));
    }
    #[test]
    fn parse_grid_track_owned_bare_fr_suffix_is_err() {
        assert_eq!(parse_grid_track_owned("fr"), Err(()));
        assert_eq!(parse_grid_track_owned("  fr"), Err(()));
        assert_eq!(parse_grid_track_owned("xfr"), Err(()));
    }
    #[test]
    fn parse_grid_track_owned_minmax_comma_split_is_paren_unaware() {
        assert_eq!(
            parse_grid_track_owned("minmax(100px, 1fr)"),
            Ok(GridTrackSizing::MinMax(GridMinMax {
                min: Box::new(GridTrackSizing::Fixed(PixelValue::px(100.0))),
                max: Box::new(GridTrackSizing::Fr(100)),
            }))
        );
        // fit-content nests fine (it has no top-level comma)...
        assert!(matches!(
            parse_grid_track_owned("minmax(fit-content(10px), 2px)"),
            Ok(GridTrackSizing::MinMax(_))
        ));
        // ...but anything with an inner comma blows the `split(',')` arity check.
        // DEVIATION: `minmax(minmax(1px,2px), 3px)` is legal-ish CSS shape-wise and is
        // rejected here purely because the comma split ignores parens. It fails closed
        // (Err, not a mis-parse), and it is also what bounds recursion depth to 2.
        assert_eq!(parse_grid_track_owned("minmax(minmax(1px,2px), 3px)"), Err(()));
        assert_eq!(parse_grid_track_owned("minmax(1px,2px,3px)"), Err(()));
        assert_eq!(parse_grid_track_owned("minmax(1px)"), Err(()));
        assert_eq!(parse_grid_track_owned("minmax()"), Err(()));
    }
    #[test]
    fn parse_grid_track_owned_deeply_nested_minmax_cannot_stack_overflow() {
        // 10k nested minmax: rejected at the arity check on the first level, so the
        // recursive descent never actually descends. Guards against a regression that
        // makes the comma split paren-aware without adding a depth limit.
        let deep = format!("{}1px{}", "minmax(".repeat(10_000), ")".repeat(10_000));
        assert_eq!(parse_grid_track_owned(&deep), Err(()));
        let deep_fit = format!("{}1px{}", "fit-content(".repeat(10_000), ")".repeat(10_000));
        assert_eq!(parse_grid_track_owned(&deep_fit), Err(()));
    }
    #[test]
    fn parse_grid_track_owned_unclosed_and_truncated_funcs_are_err() {
        // The `&input[7..len-1]` / `&input[12..len-1]` slices are only reached when both
        // the prefix and the ')' suffix match, which makes len >= 8 / >= 13. These inputs
        // probe every shape near that boundary for an out-of-bounds slice panic.
        for bad in [
            "minmax(", "minmax", "minmax)", "fit-content(", "fit-content",
            "fit-content)", "repeat(", ")", "(", "()", "fit-content()",
        ] {
            assert_eq!(parse_grid_track_owned(bad), Err(()), "{bad:?}");
        }
    }
    #[test]
    fn parse_grid_track_owned_fit_content_and_pixel_fallback() {
        assert_eq!(
            parse_grid_track_owned("fit-content(200px)"),
            Ok(GridTrackSizing::FitContent(PixelValue::px(200.0)))
        );
        assert_eq!(
            parse_grid_track_owned("100px"),
            Ok(GridTrackSizing::Fixed(PixelValue::px(100.0)))
        );
        assert_eq!(
            parse_grid_track_owned("25%"),
            Ok(GridTrackSizing::Fixed(PixelValue::percent(25.0)))
        );
        // DEVIATION (inherited from parse_pixel_value): a unitless number is accepted
        // and silently means px.
        assert_eq!(
            parse_grid_track_owned("100"),
            Ok(GridTrackSizing::Fixed(PixelValue::px(100.0)))
        );
    }
    #[test]
    fn parse_grid_track_owned_pixel_fallback_swallows_nan_and_inf() {
        // BUG (inherited from parse_pixel_value + FloatValue::new): the fr path guards
        // against NaN/inf, the *pixel* path does not. "NaN" is silently coerced to 0px
        // and "inf" saturates to isize::MAX rather than being rejected.
        assert_eq!(
            parse_grid_track_owned("NaNpx"),
            Ok(GridTrackSizing::Fixed(PixelValue::px(0.0))),
            "NaN silently becomes 0px"
        );
        assert_eq!(
            parse_grid_track_owned("inf"),
            Ok(GridTrackSizing::Fixed(PixelValue::px(f32::INFINITY))),
            "inf is accepted and saturates"
        );
        assert_eq!(
            parse_grid_track_owned("1e40px"),
            Ok(GridTrackSizing::Fixed(PixelValue::px(f32::INFINITY)))
        );
    }
    #[test]
    fn parse_grid_track_owned_garbage_and_long_input_never_panic() {
        for bad in [
            "!!!", ";;;", "\u{1F600}", "e\u{0301}\u{0301}", "\0", "100px;garbage",
            "auto;", "1fr 1fr", "--var(x)", "calc(1px + 1px)",
        ] {
            assert_eq!(parse_grid_track_owned(bad), Err(()), "{bad:?}");
        }
        // 1M chars of a non-numeric token: rejected fast, no hang.
        assert_eq!(parse_grid_track_owned(&"a".repeat(1_000_000)), Err(()));
    }
    // ---------------------------------------------------------------------
    // parse_grid_track_or_repeat (private)
    // ---------------------------------------------------------------------
    #[test]
    fn parse_grid_track_or_repeat_valid_minimal() {
        let mut tracks = Vec::new();
        assert_eq!(parse_grid_track_or_repeat("repeat(2, 1fr)", &mut tracks), Ok(()));
        assert_eq!(tracks, vec![GridTrackSizing::Fr(100), GridTrackSizing::Fr(100)]);
        // Plain (non-repeat) tracks are appended to whatever is already there.
        assert_eq!(parse_grid_track_or_repeat("auto", &mut tracks), Ok(()));
        assert_eq!(tracks.len(), 3);
        assert_eq!(tracks[2], GridTrackSizing::Auto);
    }
    #[test]
    fn parse_grid_track_or_repeat_count_is_bounded_at_10_000() {
        let mut tracks = Vec::new();
        assert_eq!(parse_grid_track_or_repeat("repeat(10000, 1fr)", &mut tracks), Ok(()));
        assert_eq!(tracks.len(), 10_000, "the documented maximum is accepted");
        // One past the cap, zero, negative, and a count that overflows usize.
        for bad in [
            "repeat(10001, 1fr)",
            "repeat(0, 1fr)",
            "repeat(-1, 1fr)",
            "repeat(18446744073709551616, 1fr)",
            "repeat(99999999999999999999999999, 1fr)",
            "repeat(1.5, 1fr)",
            "repeat(NaN, 1fr)",
            "repeat(inf, 1fr)",
            "repeat(auto-fill, 1fr)",
        ] {
            let mut t = Vec::new();
            assert_eq!(parse_grid_track_or_repeat(bad, &mut t), Err(()), "{bad:?}");
        }
    }
    #[test]
    fn parse_grid_track_or_repeat_expansion_is_amplified_by_the_track_list_length() {
        // The 10_000 cap bounds the *count*, not the expansion: `repeat(N, <k tracks>)`
        // yields N*k tracks. 10 tracks x 10_000 => 100_000 entries from a 60-byte input.
        // Not unsound, but worth pinning — the real bound on memory is N * k, and k is
        // limited only by the length of the declaration.
        let mut tracks = Vec::new();
        let input = format!("repeat(10000, {})", "1fr ".repeat(10).trim());
        assert_eq!(parse_grid_track_or_repeat(&input, &mut tracks), Ok(()));
        assert_eq!(tracks.len(), 100_000);
        assert!(tracks.iter().all(|t| *t == GridTrackSizing::Fr(100)));
    }
    #[test]
    fn parse_grid_track_or_repeat_with_an_empty_track_list_is_lax() {
        // DEVIATION: `repeat(2, )` has nothing to repeat. It is accepted and contributes
        // zero tracks instead of being rejected.
        let mut tracks = Vec::new();
        assert_eq!(parse_grid_track_or_repeat("repeat(2, )", &mut tracks), Ok(()));
        assert!(tracks.is_empty());
        assert_eq!(parse_grid_template("repeat(2, )").unwrap().tracks.len(), 0);
    }
    #[test]
    fn parse_grid_track_or_repeat_rejects_nested_repeat_and_malformed_shapes() {
        for bad in [
            "repeat(2, repeat(2, 1fr))", // repeat does not recurse
            "repeat(2)",                 // no comma
            "repeat()",
            "repeat(2, 1fr", // unterminated -> falls through to the plain-track path
            "",
            "   ",
            "\u{1F600}",
            "repeat(2, garbage)",
        ] {
            let mut t = Vec::new();
            assert_eq!(parse_grid_track_or_repeat(bad, &mut t), Err(()), "{bad:?}");
        }
    }
    #[test]
    fn parse_grid_track_or_repeat_appends_nothing_when_it_fails() {
        // Atomicity invariant: on Err the caller's Vec must be left exactly as it was.
        // The repeat path validates the entire track list (collect::<Result<Vec<_>,_>>)
        // *before* it expands, and the plain path propagates with `?` before pushing —
        // so a half-expanded repeat can never leak into the caller's tracks.
        let mut tracks = vec![GridTrackSizing::Auto];
        for bad in [
            "repeat(2, 1fr garbage)", // fails on the 2nd track of the list
            "repeat(3, 1fr) trailing", // not a single token: rejected as a plain track
            "repeat(0, 1fr)",
            "repeat(2, 1fr", // unterminated
            "garbage",
        ] {
            assert_eq!(parse_grid_track_or_repeat(bad, &mut tracks), Err(()), "{bad:?}");
            assert_eq!(
                tracks,
                vec![GridTrackSizing::Auto],
                "{bad:?} mutated the caller's Vec despite returning Err"
            );
        }
    }
    // ---------------------------------------------------------------------
    // parse_grid_template
    // ---------------------------------------------------------------------
    #[test]
    fn parse_grid_template_empty_and_whitespace_are_ok_empty_is_lax() {
        // DEVIATION: `grid-template-columns: ;` is not valid CSS, but empty / whitespace
        // input yields `Ok` with zero tracks — silently identical to `none`.
        for input in ["", "   ", "\t\n", "  \r\n  "] {
            let parsed = parse_grid_template(input)
                .unwrap_or_else(|e| panic!("{input:?} unexpectedly failed: {e:?}"));
            assert_eq!(parsed.tracks.len(), 0, "{input:?}");
            assert_eq!(parsed, GridTemplate::default(), "{input:?}");
        }
    }
    #[test]
    fn parse_grid_template_none_is_case_sensitive() {
        assert_eq!(parse_grid_template("none").unwrap(), GridTemplate::default());
        assert_eq!(parse_grid_template("  none  ").unwrap(), GridTemplate::default());
        // DEVIATION: CSS keywords are ASCII case-insensitive.
        assert!(parse_grid_template("NONE").is_err());
        assert!(parse_grid_template("None").is_err());
    }
    #[test]
    fn parse_grid_template_garbage_is_err_and_reports_the_trimmed_input() {
        let err = parse_grid_template("  !!! garbage  ").unwrap_err();
        assert_eq!(
            err,
            GridParseError::InvalidValue("!!! garbage"),
            "the error borrows the trimmed input, not the raw one"
        );
        assert_eq!(format!("{err}"), "Invalid grid value: \"!!! garbage\"");
        for bad in [
            ")", "a)b", "100px;200px", "1 fr", "100px, 200px", "\u{1F600}",
            "100px \u{1F600}", "\0", "calc(100px)",
        ] {
            assert!(parse_grid_template(bad).is_err(), "{bad:?} must be rejected");
        }
    }
    #[test]
    fn parse_grid_template_boundary_numbers_do_not_overflow() {
        // fr guards against NaN/inf/out-of-range...
        for bad in ["NaNfr", "inffr", "-inffr", "1e30fr"] {
            assert!(parse_grid_template(bad).is_err(), "{bad:?}");
        }
        // ...the pixel path saturates instead (see parse_grid_track_owned tests).
        assert_eq!(
            parse_grid_template("1e40px").unwrap().tracks.as_ref()[0],
            GridTrackSizing::Fixed(PixelValue::px(f32::INFINITY))
        );
        // i64::MAX as a bare number: parsed as an f32 px value, no integer overflow.
        assert!(parse_grid_template("9223372036854775807").is_ok());
        assert_eq!(
            parse_grid_template("0px -0px 0fr").unwrap().tracks.len(),
            3,
            "zero and negative-zero are accepted"
        );
    }
    #[test]
    fn parse_grid_template_extremely_long_input_terminates() {
        // 50k tokens.
        let many = "1fr ".repeat(50_000);
        assert_eq!(parse_grid_template(&many).unwrap().tracks.len(), 50_000);
        // 1M-char single garbage token: rejected without hanging.
        assert!(parse_grid_template(&"a".repeat(1_000_000)).is_err());
    }
    #[test]
    fn parse_grid_template_deeply_nested_parens_do_not_stack_overflow() {
        let nested = format!("{}1px{}", "(".repeat(10_000), ")".repeat(10_000));
        assert!(parse_grid_template(&nested).is_err());
        // Unbalanced in the *other* direction is caught by split_respecting_parens.
        assert!(parse_grid_template(&")".repeat(10_000)).is_err());
    }
    // ---------------------------------------------------------------------
    // parse_grid_line_owned
    // ---------------------------------------------------------------------
    #[test]
    fn parse_grid_line_owned_valid_minimal() {
        assert_eq!(parse_grid_line_owned("auto"), Ok(GridLine::Auto));
        assert_eq!(parse_grid_line_owned("1"), Ok(GridLine::Line(1)));
        assert_eq!(parse_grid_line_owned("-1"), Ok(GridLine::Line(-1)));
        assert_eq!(parse_grid_line_owned("+1"), Ok(GridLine::Line(1)));
        assert_eq!(parse_grid_line_owned("0"), Ok(GridLine::Line(0)));
        assert_eq!(parse_grid_line_owned("-0"), Ok(GridLine::Line(0)));
        assert_eq!(parse_grid_line_owned("span 2"), Ok(GridLine::Span(2)));
        assert_eq!(parse_grid_line_owned("span   2"), Ok(GridLine::Span(2)));
    }
    #[test]
    fn parse_grid_line_owned_i32_boundaries_saturate_into_a_named_line() {
        assert_eq!(parse_grid_line_owned("2147483647"), Ok(GridLine::Line(i32::MAX)));
        assert_eq!(parse_grid_line_owned("-2147483648"), Ok(GridLine::Line(i32::MIN)));
        // BUG (silent reinterpretation): one past the i32 range, the integer parse fails
        // and the input falls through to the "named line" catch-all — so `grid-row:
        // 2147483648` becomes a *named* line called "2147483648" instead of an error.
        // A typo'd or overflowing line number is silently accepted as a name.
        assert_eq!(
            parse_grid_line_owned("2147483648"),
            Ok(GridLine::Named(NamedGridLine::create("2147483648".to_string().into(), None)))
        );
        assert_eq!(
            parse_grid_line_owned("-2147483649"),
            Ok(GridLine::Named(NamedGridLine::create("-2147483649".to_string().into(), None)))
        );
        // The `span` path has no such fallback: it fails closed.
        assert_eq!(parse_grid_line_owned("span 2147483648"), Err(()));
        assert_eq!(parse_grid_line_owned("span -2147483649"), Err(()));
        assert_eq!(parse_grid_line_owned("span abc"), Err(()));
        // ...unless the input trims down to bare "span": the leading `trim()` eats the
        // trailing space, `strip_prefix("span ")` then misses, and `span` with no count
        // (invalid CSS) becomes a grid line *named* "span" instead of an error.
        assert_eq!(
            parse_grid_line_owned("span "),
            Ok(GridLine::Named(NamedGridLine::create("span".to_string().into(), None)))
        );
    }
    #[test]
    fn parse_grid_line_owned_span_accepts_zero_and_negative_is_lax() {
        // DEVIATION: CSS requires `span <integer [1,∞]>`. Both of these are invalid CSS
        // and both are accepted here; taffy gets a nonsensical span.
        assert_eq!(parse_grid_line_owned("span 0"), Ok(GridLine::Span(0)));
        assert_eq!(parse_grid_line_owned("span -5"), Ok(GridLine::Span(-5)));
        assert_eq!(parse_grid_line_owned("span -2147483648"), Ok(GridLine::Span(i32::MIN)));
    }
    #[test]
    fn parse_grid_line_owned_never_errs_on_garbage_it_names_it() {
        // BUG (unbounded catch-all): every input that is not `auto` / an i32 / a bad
        // `span` becomes a named grid line. Empty string, punctuation, emoji, an entire
        // stylesheet — all `Ok`. This is why parse_grid_placement effectively cannot
        // reject anything (see below).
        for garbage in ["", "   ", "!!!", ";;;", "\u{1F600}", "\0", "span", "100px", "1 2"] {
            let parsed = parse_grid_line_owned(garbage)
                .unwrap_or_else(|()| panic!("{garbage:?} unexpectedly errored"));
            assert!(
                matches!(parsed, GridLine::Named(_)),
                "{garbage:?} became {parsed:?}, expected a Named catch-all"
            );
        }
        assert_eq!(
            parse_grid_line_owned("   "),
            Ok(GridLine::Named(NamedGridLine::create(String::new().into(), None))),
            "whitespace-only trims to an EMPTY named line"
        );
        // No panic on a huge name.
        assert!(parse_grid_line_owned(&"x".repeat(100_000)).is_ok());
    }
    // ---------------------------------------------------------------------
    // parse_grid_placement
    // ---------------------------------------------------------------------
    #[test]
    fn parse_grid_placement_valid_minimal() {
        assert_eq!(parse_grid_placement("auto").unwrap(), GridPlacement::default());
        assert_eq!(
            parse_grid_placement("1 / 3").unwrap(),
            GridPlacement { grid_start: GridLine::Line(1), grid_end: GridLine::Line(3) }
        );
        assert_eq!(
            parse_grid_placement("1/3").unwrap(),
            GridPlacement { grid_start: GridLine::Line(1), grid_end: GridLine::Line(3) },
            "the slash does not need surrounding spaces"
        );
        assert_eq!(
            parse_grid_placement("  1  /  span 2  ").unwrap(),
            GridPlacement { grid_start: GridLine::Line(1), grid_end: GridLine::Span(2) }
        );
    }
    #[test]
    fn parse_grid_placement_extra_slash_segments_are_silently_dropped() {
        // DEVIATION: `1 / 2 / 3` is not valid CSS. Only parts[0] and parts[1] are read;
        // everything after the second slash is discarded without an error.
        assert_eq!(
            parse_grid_placement("1 / 2 / 3").unwrap(),
            GridPlacement { grid_start: GridLine::Line(1), grid_end: GridLine::Line(2) }
        );
        assert_eq!(
            parse_grid_placement("1 / 2 / 3 / 4 / garbage").unwrap(),
            GridPlacement { grid_start: GridLine::Line(1), grid_end: GridLine::Line(2) }
        );
    }
    #[test]
    fn parse_grid_placement_empty_and_garbage_are_ok_named_is_lax() {
        // BUG: consequence of the parse_grid_line_owned catch-all — `grid-row: ;` and
        // `grid-row: !!!;` both parse successfully into a named line. There is no
        // input-validation value in the Result at all except via the `span` path.
        let empty = parse_grid_placement("").unwrap();
        assert_eq!(
            empty,
            GridPlacement {
                grid_start: GridLine::Named(NamedGridLine::create(String::new().into(), None)),
                grid_end: GridLine::Auto,
            },
            "empty input should be Err, but yields a Named(\"\") start"
        );
        for garbage in ["   ", "!!!", "\u{1F600}", "/", "///", "\0"] {
            assert!(parse_grid_placement(garbage).is_ok(), "{garbage:?}");
        }
        // The ONLY rejection path: a malformed `span`.
        let err = parse_grid_placement("  span x  ").unwrap_err();
        assert_eq!(err, GridParseError::InvalidValue("span x"));
        assert_eq!(format!("{err}"), "Invalid grid value: \"span x\"");
        assert!(parse_grid_placement("1 / span x").is_err());
    }
    #[test]
    fn parse_grid_placement_long_and_nested_input_does_not_panic() {
        let long = "a".repeat(200_000);
        assert!(parse_grid_placement(&long).is_ok());
        assert!(parse_grid_placement(&format!("{long} / {long}")).is_ok());
        // No paren handling here at all, so nesting is inert (but must not panic).
        let nested = format!("{}x{}", "(".repeat(10_000), ")".repeat(10_000));
        assert!(parse_grid_placement(&nested).is_ok());
    }
    // ---------------------------------------------------------------------
    // parse_layout_grid_auto_flow
    // ---------------------------------------------------------------------
    #[test]
    fn parse_layout_grid_auto_flow_accepts_exactly_five_spellings() {
        assert_eq!(parse_layout_grid_auto_flow("row"), Ok(LayoutGridAutoFlow::Row));
        assert_eq!(parse_layout_grid_auto_flow("column"), Ok(LayoutGridAutoFlow::Column));
        assert_eq!(parse_layout_grid_auto_flow("row dense"), Ok(LayoutGridAutoFlow::RowDense));
        assert_eq!(parse_layout_grid_auto_flow("dense"), Ok(LayoutGridAutoFlow::RowDense));
        assert_eq!(
            parse_layout_grid_auto_flow("column dense"),
            Ok(LayoutGridAutoFlow::ColumnDense)
        );
        assert_eq!(parse_layout_grid_auto_flow("  row  "), Ok(LayoutGridAutoFlow::Row));
    }
    #[test]
    fn parse_layout_grid_auto_flow_rejects_everything_else() {
        for bad in [
            "",
            "   ",
            "\t\n",
            "ROW",           // DEVIATION: CSS keywords are case-insensitive
            "Row",
            "dense row",     // DEVIATION: CSS allows either order
            "dense column",
            "row  dense",    // DEVIATION: internal whitespace is not collapsed
            "row\tdense",
            "row dense extra",
            "row;",
            "\u{1F600}",
            "\0",
        ] {
            assert!(parse_layout_grid_auto_flow(bad).is_err(), "{bad:?} must be rejected");
        }
        // Extremely long input: a straight match, no hang.
        assert!(parse_layout_grid_auto_flow(&"row ".repeat(250_000)).is_err());
    }
    #[test]
    fn parse_layout_grid_auto_flow_error_borrows_the_untrimmed_input() {
        // The match trims, but the error variant is built from the *raw* `input`, so the
        // rendered message keeps the caller's padding. Pinned because Display output of
        // these errors ends up in user-facing CSS diagnostics.
        let err = parse_layout_grid_auto_flow("  bogus  ").unwrap_err();
        assert_eq!(err, GridAutoFlowParseError::InvalidValue("  bogus  "));
        assert_eq!(format!("{err}"), "Invalid grid-auto-flow value: \"  bogus  \"");
    }
    // ---------------------------------------------------------------------
    // parse_layout_justify_self / parse_layout_justify_items
    // ---------------------------------------------------------------------
    #[test]
    fn parse_layout_justify_self_accepts_the_flex_aliases() {
        assert_eq!(parse_layout_justify_self("auto"), Ok(LayoutJustifySelf::Auto));
        assert_eq!(parse_layout_justify_self("start"), Ok(LayoutJustifySelf::Start));
        assert_eq!(parse_layout_justify_self("flex-start"), Ok(LayoutJustifySelf::Start));
        assert_eq!(parse_layout_justify_self("end"), Ok(LayoutJustifySelf::End));
        assert_eq!(parse_layout_justify_self("flex-end"), Ok(LayoutJustifySelf::End));
        assert_eq!(parse_layout_justify_self("center"), Ok(LayoutJustifySelf::Center));
        assert_eq!(parse_layout_justify_self("stretch"), Ok(LayoutJustifySelf::Stretch));
        assert_eq!(parse_layout_justify_self("  center  "), Ok(LayoutJustifySelf::Center));
    }
    #[test]
    fn parse_layout_justify_self_rejects_everything_else() {
        for bad in [
            "", "   ", "Start", "START", "space-between", "normal", "left", "right",
            "\u{1F600}", "\0", "start end", "flex-start;",
        ] {
            assert!(parse_layout_justify_self(bad).is_err(), "{bad:?}");
        }
        let err = parse_layout_justify_self("  bogus  ").unwrap_err();
        assert_eq!(err, JustifySelfParseError::InvalidValue("  bogus  "));
        assert_eq!(format!("{err}"), "Invalid justify-self value: \"  bogus  \"");
        assert!(parse_layout_justify_self(&"x".repeat(1_000_000)).is_err());
    }
    #[test]
    fn parse_layout_justify_items_has_no_auto_and_no_flex_aliases() {
        assert_eq!(parse_layout_justify_items("start"), Ok(LayoutJustifyItems::Start));
        assert_eq!(parse_layout_justify_items("end"), Ok(LayoutJustifyItems::End));
        assert_eq!(parse_layout_justify_items("center"), Ok(LayoutJustifyItems::Center));
        assert_eq!(parse_layout_justify_items("stretch"), Ok(LayoutJustifyItems::Stretch));
        // Asymmetry with justify-self, pinned deliberately: `auto` and the `flex-*`
        // aliases are accepted by justify-self but rejected by justify-items. `auto` is
        // in fact valid CSS for justify-items (it means "legacy"/inherit-ish), so this
        // is a DEVIATION, and the two parsers disagree about the same input.
        assert!(parse_layout_justify_items("auto").is_err());
        assert!(parse_layout_justify_self("auto").is_ok());
        assert!(parse_layout_justify_items("flex-start").is_err());
        assert!(parse_layout_justify_self("flex-start").is_ok());
        for bad in ["", "   ", "Start", "\u{1F600}", "\0", "stretch stretch"] {
            assert!(parse_layout_justify_items(bad).is_err(), "{bad:?}");
        }
        let err = parse_layout_justify_items("  bogus  ").unwrap_err();
        assert_eq!(err, JustifyItemsParseError::InvalidValue("  bogus  "));
        assert_eq!(format!("{err}"), "Invalid justify-items value: \"  bogus  \"");
        assert!(parse_layout_justify_items(&"x".repeat(1_000_000)).is_err());
    }
    // ---------------------------------------------------------------------
    // parse_layout_gap
    // ---------------------------------------------------------------------
    #[test]
    fn parse_layout_gap_valid_minimal_and_units() {
        assert_eq!(parse_layout_gap("10px").unwrap().inner, PixelValue::px(10.0));
        assert_eq!(parse_layout_gap("  10px  ").unwrap().inner, PixelValue::px(10.0));
        assert_eq!(parse_layout_gap("1.5em").unwrap().inner, PixelValue::em(1.5));
        assert_eq!(parse_layout_gap("50%").unwrap().inner, PixelValue::percent(50.0));
        assert_eq!(parse_layout_gap("0").unwrap().inner, PixelValue::px(0.0));
        assert_eq!(parse_layout_gap("-0").unwrap().inner, PixelValue::px(0.0));
    }
    #[test]
    fn parse_layout_gap_empty_and_garbage_are_err() {
        assert!(matches!(
            parse_layout_gap("").unwrap_err(),
            crate::props::basic::pixel::CssPixelValueParseError::EmptyString
        ));
        assert!(matches!(
            parse_layout_gap("   ").unwrap_err(),
            crate::props::basic::pixel::CssPixelValueParseError::EmptyString
        ));
        for bad in ["px", "10pxpx", "!!!", "\u{1F600}", "\0", "auto", "normal"] {
            assert!(parse_layout_gap(bad).is_err(), "{bad:?}");
        }
    }
    #[test]
    fn parse_layout_gap_accepts_negative_unitless_and_split_units_is_lax() {
        // DEVIATION: `gap` is a <length-percentage [0,∞]> — negatives are invalid CSS,
        // and a nonzero unitless number is invalid too. Both are accepted here.
        assert_eq!(parse_layout_gap("-20px").unwrap().inner, PixelValue::px(-20.0));
        assert_eq!(parse_layout_gap("10").unwrap().inner, PixelValue::px(10.0));
        // DEVIATION (inherited from parse_pixel_value_inner): the unit suffix is stripped
        // *before* the remainder is trimmed, so whitespace between the number and its
        // unit is silently tolerated. `gap: 10 px` is not valid CSS but parses as 10px.
        assert_eq!(parse_layout_gap("10 px").unwrap().inner, PixelValue::px(10.0));
        assert_eq!(parse_layout_gap("10\tpx").unwrap().inner, PixelValue::px(10.0));
        // Same hole in the track parser's fr path, though the tokenizer usually hides it
        // by splitting "1 fr" into two tokens first.
        assert_eq!(parse_grid_track_owned("1 fr"), Ok(GridTrackSizing::Fr(100)));
        assert!(parse_grid_template("1 fr").is_err(), "...but not via the tokenizer");
    }
    #[test]
    fn parse_layout_gap_nan_and_inf_do_not_panic_but_are_swallowed() {
        // BUG (silent coercion, inherited from FloatValue::new): the f32 -> isize cast
        // saturates, so `gap: NaN` becomes 0px and `gap: inf` becomes isize::MAX/1000 px.
        // Neither is rejected, and neither panics.
        assert_eq!(
            parse_layout_gap("NaN").unwrap().inner,
            PixelValue::px(0.0),
            "NaN silently coerces to 0px"
        );
        assert_eq!(
            parse_layout_gap("inf").unwrap().inner,
            PixelValue::px(f32::INFINITY),
            "inf saturates instead of erroring"
        );
        assert_eq!(
            parse_layout_gap("-inf").unwrap().inner,
            PixelValue::px(f32::NEG_INFINITY)
        );
        assert_eq!(
            parse_layout_gap("1e40px").unwrap().inner,
            PixelValue::px(f32::INFINITY),
            "an f32 overflow saturates rather than wrapping"
        );
        // 10k-digit number: parses (to inf) without hanging.
        assert!(parse_layout_gap(&format!("{}px", "9".repeat(10_000))).is_ok());
    }
    // ---------------------------------------------------------------------
    // parse_grid_template_areas
    // ---------------------------------------------------------------------
    fn area_named<'a>(areas: &'a GridTemplateAreas, name: &str) -> &'a GridAreaDefinition {
        areas
            .areas
            .as_ref()
            .iter()
            .find(|a| a.name.as_str() == name)
            .unwrap_or_else(|| panic!("no area named {name:?}"))
    }
    #[test]
    fn parse_grid_template_areas_valid_minimal() {
        let parsed = parse_grid_template_areas("\"a\"").unwrap();
        assert_eq!(parsed.areas.len(), 1);
        assert_eq!(
            parsed.areas.as_ref()[0],
            GridAreaDefinition {
                name: "a".to_string().into(),
                row_start: 1,
                row_end: 2,
                column_start: 1,
                column_end: 2,
            },
            "1-based, end line is one past the last cell"
        );
        assert_eq!(
            parse_grid_template_areas("none").unwrap(),
            GridTemplateAreas::default()
        );
        assert_eq!(
            parse_grid_template_areas("  none  ").unwrap(),
            GridTemplateAreas::default()
        );
    }
    #[test]
    fn parse_grid_template_areas_computes_bounds_and_sorts_by_name() {
        let parsed =
            parse_grid_template_areas("\"header header\" \"sidebar main\" \"footer footer\"")
                .unwrap();
        assert_eq!(parsed.areas.len(), 4);
        // BTreeMap iteration => areas come out alphabetically, NOT in source order.
        let names: Vec<&str> = parsed.areas.as_ref().iter().map(|a| a.name.as_str()).collect();
        assert_eq!(names, vec!["footer", "header", "main", "sidebar"]);
        let header = area_named(&parsed, "header");
        assert_eq!((header.row_start, header.row_end), (1, 2));
        assert_eq!((header.column_start, header.column_end), (1, 3), "spans both columns");
        let sidebar = area_named(&parsed, "sidebar");
        assert_eq!((sidebar.row_start, sidebar.row_end), (2, 3));
        assert_eq!((sidebar.column_start, sidebar.column_end), (1, 2));
        let footer = area_named(&parsed, "footer");
        assert_eq!((footer.row_start, footer.row_end), (3, 4));
        assert_eq!((footer.column_start, footer.column_end), (1, 3));
    }
    #[test]
    fn parse_grid_template_areas_null_cells_are_skipped() {
        let parsed = parse_grid_template_areas("\". a\" \". a\"").unwrap();
        assert_eq!(parsed.areas.len(), 1, "'.' never becomes an area");
        let a = area_named(&parsed, "a");
        assert_eq!((a.row_start, a.row_end), (1, 3));
        assert_eq!((a.column_start, a.column_end), (2, 3));
        // An all-null grid is Ok-but-empty, indistinguishable from `none`.
        let all_null = parse_grid_template_areas("\". .\" \". .\"").unwrap();
        assert_eq!(all_null, GridTemplateAreas::default());
    }
    #[test]
    fn parse_grid_template_areas_rejects_empty_unterminated_and_ragged() {
        for bad in [
            "",              // no rows
            "   ",
            "\t\n",
            "\"\"",          // empty quoted row -> zero cells
            "\"  \"",        // whitespace-only row -> zero cells
            "\"abc",         // unterminated quote
            "'abc",
            "\"a\" \"b",     // second row unterminated
            "\"a b\" \"c\"", // ragged: 2 columns then 1
            "\"a\" \"b c\"",
            "abc",           // no quotes at all
            "\u{1F600}",
        ] {
            assert_eq!(parse_grid_template_areas(bad), Err(()), "{bad:?} must be rejected");
        }
    }
    #[test]
    fn parse_grid_template_areas_ignores_junk_outside_the_quotes_is_lax() {
        // DEVIATION: the scanner only looks at quoted runs; anything between them is
        // skipped without complaint. `grid-template-areas: garbage "a" ;;;` parses.
        let parsed = parse_grid_template_areas("garbage \"a\" ;;; \u{1F600}").unwrap();
        assert_eq!(parsed.areas.len(), 1);
        assert_eq!(area_named(&parsed, "a").row_start, 1);
        // Single and double quotes are interchangeable, and may be mixed.
        let mixed = parse_grid_template_areas("'a' \"b\"").unwrap();
        assert_eq!(mixed.areas.len(), 2);
        // A double quote inside a single-quoted row is just a name character.
        let weird = parse_grid_template_areas("'a\"b'").unwrap();
        assert_eq!(weird.areas.as_ref()[0].name.as_str(), "a\"b");
    }
    #[test]
    fn parse_grid_template_areas_uses_tabs_and_newlines_as_cell_separators() {
        // Contrast with split_respecting_parens: this path uses split_whitespace(), so
        // it *does* handle the whitespace CSS actually allows.
        let parsed = parse_grid_template_areas("\"a\tb\" \"c\nd\"").unwrap();
        assert_eq!(parsed.areas.len(), 4);
        for name in ["a", "b", "c", "d"] {
            assert_eq!(area_named(&parsed, name).name.as_str(), name);
        }
    }
    #[test]
    fn parse_grid_template_areas_non_rectangular_becomes_a_bounding_box() {
        // BUG (spec violation): CSS requires each named area to form a single rectangle;
        // a discontiguous name is a parse error. Here the min/max reduction just takes
        // the bounding box, so `"b a b"` yields a `b` that spans columns 1..4 and
        // *swallows* the `a` sitting between the two halves.
        let parsed = parse_grid_template_areas("\"b a b\"").unwrap();
        let b = area_named(&parsed, "b");
        let a = area_named(&parsed, "a");
        assert_eq!((b.column_start, b.column_end), (1, 4), "bounding box, not a rectangle");
        assert_eq!((a.column_start, a.column_end), (2, 3));
        assert!(
            a.column_start >= b.column_start && a.column_end <= b.column_end,
            "the two areas overlap, which taffy is never supposed to be handed"
        );
    }
    #[test]
    fn parse_grid_template_areas_row_bounds_saturate_past_u16_max() {
        // 65_599 rows of "a" then one row of "b". True line numbers exceed u16::MAX, and
        // `u16::try_from(..).unwrap_or(u16::MAX)` clamps rather than panicking.
        let mut input = String::with_capacity(65_600 * 4);
        for _ in 0..65_599 {
            input.push_str("\"a\" ");
        }
        input.push_str("\"b\"");
        let parsed = parse_grid_template_areas(&input).unwrap();
        assert_eq!(parsed.areas.len(), 2);
        let a = area_named(&parsed, "a");
        assert_eq!(a.row_start, 1);
        assert_eq!(
            a.row_end,
            u16::MAX,
            "true end line is 65_600; it saturates and the grid is silently truncated"
        );
        // BUG (degenerate output): "b" starts at row 65_600, so BOTH of its line numbers
        // clamp to u16::MAX — producing a zero-height area (row_start == row_end). Every
        // other area produced by this parser satisfies row_end > row_start.
        let b = area_named(&parsed, "b");
        assert_eq!(b.row_start, u16::MAX);
        assert_eq!(b.row_end, u16::MAX);
        assert_eq!(b.row_start, b.row_end, "zero-height area from double saturation");
    }
    #[test]
    fn parse_grid_template_areas_column_bounds_saturate_past_u16_max() {
        let mut row = String::with_capacity(70_000 * 2 + 2);
        row.push('"');
        for _ in 0..70_000 {
            row.push_str("a ");
        }
        row.push('"');
        let parsed = parse_grid_template_areas(&row).unwrap();
        assert_eq!(parsed.areas.len(), 1);
        let a = area_named(&parsed, "a");
        assert_eq!(a.column_start, 1);
        assert_eq!(a.column_end, u16::MAX, "true end line is 70_001; clamped, not wrapped");
        assert_eq!((a.row_start, a.row_end), (1, 2));
    }
    #[test]
    fn parse_grid_template_areas_invariant_end_line_exceeds_start_line() {
        // The invariant PrintAsCssValue relies on (it computes `max_row - 1` on u16 and
        // would underflow if any row_end were 0). Holds for every non-degenerate input;
        // the saturation test above is the one case that violates row_end > row_start.
        for input in [
            "\"a\"",
            "\"a b\" \"c d\"",
            "\"h h h\" \"s m a\" \"f f f\"",
            "\". a .\" \"b a c\"",
            "'x'",
        ] {
            let parsed = parse_grid_template_areas(input).unwrap();
            for area in parsed.areas.as_ref() {
                assert!(area.row_start >= 1, "{input:?}: row_start must be 1-based");
                assert!(area.column_start >= 1, "{input:?}: column_start must be 1-based");
                assert!(area.row_end > area.row_start, "{input:?}: {area:?}");
                assert!(area.column_end > area.column_start, "{input:?}: {area:?}");
            }
        }
    }
    // ---------------------------------------------------------------------
    // Round-trips: print_as_css_value -> parse
    // ---------------------------------------------------------------------
    #[test]
    fn roundtrip_grid_auto_flow_all_variants() {
        for value in [
            LayoutGridAutoFlow::Row,
            LayoutGridAutoFlow::Column,
            LayoutGridAutoFlow::RowDense,
            LayoutGridAutoFlow::ColumnDense,
        ] {
            let printed = value.print_as_css_value();
            assert_eq!(
                parse_layout_grid_auto_flow(&printed),
                Ok(value),
                "{value:?} printed as {printed:?} must re-parse to itself"
            );
        }
    }
    #[test]
    fn roundtrip_justify_self_and_justify_items_all_variants() {
        for value in [
            LayoutJustifySelf::Auto,
            LayoutJustifySelf::Start,
            LayoutJustifySelf::End,
            LayoutJustifySelf::Center,
            LayoutJustifySelf::Stretch,
        ] {
            let printed = value.print_as_css_value();
            assert_eq!(parse_layout_justify_self(&printed), Ok(value), "{value:?}");
        }
        for value in [
            LayoutJustifyItems::Start,
            LayoutJustifyItems::End,
            LayoutJustifyItems::Center,
            LayoutJustifyItems::Stretch,
        ] {
            let printed = value.print_as_css_value();
            assert_eq!(parse_layout_justify_items(&printed), Ok(value), "{value:?}");
        }
    }
    #[test]
    fn roundtrip_gap_and_fixed_tracks_are_stable() {
        for input in ["0px", "10px", "-20px", "1.5em", "2rem", "50%", "10mm", "1in"] {
            let gap = parse_layout_gap(input).unwrap();
            let printed = gap.print_as_css_value();
            assert_eq!(parse_layout_gap(&printed).unwrap(), gap, "gap {input:?} -> {printed:?}");
        }
        for input in ["auto", "min-content", "max-content", "100px", "25%", "fit-content(2px)"] {
            let track = parse_grid_track_owned(input).unwrap();
            let printed = track.print_as_css_value();
            assert_eq!(
                parse_grid_track_owned(&printed),
                Ok(track.clone()),
                "track {input:?} -> {printed:?}"
            );
        }
    }
    #[test]
    fn roundtrip_fr_track_inflates_by_100x_every_cycle() {
        // BUG (serialization): GridTrackSizing::Fr stores the value pre-multiplied by
        // FR_SCALING_FACTOR (1fr == Fr(100)), but PrintAsCssValue prints the RAW integer
        // — `format!("{f}fr")` — instead of dividing it back out. So `1fr` serializes as
        // `100fr`, and every print/parse cycle multiplies the track's flex factor by 100.
        // Any code path that round-trips a stylesheet (serialize a computed style, then
        // re-parse it) corrupts every fr track. Correct output would be "1fr".
        let once = parse_grid_track_owned("1fr").unwrap();
        assert_eq!(once, GridTrackSizing::Fr(100));
        assert_eq!(once.print_as_css_value(), "100fr", "should be \"1fr\"");
        let twice = parse_grid_track_owned(&once.print_as_css_value()).unwrap();
        assert_eq!(twice, GridTrackSizing::Fr(10_000), "100x inflation per cycle");
        let thrice = parse_grid_track_owned(&twice.print_as_css_value()).unwrap();
        assert_eq!(thrice, GridTrackSizing::Fr(1_000_000));
        // Four cycles overflow the i32 guard and the value is dropped entirely.
        let four = parse_grid_track_owned(&thrice.print_as_css_value()).unwrap();
        assert_eq!(four, GridTrackSizing::Fr(100_000_000));
        assert_eq!(
            parse_grid_track_owned(&four.print_as_css_value()),
            Err(()),
            "the fifth cycle exceeds i32 and the declaration is discarded"
        );
        // Same bug through the minmax and template printers.
        assert_eq!(
            parse_grid_track_owned("minmax(100px, 1fr)").unwrap().print_as_css_value(),
            "minmax(100px, 100fr)"
        );
        assert_eq!(
            parse_grid_template("1fr 2fr").unwrap().print_as_css_value(),
            "100fr 200fr"
        );
    }
    #[test]
    fn roundtrip_grid_template_none_and_repeat_expansion() {
        assert_eq!(GridTemplate::default().print_as_css_value(), "none");
        assert_eq!(
            parse_grid_template(&GridTemplate::default().print_as_css_value()).unwrap(),
            GridTemplate::default()
        );
        // repeat() is expanded at parse time and never re-emitted as repeat().
        let expanded = parse_grid_template("repeat(3, 100px)").unwrap();
        assert_eq!(expanded.print_as_css_value(), "100px 100px 100px");
        assert_eq!(parse_grid_template(&expanded.print_as_css_value()).unwrap(), expanded);
    }
    #[test]
    fn roundtrip_grid_auto_tracks_empty_prints_auto_but_reparses_to_one_track() {
        // GridAutoTracks::default() is *zero* tracks yet prints as "auto"; re-parsing
        // that text yields *one* Auto track. Structurally lossy, semantically equivalent
        // (taffy treats a missing grid-auto-* as `auto`), pinned so a future change to
        // either side is a deliberate one.
        let empty = GridAutoTracks::default();
        assert_eq!(empty.tracks.len(), 0);
        assert_eq!(empty.print_as_css_value(), "auto");
        let reparsed: GridAutoTracks = parse_grid_template(&empty.print_as_css_value())
            .unwrap()
            .into();
        assert_eq!(reparsed.tracks.len(), 1);
        assert_eq!(reparsed.tracks.as_ref()[0], GridTrackSizing::Auto);
        assert_ne!(reparsed, empty);
    }
    #[test]
    fn roundtrip_grid_placement_hides_a_trailing_auto_end() {
        // `GridPlacement { start, end: Auto }` prints only the start (no " / auto"), which
        // is what CSS does too. Both directions round-trip.
        for input in ["auto", "1", "-1", "span 2", "1 / 3", "1 / span 2", "auto / 3"] {
            let placement = parse_grid_placement(input).unwrap();
            let printed = placement.print_as_css_value();
            assert_eq!(
                parse_grid_placement(&printed).unwrap(),
                placement,
                "{input:?} printed as {printed:?}"
            );
        }
        assert_eq!(
            GridPlacement { grid_start: GridLine::Line(1), grid_end: GridLine::Auto }
                .print_as_css_value(),
            "1"
        );
        assert_eq!(
            GridPlacement { grid_start: GridLine::Auto, grid_end: GridLine::Line(3) }
                .print_as_css_value(),
            "auto / 3"
        );
    }
    #[test]
    fn roundtrip_named_grid_line_with_a_span_is_lossy() {
        // BUG (unparseable output): `GridLine::Named` with a span prints as "name N"
        // (e.g. "header 2"), but parse_grid_line_owned has no production for that shape —
        // it falls into the named catch-all and produces a line literally *named*
        // "header 2" with span 0. The span is silently lost. Nothing in this file can
        // construct a spanned Named line from CSS text, so the only way to hit this is to
        // build one via NamedGridLine::create and serialize it.
        let spanned = GridLine::Named(NamedGridLine::create("header".to_string().into(), Some(2)));
        assert_eq!(spanned.print_as_css_value(), "header 2");
        let reparsed = parse_grid_line_owned(&spanned.print_as_css_value()).unwrap();
        assert_eq!(
            reparsed,
            GridLine::Named(NamedGridLine::create("header 2".to_string().into(), None))
        );
        assert_ne!(reparsed, spanned);
        // Without a span it round-trips cleanly.
        let plain = GridLine::Named(NamedGridLine::create("header".to_string().into(), None));
        assert_eq!(plain.print_as_css_value(), "header");
        assert_eq!(parse_grid_line_owned(&plain.print_as_css_value()), Ok(plain));
    }
    #[test]
    fn roundtrip_grid_template_areas_rectangular_is_stable() {
        for input in [
            "\"a\"",
            "\"a a\"",
            "\"h h\" \"s m\"",
            "\"a a\" \"a a\"",
            "\". a\" \". a\"",
        ] {
            let parsed = parse_grid_template_areas(input).unwrap();
            let printed = parsed.print_as_css_value();
            assert_eq!(
                parse_grid_template_areas(&printed).unwrap(),
                parsed,
                "{input:?} printed as {printed:?}"
            );
        }
        assert_eq!(GridTemplateAreas::default().print_as_css_value(), "none");
        assert_eq!(
            parse_grid_template_areas(&GridTemplateAreas::default().print_as_css_value()).unwrap(),
            GridTemplateAreas::default()
        );
    }
    #[test]
    fn roundtrip_grid_template_areas_non_rectangular_loses_a_whole_area() {
        // BUG (data loss), the printing half of the bounding-box bug above: areas are
        // repainted in alphabetical order, so a later name overwrites the cells of an
        // earlier one that its bounding box happens to cover. `"b a b"` -> `"b b b"`:
        // the `a` area vanishes on serialization.
        let parsed = parse_grid_template_areas("\"b a b\"").unwrap();
        assert_eq!(parsed.areas.len(), 2);
        assert_eq!(parsed.print_as_css_value(), "\"b b b\"", "the `a` area is erased");
        let reparsed = parse_grid_template_areas(&parsed.print_as_css_value()).unwrap();
        assert_eq!(reparsed.areas.len(), 1);
        assert_ne!(reparsed, parsed);
    }
    // ---------------------------------------------------------------------
    // Error types: to_contained / to_shared
    // ---------------------------------------------------------------------
    #[test]
    fn grid_parse_error_to_contained_and_back_is_identity() {
        for payload in ["", "   ", "bogus", "\u{1F600}\u{0301}", "\0", "a\nb\tc"] {
            let shared = GridParseError::InvalidValue(payload);
            let owned = shared.to_contained();
            assert_eq!(owned, GridParseErrorOwned::InvalidValue(payload.to_string().into()));
            assert_eq!(owned.to_shared(), shared, "{payload:?} must survive the round-trip");
        }
        // A 100k-char payload: no truncation, no panic.
        let huge = "x".repeat(100_000);
        let owned = GridParseError::InvalidValue(&huge).to_contained();
        assert_eq!(owned.to_shared(), GridParseError::InvalidValue(huge.as_str()));
    }
    #[test]
    fn grid_auto_flow_parse_error_to_contained_and_back_is_identity() {
        for payload in ["", "bogus", "\u{1F600}", "\0"] {
            let shared = GridAutoFlowParseError::InvalidValue(payload);
            let owned = shared.to_contained();
            assert_eq!(
                owned,
                GridAutoFlowParseErrorOwned::InvalidValue(payload.to_string().into())
            );
            assert_eq!(owned.to_shared(), shared);
        }
        let huge = "x".repeat(100_000);
        let owned = GridAutoFlowParseError::InvalidValue(&huge).to_contained();
        assert_eq!(owned.to_shared(), GridAutoFlowParseError::InvalidValue(huge.as_str()));
    }
    #[test]
    fn justify_self_parse_error_to_contained_and_back_is_identity() {
        for payload in ["", "bogus", "\u{1F600}", "\0"] {
            let shared = JustifySelfParseError::InvalidValue(payload);
            let owned = shared.to_contained();
            assert_eq!(
                owned,
                JustifySelfParseErrorOwned::InvalidValue(payload.to_string().into())
            );
            assert_eq!(owned.to_shared(), shared);
        }
        let huge = "x".repeat(100_000);
        let owned = JustifySelfParseError::InvalidValue(&huge).to_contained();
        assert_eq!(owned.to_shared(), JustifySelfParseError::InvalidValue(huge.as_str()));
    }
    #[test]
    fn justify_items_parse_error_to_contained_and_back_is_identity() {
        for payload in ["", "bogus", "\u{1F600}", "\0"] {
            let shared = JustifyItemsParseError::InvalidValue(payload);
            let owned = shared.to_contained();
            assert_eq!(
                owned,
                JustifyItemsParseErrorOwned::InvalidValue(payload.to_string().into())
            );
            assert_eq!(owned.to_shared(), shared);
        }
        let huge = "x".repeat(100_000);
        let owned = JustifyItemsParseError::InvalidValue(&huge).to_contained();
        assert_eq!(owned.to_shared(), JustifyItemsParseError::InvalidValue(huge.as_str()));
    }
    #[test]
    fn error_types_round_trip_through_a_real_parse_failure() {
        // The realistic path: borrow an error out of a parser, own it, hand it back.
        let input = String::from("  span x  ");
        let owned = parse_grid_placement(&input).unwrap_err().to_contained();
        drop(input); // the owned form must not borrow the parsed input
        assert_eq!(owned.to_shared(), GridParseError::InvalidValue("span x"));
        let flow = String::from("bogus-flow");
        let owned_flow = parse_layout_grid_auto_flow(&flow).unwrap_err().to_contained();
        drop(flow);
        assert_eq!(
            owned_flow.to_shared(),
            GridAutoFlowParseError::InvalidValue("bogus-flow")
        );
    }
    // ---------------------------------------------------------------------
    // Debug / display invariants
    // ---------------------------------------------------------------------
    #[test]
    fn debug_impls_match_print_as_css_value() {
        // GridTrackSizing / GridTemplate / GridPlacement all forward Debug to the CSS
        // printer, so a Debug regression is a serialization regression.
        let track = parse_grid_track_owned("minmax(100px, max-content)").unwrap();
        assert_eq!(format!("{track:?}"), track.print_as_css_value());
        let template = parse_grid_template("100px auto").unwrap();
        assert_eq!(format!("{template:?}"), template.print_as_css_value());
        let placement = parse_grid_placement("1 / span 2").unwrap();
        assert_eq!(format!("{placement:?}"), placement.print_as_css_value());
        let minmax = GridMinMax {
            min: Box::new(GridTrackSizing::Fixed(PixelValue::px(1.0))),
            max: Box::new(GridTrackSizing::Auto),
        };
        assert_eq!(format!("{minmax:?}"), "minmax(1px, auto)");
    }
}