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
    format_rust_code::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
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
29
        write!(
30
            f,
31
            "minmax({}, {})",
32
            self.min.print_as_css_value(),
33
            self.max.print_as_css_value()
34
        )
35
    }
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
    [Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
66
);
67

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

            
74

            
75
impl PrintAsCssValue for GridTrackSizing {
76
42
    fn print_as_css_value(&self) -> String {
77
42
        match self {
78
28
            GridTrackSizing::Fixed(px) => px.print_as_css_value(),
79
14
            GridTrackSizing::Fr(f) => format!("{}fr", f),
80
            GridTrackSizing::MinContent => "min-content".to_string(),
81
            GridTrackSizing::MaxContent => "max-content".to_string(),
82
            GridTrackSizing::Auto => "auto".to_string(),
83
            GridTrackSizing::MinMax(minmax) => {
84
                format!(
85
                    "minmax({}, {})",
86
                    minmax.min.print_as_css_value(),
87
                    minmax.max.print_as_css_value()
88
                )
89
            }
90
            GridTrackSizing::FitContent(size) => {
91
                format!("fit-content({})", size.print_as_css_value())
92
            }
93
        }
94
42
    }
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
14
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
121
14
        write!(f, "{}", self.print_as_css_value())
122
14
    }
123
}
124

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

            
133
impl PrintAsCssValue for GridTemplate {
134
14
    fn print_as_css_value(&self) -> String {
135
14
        let tracks_slice = self.tracks.as_ref();
136
14
        if tracks_slice.is_empty() {
137
            "none".to_string()
138
        } else {
139
14
            tracks_slice
140
14
                .iter()
141
42
                .map(|t| t.print_as_css_value())
142
14
                .collect::<Vec<_>>()
143
14
                .join(" ")
144
        }
145
14
    }
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
    fn default() -> Self {
166
        GridAutoTracks {
167
            tracks: GridTrackSizingVec::from_vec(Vec::new()),
168
        }
169
    }
170
}
171

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

            
187
impl From<GridTemplate> for GridAutoTracks {
188
    fn from(template: GridTemplate) -> Self {
189
        GridAutoTracks {
190
            tracks: template.tracks,
191
        }
192
    }
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
171
    pub fn create(name: AzString, span: Option<i32>) -> Self {
208
171
        Self {
209
171
            grid_line_name: name,
210
171
            span_count: span.unwrap_or(0),
211
171
        }
212
171
    }
213

            
214
    pub fn span(&self) -> Option<i32> {
215
        if self.span_count == 0 {
216
            None
217
        } else {
218
            Some(self.span_count)
219
        }
220
    }
221
}
222

            
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
28
    fn print_as_css_value(&self) -> String {
248
28
        match self {
249
            GridLine::Auto => "auto".to_string(),
250
            GridLine::Line(n) => n.to_string(),
251
28
            GridLine::Named(named) => {
252
28
                if named.span_count == 0 {
253
28
                    named.grid_line_name.as_str().to_string()
254
                } else {
255
                    format!("{} {}", named.grid_line_name.as_str(), named.span_count)
256
                }
257
            }
258
            GridLine::Span(n) => format!("span {}", n),
259
        }
260
28
    }
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
14
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
273
14
        write!(f, "{}", self.print_as_css_value())
274
14
    }
275
}
276

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

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

            
300
#[cfg(feature = "parser")]
301
#[derive(Clone, PartialEq)]
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)]
315
#[repr(C, u8)]
316
pub enum GridParseErrorOwned {
317
    InvalidValue(AzString),
318
}
319

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

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

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

            
344
1019
    for ch in input.chars() {
345
104
        match ch {
346
26
            '(' => { paren_depth += 1; current.push(ch); }
347
26
            ')' => { paren_depth -= 1; if paren_depth < 0 { return Err(()); } current.push(ch); }
348
104
            ' ' if paren_depth == 0 => {
349
78
                if !current.trim().is_empty() {
350
76
                    parts.push(current.trim().to_string());
351
76
                    current.clear();
352
76
                }
353
            }
354
889
            _ => current.push(ch),
355
        }
356
    }
357
82
    if !current.trim().is_empty() {
358
82
        parts.push(current.trim().to_string());
359
82
    }
360
82
    Ok(parts)
361
82
}
362

            
363
#[cfg(feature = "parser")]
364
63
pub fn parse_grid_template<'a>(input: &'a str) -> Result<GridTemplate, GridParseError<'a>> {
365
    use crate::props::basic::pixel::parse_pixel_value;
366

            
367
63
    let input = input.trim();
368

            
369
63
    if input == "none" {
370
1
        return Ok(GridTemplate::default());
371
62
    }
372

            
373
62
    let parts = split_respecting_parens(input)
374
62
        .map_err(|_| GridParseError::InvalidValue(input))?;
375

            
376
62
    let mut tracks = Vec::new();
377
199
    for part in &parts {
378
137
        parse_grid_track_or_repeat(part, &mut tracks)
379
137
            .map_err(|_| GridParseError::InvalidValue(input))?;
380
    }
381

            
382
62
    Ok(GridTemplate {
383
62
        tracks: GridTrackSizingVec::from_vec(tracks),
384
62
    })
385
63
}
386

            
387
/// Parse a single grid track token, which may be `repeat(N, track)` or a plain track.
388
/// For `repeat(N, track_list)`, the tracks are expanded inline.
389
#[cfg(feature = "parser")]
390
137
fn parse_grid_track_or_repeat(input: &str, tracks: &mut Vec<GridTrackSizing>) -> Result<(), ()> {
391
137
    let input = input.trim();
392

            
393
    // Handle repeat(N, track_list)
394
137
    if input.starts_with("repeat(") && input.ends_with(')') {
395
20
        let content = &input[7..input.len() - 1];
396
        // Find the first comma that separates the count from the track list
397
20
        let comma_pos = content.find(',').ok_or(())?;
398
20
        let count_str = content[..comma_pos].trim();
399
20
        let track_list_str = content[comma_pos + 1..].trim();
400

            
401
20
        let count: usize = count_str.parse().map_err(|_| ())?;
402
        const MAX_GRID_REPEAT_COUNT: usize = 10_000;
403
20
        if count == 0 || count > MAX_GRID_REPEAT_COUNT {
404
            return Err(());
405
20
        }
406

            
407
        // Parse the track list (may contain multiple space-separated tracks)
408
20
        let parts = split_respecting_parens(track_list_str)?;
409
20
        let repeat_tracks: Vec<GridTrackSizing> = parts
410
20
            .iter()
411
21
            .map(|p| parse_grid_track_owned(p))
412
20
            .collect::<Result<Vec<_>, _>>()?;
413

            
414
        // Expand: repeat N times
415
51
        for _ in 0..count {
416
51
            tracks.extend(repeat_tracks.iter().cloned());
417
51
        }
418
20
        return Ok(());
419
117
    }
420

            
421
    // Plain single track
422
117
    tracks.push(parse_grid_track_owned(input)?);
423
117
    Ok(())
424
137
}
425

            
426
#[cfg(feature = "parser")]
427
146
fn parse_grid_track_owned(input: &str) -> Result<GridTrackSizing, ()> {
428
    use crate::props::basic::pixel::parse_pixel_value;
429

            
430
146
    let input = input.trim();
431

            
432
146
    if input == "auto" {
433
5
        return Ok(GridTrackSizing::Auto);
434
141
    }
435

            
436
141
    if input == "min-content" {
437
2
        return Ok(GridTrackSizing::MinContent);
438
139
    }
439

            
440
139
    if input == "max-content" {
441
2
        return Ok(GridTrackSizing::MaxContent);
442
137
    }
443

            
444
137
    if input.ends_with("fr") {
445
        /// Fr values are stored as integers scaled by this factor (e.g. `1fr` = 100, `0.5fr` = 50).
446
        const FR_SCALING_FACTOR: f32 = 100.0;
447
57
        let num_str = &input[..input.len() - 2].trim();
448
57
        if let Ok(num) = num_str.parse::<f32>() {
449
57
            let scaled = num * FR_SCALING_FACTOR;
450
57
            if scaled.is_nan() || scaled < (i32::MIN as f32) || scaled > (i32::MAX as f32) {
451
                return Err(());
452
57
            }
453
57
            return Ok(GridTrackSizing::Fr(scaled as i32));
454
        }
455
        return Err(());
456
80
    }
457

            
458
80
    if input.starts_with("minmax(") && input.ends_with(')') {
459
4
        let content = &input[7..input.len() - 1];
460
4
        let parts: Vec<&str> = content.split(',').collect();
461
4
        if parts.len() == 2 {
462
4
            let min = parse_grid_track_owned(parts[0].trim())?;
463
4
            let max = parse_grid_track_owned(parts[1].trim())?;
464
4
            return Ok(GridTrackSizing::MinMax(GridMinMax {
465
4
                min: Box::new(min),
466
4
                max: Box::new(max),
467
4
            }));
468
        }
469
        return Err(());
470
76
    }
471

            
472
76
    if input.starts_with("fit-content(") && input.ends_with(')') {
473
1
        let size_str = &input[12..input.len() - 1].trim();
474
1
        if let Ok(size) = parse_pixel_value(size_str) {
475
1
            return Ok(GridTrackSizing::FitContent(size));
476
        }
477
        return Err(());
478
75
    }
479

            
480
    // Try to parse as pixel value
481
75
    if let Ok(px) = parse_pixel_value(input) {
482
75
        return Ok(GridTrackSizing::Fixed(px));
483
    }
484

            
485
    Err(())
486
146
}
487

            
488
#[cfg(feature = "parser")]
489
10
pub fn parse_grid_placement<'a>(input: &'a str) -> Result<GridPlacement, GridParseError<'a>> {
490
10
    let input = input.trim();
491

            
492
10
    if input == "auto" {
493
1
        return Ok(GridPlacement::default());
494
9
    }
495

            
496
    // Split by "/"
497
13
    let parts: Vec<&str> = input.split('/').map(|s| s.trim()).collect();
498

            
499
9
    let grid_start =
500
9
        parse_grid_line_owned(parts[0]).map_err(|_| GridParseError::InvalidValue(input))?;
501
9
    let grid_end = if parts.len() > 1 {
502
4
        parse_grid_line_owned(parts[1]).map_err(|_| GridParseError::InvalidValue(input))?
503
    } else {
504
5
        GridLine::Auto
505
    };
506

            
507
9
    Ok(GridPlacement {
508
9
        grid_start,
509
9
        grid_end,
510
9
    })
511
10
}
512

            
513
// --- grid-auto-flow ---
514

            
515
/// Represents the `grid-auto-flow` property
516
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
517
#[repr(C)]
518
#[derive(Default)]
519
pub enum LayoutGridAutoFlow {
520
    #[default]
521
    Row,
522
    Column,
523
    RowDense,
524
    ColumnDense,
525
}
526

            
527

            
528
impl crate::props::formatter::PrintAsCssValue for LayoutGridAutoFlow {
529
    fn print_as_css_value(&self) -> alloc::string::String {
530
        match self {
531
            LayoutGridAutoFlow::Row => "row".to_string(),
532
            LayoutGridAutoFlow::Column => "column".to_string(),
533
            LayoutGridAutoFlow::RowDense => "row dense".to_string(),
534
            LayoutGridAutoFlow::ColumnDense => "column dense".to_string(),
535
        }
536
    }
537
}
538

            
539
#[cfg(feature = "parser")]
540
#[derive(Clone, PartialEq)]
541
pub enum GridAutoFlowParseError<'a> {
542
    InvalidValue(&'a str),
543
}
544

            
545
#[cfg(feature = "parser")]
546
impl_debug_as_display!(GridAutoFlowParseError<'a>);
547
#[cfg(feature = "parser")]
548
impl_display! { GridAutoFlowParseError<'a>, {
549
    InvalidValue(e) => format!("Invalid grid-auto-flow value: \"{}\"", e),
550
}}
551

            
552
#[cfg(feature = "parser")]
553
#[derive(Debug, Clone, PartialEq)]
554
#[repr(C, u8)]
555
pub enum GridAutoFlowParseErrorOwned {
556
    InvalidValue(AzString),
557
}
558

            
559
#[cfg(feature = "parser")]
560
impl<'a> GridAutoFlowParseError<'a> {
561
    pub fn to_contained(&self) -> GridAutoFlowParseErrorOwned {
562
        match self {
563
            GridAutoFlowParseError::InvalidValue(s) => {
564
                GridAutoFlowParseErrorOwned::InvalidValue(s.to_string().into())
565
            }
566
        }
567
    }
568
}
569

            
570
#[cfg(feature = "parser")]
571
impl GridAutoFlowParseErrorOwned {
572
    pub fn to_shared<'a>(&'a self) -> GridAutoFlowParseError<'a> {
573
        match self {
574
            GridAutoFlowParseErrorOwned::InvalidValue(s) => {
575
                GridAutoFlowParseError::InvalidValue(s.as_str())
576
            }
577
        }
578
    }
579
}
580

            
581
#[cfg(feature = "parser")]
582
pub fn parse_layout_grid_auto_flow<'a>(
583
    input: &'a str,
584
) -> Result<LayoutGridAutoFlow, GridAutoFlowParseError<'a>> {
585
    match input.trim() {
586
        "row" => Ok(LayoutGridAutoFlow::Row),
587
        "column" => Ok(LayoutGridAutoFlow::Column),
588
        "row dense" => Ok(LayoutGridAutoFlow::RowDense),
589
        "column dense" => Ok(LayoutGridAutoFlow::ColumnDense),
590
        "dense" => Ok(LayoutGridAutoFlow::RowDense),
591
        _ => Err(GridAutoFlowParseError::InvalidValue(input)),
592
    }
593
}
594

            
595
// --- justify-self / justify-items ---
596

            
597
/// Represents `justify-self` for grid items
598
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
599
#[repr(C)]
600
#[derive(Default)]
601
pub enum LayoutJustifySelf {
602
    #[default]
603
    Auto,
604
    Start,
605
    End,
606
    Center,
607
    Stretch,
608
}
609

            
610

            
611
impl crate::props::formatter::PrintAsCssValue for LayoutJustifySelf {
612
    fn print_as_css_value(&self) -> alloc::string::String {
613
        match self {
614
            LayoutJustifySelf::Auto => "auto".to_string(),
615
            LayoutJustifySelf::Start => "start".to_string(),
616
            LayoutJustifySelf::End => "end".to_string(),
617
            LayoutJustifySelf::Center => "center".to_string(),
618
            LayoutJustifySelf::Stretch => "stretch".to_string(),
619
        }
620
    }
621
}
622

            
623
#[cfg(feature = "parser")]
624
#[derive(Clone, PartialEq)]
625
pub enum JustifySelfParseError<'a> {
626
    InvalidValue(&'a str),
627
}
628

            
629
#[cfg(feature = "parser")]
630
#[derive(Debug, Clone, PartialEq)]
631
#[repr(C, u8)]
632
pub enum JustifySelfParseErrorOwned {
633
    InvalidValue(AzString),
634
}
635

            
636
#[cfg(feature = "parser")]
637
impl<'a> JustifySelfParseError<'a> {
638
    pub fn to_contained(&self) -> JustifySelfParseErrorOwned {
639
        match self {
640
            JustifySelfParseError::InvalidValue(s) => {
641
                JustifySelfParseErrorOwned::InvalidValue(s.to_string().into())
642
            }
643
        }
644
    }
645
}
646

            
647
#[cfg(feature = "parser")]
648
impl JustifySelfParseErrorOwned {
649
    pub fn to_shared<'a>(&'a self) -> JustifySelfParseError<'a> {
650
        match self {
651
            JustifySelfParseErrorOwned::InvalidValue(s) => {
652
                JustifySelfParseError::InvalidValue(s.as_str())
653
            }
654
        }
655
    }
656
}
657

            
658
#[cfg(feature = "parser")]
659
impl_debug_as_display!(JustifySelfParseError<'a>);
660
#[cfg(feature = "parser")]
661
impl_display! { JustifySelfParseError<'a>, {
662
    InvalidValue(e) => format!("Invalid justify-self value: \"{}\"", e),
663
}}
664

            
665
#[cfg(feature = "parser")]
666
pub fn parse_layout_justify_self<'a>(
667
    input: &'a str,
668
) -> Result<LayoutJustifySelf, JustifySelfParseError<'a>> {
669
    match input.trim() {
670
        "auto" => Ok(LayoutJustifySelf::Auto),
671
        "start" | "flex-start" => Ok(LayoutJustifySelf::Start),
672
        "end" | "flex-end" => Ok(LayoutJustifySelf::End),
673
        "center" => Ok(LayoutJustifySelf::Center),
674
        "stretch" => Ok(LayoutJustifySelf::Stretch),
675
        _ => Err(JustifySelfParseError::InvalidValue(input)),
676
    }
677
}
678

            
679
/// Represents `justify-items` for grid containers
680
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
681
#[repr(C)]
682
#[derive(Default)]
683
pub enum LayoutJustifyItems {
684
    Start,
685
    End,
686
    Center,
687
    #[default]
688
    Stretch,
689
}
690

            
691

            
692
impl crate::props::formatter::PrintAsCssValue for LayoutJustifyItems {
693
    fn print_as_css_value(&self) -> alloc::string::String {
694
        match self {
695
            LayoutJustifyItems::Start => "start".to_string(),
696
            LayoutJustifyItems::End => "end".to_string(),
697
            LayoutJustifyItems::Center => "center".to_string(),
698
            LayoutJustifyItems::Stretch => "stretch".to_string(),
699
        }
700
    }
701
}
702

            
703
#[cfg(feature = "parser")]
704
#[derive(Clone, PartialEq)]
705
pub enum JustifyItemsParseError<'a> {
706
    InvalidValue(&'a str),
707
}
708

            
709
#[cfg(feature = "parser")]
710
#[derive(Debug, Clone, PartialEq)]
711
#[repr(C, u8)]
712
pub enum JustifyItemsParseErrorOwned {
713
    InvalidValue(AzString),
714
}
715

            
716
#[cfg(feature = "parser")]
717
impl<'a> JustifyItemsParseError<'a> {
718
    pub fn to_contained(&self) -> JustifyItemsParseErrorOwned {
719
        match self {
720
            JustifyItemsParseError::InvalidValue(s) => {
721
                JustifyItemsParseErrorOwned::InvalidValue(s.to_string().into())
722
            }
723
        }
724
    }
725
}
726

            
727
#[cfg(feature = "parser")]
728
impl JustifyItemsParseErrorOwned {
729
    pub fn to_shared<'a>(&'a self) -> JustifyItemsParseError<'a> {
730
        match self {
731
            JustifyItemsParseErrorOwned::InvalidValue(s) => {
732
                JustifyItemsParseError::InvalidValue(s.as_str())
733
            }
734
        }
735
    }
736
}
737

            
738
#[cfg(feature = "parser")]
739
impl_debug_as_display!(JustifyItemsParseError<'a>);
740
#[cfg(feature = "parser")]
741
impl_display! { JustifyItemsParseError<'a>, {
742
    InvalidValue(e) => format!("Invalid justify-items value: \"{}\"", e),
743
}}
744

            
745
#[cfg(feature = "parser")]
746
pub fn parse_layout_justify_items<'a>(
747
    input: &'a str,
748
) -> Result<LayoutJustifyItems, JustifyItemsParseError<'a>> {
749
    match input.trim() {
750
        "start" => Ok(LayoutJustifyItems::Start),
751
        "end" => Ok(LayoutJustifyItems::End),
752
        "center" => Ok(LayoutJustifyItems::Center),
753
        "stretch" => Ok(LayoutJustifyItems::Stretch),
754
        _ => Err(JustifyItemsParseError::InvalidValue(input)),
755
    }
756
}
757

            
758
// --- gap (single value type) ---
759

            
760
#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
761
#[repr(C)]
762
pub struct LayoutGap {
763
    pub inner: crate::props::basic::pixel::PixelValue,
764
}
765

            
766
impl core::fmt::Debug for LayoutGap {
767
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
768
        write!(f, "{}", self.inner)
769
    }
770
}
771

            
772
impl crate::props::formatter::PrintAsCssValue for LayoutGap {
773
    fn print_as_css_value(&self) -> alloc::string::String {
774
        self.inner.print_as_css_value()
775
    }
776
}
777

            
778
// Implement FormatAsRustCode for the new types so they can be emitted by the
779
// code generator.
780
impl FormatAsRustCode for LayoutGridAutoFlow {
781
    fn format_as_rust_code(&self, _tabs: usize) -> String {
782
        format!(
783
            "LayoutGridAutoFlow::{}",
784
            match self {
785
                LayoutGridAutoFlow::Row => "Row",
786
                LayoutGridAutoFlow::Column => "Column",
787
                LayoutGridAutoFlow::RowDense => "RowDense",
788
                LayoutGridAutoFlow::ColumnDense => "ColumnDense",
789
            }
790
        )
791
    }
792
}
793

            
794
impl FormatAsRustCode for LayoutJustifySelf {
795
    fn format_as_rust_code(&self, _tabs: usize) -> String {
796
        format!(
797
            "LayoutJustifySelf::{}",
798
            match self {
799
                LayoutJustifySelf::Auto => "Auto",
800
                LayoutJustifySelf::Start => "Start",
801
                LayoutJustifySelf::End => "End",
802
                LayoutJustifySelf::Center => "Center",
803
                LayoutJustifySelf::Stretch => "Stretch",
804
            }
805
        )
806
    }
807
}
808

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

            
823
impl FormatAsRustCode for LayoutGap {
824
    fn format_as_rust_code(&self, _tabs: usize) -> String {
825
        use crate::format_rust_code::format_pixel_value;
826
        format!("LayoutGap {{ inner: {} }}", format_pixel_value(&self.inner))
827
    }
828
}
829

            
830
impl FormatAsRustCode for GridTrackSizing {
831
    fn format_as_rust_code(&self, tabs: usize) -> String {
832
        use crate::format_rust_code::format_pixel_value;
833
        match self {
834
            GridTrackSizing::Fixed(pv) => {
835
                format!("GridTrackSizing::Fixed({})", format_pixel_value(pv))
836
            }
837
            GridTrackSizing::Fr(f) => format!("GridTrackSizing::Fr({})", f),
838
            GridTrackSizing::MinContent => "GridTrackSizing::MinContent".to_string(),
839
            GridTrackSizing::MaxContent => "GridTrackSizing::MaxContent".to_string(),
840
            GridTrackSizing::Auto => "GridTrackSizing::Auto".to_string(),
841
            GridTrackSizing::MinMax(minmax) => {
842
                format!(
843
                    "GridTrackSizing::MinMax(GridMinMax {{ min: Box::new({}), max: Box::new({}) }})",
844
                    minmax.min.format_as_rust_code(tabs),
845
                    minmax.max.format_as_rust_code(tabs)
846
                )
847
            }
848
            GridTrackSizing::FitContent(pv) => {
849
                format!("GridTrackSizing::FitContent({})", format_pixel_value(pv))
850
            }
851
        }
852
    }
853
}
854

            
855
impl FormatAsRustCode for GridAutoTracks {
856
    fn format_as_rust_code(&self, tabs: usize) -> String {
857
        let tracks: Vec<String> = self
858
            .tracks
859
            .as_ref()
860
            .iter()
861
            .map(|t| t.format_as_rust_code(tabs))
862
            .collect();
863
        format!(
864
            "GridAutoTracks {{ tracks: GridTrackSizingVec::from_vec(vec![{}]) }}",
865
            tracks.join(", ")
866
        )
867
    }
868
}
869

            
870
impl FormatAsRustCode for GridTemplateAreas {
871
    fn format_as_rust_code(&self, _tabs: usize) -> String {
872
        format!("GridTemplateAreas {{ areas: GridAreaDefinitionVec::from_vec(vec!{:?}) }}", self.areas.as_ref())
873
    }
874
}
875

            
876
#[cfg(feature = "parser")]
877
1
pub fn parse_layout_gap<'a>(
878
1
    input: &'a str,
879
1
) -> Result<LayoutGap, crate::props::basic::pixel::CssPixelValueParseError<'a>> {
880
1
    crate::props::basic::pixel::parse_pixel_value(input).map(|p| LayoutGap { inner: p })
881
1
}
882

            
883
#[cfg(feature = "parser")]
884
181
pub fn parse_grid_line_owned(input: &str) -> Result<GridLine, ()> {
885
181
    let input = input.trim();
886

            
887
181
    if input == "auto" {
888
        return Ok(GridLine::Auto);
889
181
    }
890

            
891
181
    if input.starts_with("span ") {
892
2
        let num_str = &input[5..].trim();
893
2
        if let Ok(num) = num_str.parse::<i32>() {
894
2
            return Ok(GridLine::Span(num));
895
        }
896
        return Err(());
897
179
    }
898

            
899
    // Try to parse as line number
900
179
    if let Ok(num) = input.parse::<i32>() {
901
8
        return Ok(GridLine::Line(num));
902
171
    }
903

            
904
    // Otherwise treat as named line
905
171
    Ok(GridLine::Named(NamedGridLine::create(
906
171
        input.to_string().into(),
907
171
        None,
908
171
    )))
909
181
}
910

            
911
#[cfg(all(test, feature = "parser"))]
912
mod tests {
913
    use super::*;
914

            
915
    // Grid template tests
916
    #[test]
917
1
    fn test_parse_grid_template_none() {
918
1
        let result = parse_grid_template("none").unwrap();
919
1
        assert_eq!(result.tracks.len(), 0);
920
1
    }
921

            
922
    #[test]
923
1
    fn test_parse_grid_template_single_px() {
924
1
        let result = parse_grid_template("100px").unwrap();
925
1
        assert_eq!(result.tracks.len(), 1);
926
1
        assert!(matches!(
927
1
            result.tracks.as_ref()[0],
928
            GridTrackSizing::Fixed(_)
929
        ));
930
1
    }
931

            
932
    #[test]
933
1
    fn test_parse_grid_template_multiple_tracks() {
934
1
        let result = parse_grid_template("100px 200px 1fr").unwrap();
935
1
        assert_eq!(result.tracks.len(), 3);
936
1
    }
937

            
938
    #[test]
939
1
    fn test_parse_grid_template_fr_units() {
940
1
        let result = parse_grid_template("1fr 2fr 1fr").unwrap();
941
1
        assert_eq!(result.tracks.len(), 3);
942
1
        assert!(matches!(
943
1
            result.tracks.as_ref()[0],
944
            GridTrackSizing::Fr(100)
945
        ));
946
1
        assert!(matches!(
947
1
            result.tracks.as_ref()[1],
948
            GridTrackSizing::Fr(200)
949
        ));
950
1
    }
951

            
952
    #[test]
953
1
    fn test_parse_grid_template_fractional_fr() {
954
1
        let result = parse_grid_template("0.5fr 1.5fr").unwrap();
955
1
        assert_eq!(result.tracks.len(), 2);
956
1
        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::Fr(50)));
957
1
        assert!(matches!(
958
1
            result.tracks.as_ref()[1],
959
            GridTrackSizing::Fr(150)
960
        ));
961
1
    }
962

            
963
    #[test]
964
1
    fn test_parse_grid_template_auto() {
965
1
        let result = parse_grid_template("auto 100px auto").unwrap();
966
1
        assert_eq!(result.tracks.len(), 3);
967
1
        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::Auto));
968
1
        assert!(matches!(result.tracks.as_ref()[2], GridTrackSizing::Auto));
969
1
    }
970

            
971
    #[test]
972
1
    fn test_parse_grid_template_min_max_content() {
973
1
        let result = parse_grid_template("min-content max-content auto").unwrap();
974
1
        assert_eq!(result.tracks.len(), 3);
975
1
        assert!(matches!(
976
1
            result.tracks.as_ref()[0],
977
            GridTrackSizing::MinContent
978
        ));
979
1
        assert!(matches!(
980
1
            result.tracks.as_ref()[1],
981
            GridTrackSizing::MaxContent
982
        ));
983
1
    }
984

            
985
    #[test]
986
1
    fn test_parse_grid_template_minmax() {
987
1
        let result = parse_grid_template("minmax(100px, 1fr)").unwrap();
988
1
        assert_eq!(result.tracks.len(), 1);
989
1
        assert!(matches!(
990
1
            result.tracks.as_ref()[0],
991
            GridTrackSizing::MinMax(_)
992
        ));
993
1
    }
994

            
995
    #[test]
996
1
    fn test_parse_grid_template_minmax_complex() {
997
1
        let result = parse_grid_template("minmax(min-content, max-content)").unwrap();
998
1
        assert_eq!(result.tracks.len(), 1);
999
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,
    [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 {
    fn default() -> Self {
        GridTemplateAreas { areas: GridAreaDefinitionVec::from_vec(Vec::new()) }
    }
}
impl PrintAsCssValue for GridTemplateAreas {
    fn print_as_css_value(&self) -> String {
        let areas_slice = self.areas.as_ref();
        if areas_slice.is_empty() {
            return "none".to_string();
        }
        // Reconstruct the row strings from the area definitions
        let max_row = areas_slice.iter().map(|a| a.row_end).max().unwrap_or(1);
        let max_col = areas_slice.iter().map(|a| a.column_end).max().unwrap_or(1);
        let num_rows = (max_row - 1) as usize;
        let num_cols = (max_col - 1) as usize;
        let mut grid: Vec<Vec<String>> = vec![vec![".".to_string(); num_cols]; num_rows];
        for area in areas_slice {
            for r in (area.row_start as usize - 1)..(area.row_end as usize - 1) {
                for c in (area.column_start as usize - 1)..(area.column_end as usize - 1) {
                    if r < num_rows && c < num_cols {
                        grid[r][c] = area.name.as_str().to_string();
                    }
                }
            }
        }
        grid.iter()
            .map(|row| format!("\"{}\"", row.join(" ")))
            .collect::<Vec<_>>()
            .join(" ")
    }
}
/// 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")]
14
pub fn parse_grid_template_areas(input: &str) -> Result<GridTemplateAreas, ()> {
14
    let input = input.trim();
14
    if input == "none" {
        return Ok(GridTemplateAreas::default());
14
    }
    // Extract quoted strings: each one is a row
14
    let mut rows: Vec<Vec<String>> = Vec::new();
14
    let mut i = 0;
14
    let bytes = input.as_bytes();
476
    while i < bytes.len() {
462
        if bytes[i] == b'"' || bytes[i] == b'\'' {
42
            let quote = bytes[i];
42
            i += 1;
42
            let start = i;
854
            while i < bytes.len() && bytes[i] != quote {
812
                i += 1;
812
            }
42
            if i >= bytes.len() {
                return Err(());
42
            }
42
            let row_str = &input[start..i];
126
            let cells: Vec<String> = row_str.split_whitespace().map(|s| s.to_string()).collect();
42
            if cells.is_empty() {
                return Err(());
42
            }
42
            rows.push(cells);
42
            i += 1; // skip closing quote
420
        } else {
420
            i += 1;
420
        }
    }
14
    if rows.is_empty() {
        return Err(());
14
    }
    // Validate: all rows must have the same number of columns
14
    let num_cols = rows[0].len();
56
    for row in &rows {
42
        if row.len() != num_cols {
            return Err(());
42
        }
    }
    // Build area map: name -> (min_row, max_row, min_col, max_col) in 0-based indices
    use alloc::collections::BTreeMap;
14
    let mut area_map: BTreeMap<String, (usize, usize, usize, usize)> = BTreeMap::new();
42
    for (row_idx, row) in rows.iter().enumerate() {
126
        for (col_idx, cell) in row.iter().enumerate() {
126
            if cell == "." {
                continue; // skip null cell tokens
126
            }
126
            let entry = area_map.entry(cell.clone()).or_insert((row_idx, row_idx, col_idx, col_idx));
126
            entry.0 = entry.0.min(row_idx);
126
            entry.1 = entry.1.max(row_idx);
126
            entry.2 = entry.2.min(col_idx);
126
            entry.3 = entry.3.max(col_idx);
        }
    }
    // Convert to 1-based grid line numbers (taffy convention)
14
    let mut areas = Vec::new();
84
    for (name, (min_row, max_row, min_col, max_col)) in area_map {
70
        areas.push(GridAreaDefinition {
70
            name: name.into(),
70
            row_start: (min_row + 1) as u16,
70
            row_end: (max_row + 2) as u16,   // end line is one past the last cell
70
            column_start: (min_col + 1) as u16,
70
            column_end: (max_col + 2) as u16,
70
        });
70
    }
14
    Ok(GridTemplateAreas { areas: GridAreaDefinitionVec::from_vec(areas) })
14
}