Skip to main content

azul_css/props/layout/
grid.rs

1//! CSS properties for CSS Grid layout.
2
3use alloc::{
4    boxed::Box,
5    string::{String, ToString},
6    vec::Vec,
7};
8
9use crate::{
10    codegen::format::FormatAsRustCode,
11    corety::AzString,
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)]
22pub struct GridMinMax {
23    pub min: Box<GridTrackSizing>,
24    pub max: Box<GridTrackSizing>,
25}
26
27impl 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)]
42pub 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
61impl_option!(
62    GridTrackSizing,
63    OptionGridTrackSizing,
64    copy = false,
65    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
66);
67
68impl 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
74impl PrintAsCssValue for GridTrackSizing {
75    fn print_as_css_value(&self) -> String {
76        match self {
77            Self::Fixed(px) => px.print_as_css_value(),
78            Self::Fr(f) => format!("{f}fr"),
79            Self::MinContent => "min-content".to_string(),
80            Self::MaxContent => "max-content".to_string(),
81            Self::Auto => "auto".to_string(),
82            Self::MinMax(minmax) => {
83                format!(
84                    "minmax({}, {})",
85                    minmax.min.print_as_css_value(),
86                    minmax.max.print_as_css_value()
87                )
88            }
89            Self::FitContent(size) => {
90                format!("fit-content({})", size.print_as_css_value())
91            }
92        }
93    }
94}
95
96// C-compatible Vec for GridTrackSizing
97impl_vec!(
98    GridTrackSizing,
99    GridTrackSizingVec,
100    GridTrackSizingVecDestructor,
101    GridTrackSizingVecDestructorType,
102    GridTrackSizingVecSlice,
103    OptionGridTrackSizing
104);
105impl_vec_clone!(
106    GridTrackSizing,
107    GridTrackSizingVec,
108    GridTrackSizingVecDestructor
109);
110impl_vec_debug!(GridTrackSizing, GridTrackSizingVec);
111impl_vec_partialeq!(GridTrackSizing, GridTrackSizingVec);
112impl_vec_eq!(GridTrackSizing, GridTrackSizingVec);
113impl_vec_partialord!(GridTrackSizing, GridTrackSizingVec);
114impl_vec_ord!(GridTrackSizing, GridTrackSizingVec);
115impl_vec_hash!(GridTrackSizing, GridTrackSizingVec);
116impl_vec_mut!(GridTrackSizing, GridTrackSizingVec);
117
118/// Represents `grid-template-columns` or `grid-template-rows`
119#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
120#[repr(C)]
121pub struct GridTemplate {
122    pub tracks: GridTrackSizingVec,
123}
124
125impl core::fmt::Debug for GridTemplate {
126    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
127        write!(f, "{}", self.print_as_css_value())
128    }
129}
130
131impl Default for GridTemplate {
132    fn default() -> Self {
133        Self {
134            tracks: GridTrackSizingVec::from_vec(Vec::new()),
135        }
136    }
137}
138
139impl PrintAsCssValue for GridTemplate {
140    fn print_as_css_value(&self) -> String {
141        let tracks_slice = self.tracks.as_ref();
142        if tracks_slice.is_empty() {
143            "none".to_string()
144        } else {
145            tracks_slice
146                .iter()
147                .map(PrintAsCssValue::print_as_css_value)
148                .collect::<Vec<_>>()
149                .join(" ")
150        }
151    }
152}
153
154// --- grid-auto-columns / grid-auto-rows ---
155
156/// Represents `grid-auto-columns` or `grid-auto-rows`
157/// Structurally identical to `GridTemplate` but semantically different
158#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
159#[repr(C)]
160pub struct GridAutoTracks {
161    pub tracks: GridTrackSizingVec,
162}
163
164impl core::fmt::Debug for GridAutoTracks {
165    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
166        write!(f, "{}", self.print_as_css_value())
167    }
168}
169
170impl Default for GridAutoTracks {
171    fn default() -> Self {
172        Self {
173            tracks: GridTrackSizingVec::from_vec(Vec::new()),
174        }
175    }
176}
177
178impl PrintAsCssValue for GridAutoTracks {
179    fn print_as_css_value(&self) -> String {
180        let tracks_slice = self.tracks.as_ref();
181        if tracks_slice.is_empty() {
182            "auto".to_string()
183        } else {
184            tracks_slice
185                .iter()
186                .map(PrintAsCssValue::print_as_css_value)
187                .collect::<Vec<_>>()
188                .join(" ")
189        }
190    }
191}
192
193impl From<GridTemplate> for GridAutoTracks {
194    fn from(template: GridTemplate) -> Self {
195        Self {
196            tracks: template.tracks,
197        }
198    }
199}
200
201// --- grid-row / grid-column (grid line placement) ---
202
203/// Named grid line with optional span count (FFI-safe wrapper)
204#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
205#[repr(C)]
206pub struct NamedGridLine {
207    pub grid_line_name: AzString,
208    /// Span count, 0 means no span specified
209    pub span_count: i32,
210}
211
212impl NamedGridLine {
213    #[must_use]
214    pub fn create(name: AzString, span: Option<i32>) -> Self {
215        Self {
216            grid_line_name: name,
217            span_count: span.unwrap_or(0),
218        }
219    }
220
221    #[must_use]
222    pub const fn span(&self) -> Option<i32> {
223        if self.span_count == 0 {
224            None
225        } else {
226            Some(self.span_count)
227        }
228    }
229}
230#[allow(variant_size_differences)]
231// repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
232/// Represents a grid line position (start or end)
233#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
234#[repr(C, u8)]
235#[derive(Default)]
236pub enum GridLine {
237    /// auto
238    #[default]
239    Auto,
240    /// Line number (1-based, negative for counting from end)
241    Line(i32),
242    /// Named line with optional span count
243    Named(NamedGridLine),
244    /// span N
245    Span(i32),
246}
247
248impl core::fmt::Debug for GridLine {
249    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
250        write!(f, "{}", self.print_as_css_value())
251    }
252}
253
254impl PrintAsCssValue for GridLine {
255    fn print_as_css_value(&self) -> String {
256        match self {
257            Self::Auto => "auto".to_string(),
258            Self::Line(n) => n.to_string(),
259            Self::Named(named) => {
260                if named.span_count == 0 {
261                    named.grid_line_name.as_str().to_string()
262                } else {
263                    format!("{} {}", named.grid_line_name.as_str(), named.span_count)
264                }
265            }
266            Self::Span(n) => format!("span {n}"),
267        }
268    }
269}
270
271/// Represents `grid-row` or `grid-column` (start / end)
272#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
273#[repr(C)]
274pub struct GridPlacement {
275    pub grid_start: GridLine,
276    pub grid_end: GridLine,
277}
278
279impl core::fmt::Debug for GridPlacement {
280    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
281        write!(f, "{}", self.print_as_css_value())
282    }
283}
284
285impl Default for GridPlacement {
286    fn default() -> Self {
287        Self {
288            grid_start: GridLine::Auto,
289            grid_end: GridLine::Auto,
290        }
291    }
292}
293
294impl PrintAsCssValue for GridPlacement {
295    fn print_as_css_value(&self) -> String {
296        if self.grid_end == GridLine::Auto {
297            self.grid_start.print_as_css_value()
298        } else {
299            format!(
300                "{} / {}",
301                self.grid_start.print_as_css_value(),
302                self.grid_end.print_as_css_value()
303            )
304        }
305    }
306}
307
308#[cfg(feature = "parser")]
309#[derive(Clone, PartialEq, Eq)]
310pub enum GridParseError<'a> {
311    InvalidValue(&'a str),
312}
313
314#[cfg(feature = "parser")]
315impl_debug_as_display!(GridParseError<'a>);
316#[cfg(feature = "parser")]
317impl_display! { GridParseError<'a>, {
318    InvalidValue(e) => format!("Invalid grid value: \"{}\"", e),
319}}
320
321#[cfg(feature = "parser")]
322#[derive(Debug, Clone, PartialEq, Eq)]
323#[repr(C, u8)]
324pub enum GridParseErrorOwned {
325    InvalidValue(AzString),
326}
327
328#[cfg(feature = "parser")]
329impl GridParseError<'_> {
330    #[must_use]
331    pub fn to_contained(&self) -> GridParseErrorOwned {
332        match self {
333            GridParseError::InvalidValue(s) => {
334                GridParseErrorOwned::InvalidValue((*s).to_string().into())
335            }
336        }
337    }
338}
339
340#[cfg(feature = "parser")]
341impl GridParseErrorOwned {
342    #[must_use]
343    pub fn to_shared(&self) -> GridParseError<'_> {
344        match self {
345            Self::InvalidValue(s) => GridParseError::InvalidValue(s.as_str()),
346        }
347    }
348}
349
350#[cfg(feature = "parser")]
351fn split_respecting_parens(input: &str) -> Result<Vec<String>, ()> {
352    let mut parts = Vec::new();
353    let mut current = String::new();
354    let mut paren_depth: i32 = 0;
355
356    for ch in input.chars() {
357        match ch {
358            '(' => {
359                paren_depth += 1;
360                current.push(ch);
361            }
362            ')' => {
363                paren_depth -= 1;
364                if paren_depth < 0 {
365                    return Err(());
366                }
367                current.push(ch);
368            }
369            ' ' if paren_depth == 0 => {
370                if !current.trim().is_empty() {
371                    parts.push(current.trim().to_string());
372                    current.clear();
373                }
374            }
375            _ => current.push(ch),
376        }
377    }
378    if !current.trim().is_empty() {
379        parts.push(current.trim().to_string());
380    }
381    Ok(parts)
382}
383
384#[cfg(feature = "parser")]
385/// # Errors
386///
387/// Returns an error if `input` is not a valid CSS `grid-template` value.
388pub fn parse_grid_template(input: &str) -> Result<GridTemplate, GridParseError<'_>> {
389    use crate::props::basic::pixel::parse_pixel_value;
390
391    let input = input.trim();
392
393    if input == "none" {
394        return Ok(GridTemplate::default());
395    }
396
397    let parts = split_respecting_parens(input).map_err(|()| GridParseError::InvalidValue(input))?;
398
399    let mut tracks = Vec::new();
400    for part in &parts {
401        parse_grid_track_or_repeat(part, &mut tracks)
402            .map_err(|()| GridParseError::InvalidValue(input))?;
403    }
404
405    Ok(GridTemplate {
406        tracks: GridTrackSizingVec::from_vec(tracks),
407    })
408}
409
410/// Parse a single grid track token, which may be `repeat(N, track)` or a plain track.
411/// For `repeat(N, track_list)`, the tracks are expanded inline.
412#[cfg(feature = "parser")]
413fn parse_grid_track_or_repeat(input: &str, tracks: &mut Vec<GridTrackSizing>) -> Result<(), ()> {
414    // Maximum repeat count accepted in `repeat(N, …)` to bound expansion.
415    const MAX_GRID_REPEAT_COUNT: usize = 10_000;
416    let input = input.trim();
417
418    // Handle repeat(N, track_list)
419    if input.starts_with("repeat(") && input.ends_with(')') {
420        let content = &input[7..input.len() - 1];
421        // Find the first comma that separates the count from the track list
422        let comma_pos = content.find(',').ok_or(())?;
423        let count_str = content[..comma_pos].trim();
424        let track_list_str = content[comma_pos + 1..].trim();
425
426        let count: usize = count_str.parse().map_err(|_| ())?;
427        if count == 0 || count > MAX_GRID_REPEAT_COUNT {
428            return Err(());
429        }
430
431        // Parse the track list (may contain multiple space-separated tracks)
432        let parts = split_respecting_parens(track_list_str)?;
433        let repeat_tracks: Vec<GridTrackSizing> = parts
434            .iter()
435            .map(|p| parse_grid_track_owned(p))
436            .collect::<Result<Vec<_>, _>>()?;
437
438        // Expand: repeat N times
439        for _ in 0..count {
440            tracks.extend(repeat_tracks.iter().cloned());
441        }
442        return Ok(());
443    }
444
445    // Plain single track
446    tracks.push(parse_grid_track_owned(input)?);
447    Ok(())
448}
449
450#[cfg(feature = "parser")]
451fn parse_grid_track_owned(input: &str) -> Result<GridTrackSizing, ()> {
452    use crate::props::basic::pixel::parse_pixel_value;
453
454    let input = input.trim();
455
456    if input == "auto" {
457        return Ok(GridTrackSizing::Auto);
458    }
459
460    if input == "min-content" {
461        return Ok(GridTrackSizing::MinContent);
462    }
463
464    if input == "max-content" {
465        return Ok(GridTrackSizing::MaxContent);
466    }
467
468    if let Some(num_str) = input.strip_suffix("fr") {
469        /// Fr values are stored as integers scaled by this factor (e.g. `1fr` = 100, `0.5fr` = 50).
470        const FR_SCALING_FACTOR: f32 = 100.0;
471        let num_str = num_str.trim();
472        if let Ok(num) = num_str.parse::<f32>() {
473            let scaled = num * FR_SCALING_FACTOR;
474            if scaled.is_nan()
475                || scaled < crate::cast::i32_to_f32(i32::MIN)
476                || scaled > crate::cast::i32_to_f32(i32::MAX)
477            {
478                return Err(());
479            }
480            return Ok(GridTrackSizing::Fr(crate::cast::f32_to_i32(scaled)));
481        }
482        return Err(());
483    }
484
485    if input.starts_with("minmax(") && input.ends_with(')') {
486        let content = &input[7..input.len() - 1];
487        let parts: Vec<&str> = content.split(',').collect();
488        if parts.len() == 2 {
489            let min = parse_grid_track_owned(parts[0].trim())?;
490            let max = parse_grid_track_owned(parts[1].trim())?;
491            return Ok(GridTrackSizing::MinMax(GridMinMax {
492                min: Box::new(min),
493                max: Box::new(max),
494            }));
495        }
496        return Err(());
497    }
498
499    if input.starts_with("fit-content(") && input.ends_with(')') {
500        let size_str = &input[12..input.len() - 1].trim();
501        if let Ok(size) = parse_pixel_value(size_str) {
502            return Ok(GridTrackSizing::FitContent(size));
503        }
504        return Err(());
505    }
506
507    // Try to parse as pixel value
508    if let Ok(px) = parse_pixel_value(input) {
509        return Ok(GridTrackSizing::Fixed(px));
510    }
511
512    Err(())
513}
514
515#[cfg(feature = "parser")]
516/// # Errors
517///
518/// Returns an error if `input` is not a valid CSS `grid-placement` value.
519pub fn parse_grid_placement(input: &str) -> Result<GridPlacement, GridParseError<'_>> {
520    let input = input.trim();
521
522    if input == "auto" {
523        return Ok(GridPlacement::default());
524    }
525
526    // Split by "/"
527    let parts: Vec<&str> = input.split('/').map(str::trim).collect();
528
529    let grid_start =
530        parse_grid_line_owned(parts[0]).map_err(|()| GridParseError::InvalidValue(input))?;
531    let grid_end = if parts.len() > 1 {
532        parse_grid_line_owned(parts[1]).map_err(|()| GridParseError::InvalidValue(input))?
533    } else {
534        GridLine::Auto
535    };
536
537    Ok(GridPlacement {
538        grid_start,
539        grid_end,
540    })
541}
542
543// --- grid-auto-flow ---
544
545/// Represents the `grid-auto-flow` property
546#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
547#[repr(C)]
548#[derive(Default)]
549pub enum LayoutGridAutoFlow {
550    #[default]
551    Row,
552    Column,
553    RowDense,
554    ColumnDense,
555}
556
557impl PrintAsCssValue for LayoutGridAutoFlow {
558    fn print_as_css_value(&self) -> String {
559        match self {
560            Self::Row => "row".to_string(),
561            Self::Column => "column".to_string(),
562            Self::RowDense => "row dense".to_string(),
563            Self::ColumnDense => "column dense".to_string(),
564        }
565    }
566}
567
568#[cfg(feature = "parser")]
569#[derive(Clone, PartialEq, Eq)]
570pub enum GridAutoFlowParseError<'a> {
571    InvalidValue(&'a str),
572}
573
574#[cfg(feature = "parser")]
575impl_debug_as_display!(GridAutoFlowParseError<'a>);
576#[cfg(feature = "parser")]
577impl_display! { GridAutoFlowParseError<'a>, {
578    InvalidValue(e) => format!("Invalid grid-auto-flow value: \"{}\"", e),
579}}
580
581#[cfg(feature = "parser")]
582#[derive(Debug, Clone, PartialEq, Eq)]
583#[repr(C, u8)]
584pub enum GridAutoFlowParseErrorOwned {
585    InvalidValue(AzString),
586}
587
588#[cfg(feature = "parser")]
589impl GridAutoFlowParseError<'_> {
590    #[must_use]
591    pub fn to_contained(&self) -> GridAutoFlowParseErrorOwned {
592        match self {
593            GridAutoFlowParseError::InvalidValue(s) => {
594                GridAutoFlowParseErrorOwned::InvalidValue((*s).to_string().into())
595            }
596        }
597    }
598}
599
600#[cfg(feature = "parser")]
601impl GridAutoFlowParseErrorOwned {
602    #[must_use]
603    pub fn to_shared(&self) -> GridAutoFlowParseError<'_> {
604        match self {
605            Self::InvalidValue(s) => GridAutoFlowParseError::InvalidValue(s.as_str()),
606        }
607    }
608}
609
610#[cfg(feature = "parser")]
611/// # Errors
612///
613/// Returns an error if `input` is not a valid CSS `grid-auto-flow` value.
614pub fn parse_layout_grid_auto_flow(
615    input: &str,
616) -> Result<LayoutGridAutoFlow, GridAutoFlowParseError<'_>> {
617    match input.trim() {
618        "row" => Ok(LayoutGridAutoFlow::Row),
619        "column" => Ok(LayoutGridAutoFlow::Column),
620        "row dense" | "dense" => Ok(LayoutGridAutoFlow::RowDense),
621        "column dense" => Ok(LayoutGridAutoFlow::ColumnDense),
622        _ => Err(GridAutoFlowParseError::InvalidValue(input)),
623    }
624}
625
626// --- justify-self / justify-items ---
627
628/// Represents `justify-self` for grid items
629#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
630#[repr(C)]
631#[derive(Default)]
632pub enum LayoutJustifySelf {
633    #[default]
634    Auto,
635    Start,
636    End,
637    Center,
638    Stretch,
639}
640
641impl PrintAsCssValue for LayoutJustifySelf {
642    fn print_as_css_value(&self) -> String {
643        match self {
644            Self::Auto => "auto".to_string(),
645            Self::Start => "start".to_string(),
646            Self::End => "end".to_string(),
647            Self::Center => "center".to_string(),
648            Self::Stretch => "stretch".to_string(),
649        }
650    }
651}
652
653#[cfg(feature = "parser")]
654#[derive(Clone, PartialEq, Eq)]
655pub enum JustifySelfParseError<'a> {
656    InvalidValue(&'a str),
657}
658
659#[cfg(feature = "parser")]
660#[derive(Debug, Clone, PartialEq, Eq)]
661#[repr(C, u8)]
662pub enum JustifySelfParseErrorOwned {
663    InvalidValue(AzString),
664}
665
666#[cfg(feature = "parser")]
667impl JustifySelfParseError<'_> {
668    #[must_use]
669    pub fn to_contained(&self) -> JustifySelfParseErrorOwned {
670        match self {
671            JustifySelfParseError::InvalidValue(s) => {
672                JustifySelfParseErrorOwned::InvalidValue((*s).to_string().into())
673            }
674        }
675    }
676}
677
678#[cfg(feature = "parser")]
679impl JustifySelfParseErrorOwned {
680    #[must_use]
681    pub fn to_shared(&self) -> JustifySelfParseError<'_> {
682        match self {
683            Self::InvalidValue(s) => JustifySelfParseError::InvalidValue(s.as_str()),
684        }
685    }
686}
687
688#[cfg(feature = "parser")]
689impl_debug_as_display!(JustifySelfParseError<'a>);
690#[cfg(feature = "parser")]
691impl_display! { JustifySelfParseError<'a>, {
692    InvalidValue(e) => format!("Invalid justify-self value: \"{}\"", e),
693}}
694
695#[cfg(feature = "parser")]
696/// # Errors
697///
698/// Returns an error if `input` is not a valid CSS `justify-self` value.
699pub fn parse_layout_justify_self(
700    input: &str,
701) -> Result<LayoutJustifySelf, JustifySelfParseError<'_>> {
702    match input.trim() {
703        "auto" => Ok(LayoutJustifySelf::Auto),
704        "start" | "flex-start" => Ok(LayoutJustifySelf::Start),
705        "end" | "flex-end" => Ok(LayoutJustifySelf::End),
706        "center" => Ok(LayoutJustifySelf::Center),
707        "stretch" => Ok(LayoutJustifySelf::Stretch),
708        _ => Err(JustifySelfParseError::InvalidValue(input)),
709    }
710}
711
712/// Represents `justify-items` for grid containers
713#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
714#[repr(C)]
715#[derive(Default)]
716pub enum LayoutJustifyItems {
717    Start,
718    End,
719    Center,
720    #[default]
721    Stretch,
722}
723
724impl PrintAsCssValue for LayoutJustifyItems {
725    fn print_as_css_value(&self) -> String {
726        match self {
727            Self::Start => "start".to_string(),
728            Self::End => "end".to_string(),
729            Self::Center => "center".to_string(),
730            Self::Stretch => "stretch".to_string(),
731        }
732    }
733}
734
735#[cfg(feature = "parser")]
736#[derive(Clone, PartialEq, Eq)]
737pub enum JustifyItemsParseError<'a> {
738    InvalidValue(&'a str),
739}
740
741#[cfg(feature = "parser")]
742#[derive(Debug, Clone, PartialEq, Eq)]
743#[repr(C, u8)]
744pub enum JustifyItemsParseErrorOwned {
745    InvalidValue(AzString),
746}
747
748#[cfg(feature = "parser")]
749impl JustifyItemsParseError<'_> {
750    #[must_use]
751    pub fn to_contained(&self) -> JustifyItemsParseErrorOwned {
752        match self {
753            JustifyItemsParseError::InvalidValue(s) => {
754                JustifyItemsParseErrorOwned::InvalidValue((*s).to_string().into())
755            }
756        }
757    }
758}
759
760#[cfg(feature = "parser")]
761impl JustifyItemsParseErrorOwned {
762    #[must_use]
763    pub fn to_shared(&self) -> JustifyItemsParseError<'_> {
764        match self {
765            Self::InvalidValue(s) => JustifyItemsParseError::InvalidValue(s.as_str()),
766        }
767    }
768}
769
770#[cfg(feature = "parser")]
771impl_debug_as_display!(JustifyItemsParseError<'a>);
772#[cfg(feature = "parser")]
773impl_display! { JustifyItemsParseError<'a>, {
774    InvalidValue(e) => format!("Invalid justify-items value: \"{}\"", e),
775}}
776
777#[cfg(feature = "parser")]
778/// # Errors
779///
780/// Returns an error if `input` is not a valid CSS `justify-items` value.
781pub fn parse_layout_justify_items(
782    input: &str,
783) -> Result<LayoutJustifyItems, JustifyItemsParseError<'_>> {
784    match input.trim() {
785        "start" => Ok(LayoutJustifyItems::Start),
786        "end" => Ok(LayoutJustifyItems::End),
787        "center" => Ok(LayoutJustifyItems::Center),
788        "stretch" => Ok(LayoutJustifyItems::Stretch),
789        _ => Err(JustifyItemsParseError::InvalidValue(input)),
790    }
791}
792
793// --- gap (single value type) ---
794
795#[derive(Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
796#[repr(C)]
797pub struct LayoutGap {
798    pub inner: PixelValue,
799}
800
801impl core::fmt::Debug for LayoutGap {
802    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
803        write!(f, "{}", self.inner)
804    }
805}
806
807impl PrintAsCssValue for LayoutGap {
808    fn print_as_css_value(&self) -> String {
809        self.inner.print_as_css_value()
810    }
811}
812
813// Implement FormatAsRustCode for the new types so they can be emitted by the
814// code generator.
815impl FormatAsRustCode for LayoutGridAutoFlow {
816    fn format_as_rust_code(&self, _tabs: usize) -> String {
817        format!(
818            "LayoutGridAutoFlow::{}",
819            match self {
820                Self::Row => "Row",
821                Self::Column => "Column",
822                Self::RowDense => "RowDense",
823                Self::ColumnDense => "ColumnDense",
824            }
825        )
826    }
827}
828
829impl FormatAsRustCode for LayoutJustifySelf {
830    fn format_as_rust_code(&self, _tabs: usize) -> String {
831        format!(
832            "LayoutJustifySelf::{}",
833            match self {
834                Self::Auto => "Auto",
835                Self::Start => "Start",
836                Self::End => "End",
837                Self::Center => "Center",
838                Self::Stretch => "Stretch",
839            }
840        )
841    }
842}
843
844impl FormatAsRustCode for LayoutJustifyItems {
845    fn format_as_rust_code(&self, _tabs: usize) -> String {
846        format!(
847            "LayoutJustifyItems::{}",
848            match self {
849                Self::Start => "Start",
850                Self::End => "End",
851                Self::Center => "Center",
852                Self::Stretch => "Stretch",
853            }
854        )
855    }
856}
857
858impl FormatAsRustCode for LayoutGap {
859    fn format_as_rust_code(&self, _tabs: usize) -> String {
860        use crate::codegen::format::format_pixel_value;
861        format!("LayoutGap {{ inner: {} }}", format_pixel_value(&self.inner))
862    }
863}
864
865impl FormatAsRustCode for GridTrackSizing {
866    // `tabs` is required by the FormatAsRustCode trait signature; this variant only
867    // threads it through to nested MinMax children, never reading it locally.
868    #[allow(clippy::only_used_in_recursion)]
869    fn format_as_rust_code(&self, tabs: usize) -> String {
870        use crate::codegen::format::format_pixel_value;
871        match self {
872            Self::Fixed(pv) => {
873                format!("GridTrackSizing::Fixed({})", format_pixel_value(pv))
874            }
875            Self::Fr(f) => format!("GridTrackSizing::Fr({f})"),
876            Self::MinContent => "GridTrackSizing::MinContent".to_string(),
877            Self::MaxContent => "GridTrackSizing::MaxContent".to_string(),
878            Self::Auto => "GridTrackSizing::Auto".to_string(),
879            Self::MinMax(minmax) => {
880                format!(
881                    "GridTrackSizing::MinMax(GridMinMax {{ min: Box::new({}), max: Box::new({}) }})",
882                    minmax.min.format_as_rust_code(tabs),
883                    minmax.max.format_as_rust_code(tabs)
884                )
885            }
886            Self::FitContent(pv) => {
887                format!("GridTrackSizing::FitContent({})", format_pixel_value(pv))
888            }
889        }
890    }
891}
892
893impl FormatAsRustCode for GridAutoTracks {
894    fn format_as_rust_code(&self, tabs: usize) -> String {
895        let tracks: Vec<String> = self
896            .tracks
897            .as_ref()
898            .iter()
899            .map(|t| t.format_as_rust_code(tabs))
900            .collect();
901        format!(
902            "GridAutoTracks {{ tracks: GridTrackSizingVec::from_vec(vec![{}]) }}",
903            tracks.join(", ")
904        )
905    }
906}
907
908impl FormatAsRustCode for GridTemplateAreas {
909    fn format_as_rust_code(&self, _tabs: usize) -> String {
910        format!(
911            "GridTemplateAreas {{ areas: GridAreaDefinitionVec::from_vec(vec!{:?}) }}",
912            self.areas.as_ref()
913        )
914    }
915}
916
917#[cfg(feature = "parser")]
918/// # Errors
919///
920/// Returns an error if `input` is not a valid CSS `gap` value.
921pub fn parse_layout_gap(
922    input: &str,
923) -> Result<LayoutGap, crate::props::basic::pixel::CssPixelValueParseError<'_>> {
924    crate::props::basic::pixel::parse_pixel_value(input).map(|p| LayoutGap { inner: p })
925}
926
927#[cfg(feature = "parser")]
928/// # Errors
929///
930/// Returns an error if `input` is not a valid CSS `grid-line-owned` value.
931pub fn parse_grid_line_owned(input: &str) -> Result<GridLine, ()> {
932    let input = input.trim();
933
934    if input == "auto" {
935        return Ok(GridLine::Auto);
936    }
937
938    if let Some(num_str) = input.strip_prefix("span ") {
939        let num_str = num_str.trim();
940        if let Ok(num) = num_str.parse::<i32>() {
941            return Ok(GridLine::Span(num));
942        }
943        return Err(());
944    }
945
946    // Try to parse as line number
947    if let Ok(num) = input.parse::<i32>() {
948        return Ok(GridLine::Line(num));
949    }
950
951    // Otherwise treat as named line
952    Ok(GridLine::Named(NamedGridLine::create(
953        input.to_string().into(),
954        None,
955    )))
956}
957
958#[cfg(all(test, feature = "parser"))]
959mod tests {
960    use super::*;
961
962    // Grid template tests
963    #[test]
964    fn test_parse_grid_template_none() {
965        let result = parse_grid_template("none").unwrap();
966        assert_eq!(result.tracks.len(), 0);
967    }
968
969    #[test]
970    fn test_parse_grid_template_single_px() {
971        let result = parse_grid_template("100px").unwrap();
972        assert_eq!(result.tracks.len(), 1);
973        assert!(matches!(
974            result.tracks.as_ref()[0],
975            GridTrackSizing::Fixed(_)
976        ));
977    }
978
979    #[test]
980    fn test_parse_grid_template_multiple_tracks() {
981        let result = parse_grid_template("100px 200px 1fr").unwrap();
982        assert_eq!(result.tracks.len(), 3);
983    }
984
985    #[test]
986    fn test_parse_grid_template_fr_units() {
987        let result = parse_grid_template("1fr 2fr 1fr").unwrap();
988        assert_eq!(result.tracks.len(), 3);
989        assert!(matches!(
990            result.tracks.as_ref()[0],
991            GridTrackSizing::Fr(100)
992        ));
993        assert!(matches!(
994            result.tracks.as_ref()[1],
995            GridTrackSizing::Fr(200)
996        ));
997    }
998
999    #[test]
1000    fn test_parse_grid_template_fractional_fr() {
1001        let result = parse_grid_template("0.5fr 1.5fr").unwrap();
1002        assert_eq!(result.tracks.len(), 2);
1003        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::Fr(50)));
1004        assert!(matches!(
1005            result.tracks.as_ref()[1],
1006            GridTrackSizing::Fr(150)
1007        ));
1008    }
1009
1010    #[test]
1011    fn test_parse_grid_template_auto() {
1012        let result = parse_grid_template("auto 100px auto").unwrap();
1013        assert_eq!(result.tracks.len(), 3);
1014        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::Auto));
1015        assert!(matches!(result.tracks.as_ref()[2], GridTrackSizing::Auto));
1016    }
1017
1018    #[test]
1019    fn test_parse_grid_template_min_max_content() {
1020        let result = parse_grid_template("min-content max-content auto").unwrap();
1021        assert_eq!(result.tracks.len(), 3);
1022        assert!(matches!(
1023            result.tracks.as_ref()[0],
1024            GridTrackSizing::MinContent
1025        ));
1026        assert!(matches!(
1027            result.tracks.as_ref()[1],
1028            GridTrackSizing::MaxContent
1029        ));
1030    }
1031
1032    #[test]
1033    fn test_parse_grid_template_minmax() {
1034        let result = parse_grid_template("minmax(100px, 1fr)").unwrap();
1035        assert_eq!(result.tracks.len(), 1);
1036        assert!(matches!(
1037            result.tracks.as_ref()[0],
1038            GridTrackSizing::MinMax(_)
1039        ));
1040    }
1041
1042    #[test]
1043    fn test_parse_grid_template_minmax_complex() {
1044        let result = parse_grid_template("minmax(min-content, max-content)").unwrap();
1045        assert_eq!(result.tracks.len(), 1);
1046    }
1047
1048    #[test]
1049    fn test_parse_grid_template_fit_content() {
1050        let result = parse_grid_template("fit-content(200px)").unwrap();
1051        assert_eq!(result.tracks.len(), 1);
1052        assert!(matches!(
1053            result.tracks.as_ref()[0],
1054            GridTrackSizing::FitContent(_)
1055        ));
1056    }
1057
1058    #[test]
1059    fn test_parse_grid_template_mixed() {
1060        let result = parse_grid_template("100px minmax(100px, 1fr) auto 2fr").unwrap();
1061        assert_eq!(result.tracks.len(), 4);
1062    }
1063
1064    #[test]
1065    fn test_parse_grid_template_percent() {
1066        let result = parse_grid_template("25% 50% 25%").unwrap();
1067        assert_eq!(result.tracks.len(), 3);
1068    }
1069
1070    #[test]
1071    fn test_parse_grid_template_em_units() {
1072        let result = parse_grid_template("10em 20em 1fr").unwrap();
1073        assert_eq!(result.tracks.len(), 3);
1074    }
1075
1076    // Grid placement tests
1077    #[test]
1078    fn test_parse_grid_placement_auto() {
1079        let result = parse_grid_placement("auto").unwrap();
1080        assert!(matches!(result.grid_start, GridLine::Auto));
1081        assert!(matches!(result.grid_end, GridLine::Auto));
1082    }
1083
1084    #[test]
1085    fn test_parse_grid_placement_line_number() {
1086        let result = parse_grid_placement("1").unwrap();
1087        assert!(matches!(result.grid_start, GridLine::Line(1)));
1088        assert!(matches!(result.grid_end, GridLine::Auto));
1089    }
1090
1091    #[test]
1092    fn test_parse_grid_placement_negative_line() {
1093        let result = parse_grid_placement("-1").unwrap();
1094        assert!(matches!(result.grid_start, GridLine::Line(-1)));
1095    }
1096
1097    #[test]
1098    fn test_parse_grid_placement_span() {
1099        let result = parse_grid_placement("span 2").unwrap();
1100        assert!(matches!(result.grid_start, GridLine::Span(2)));
1101    }
1102
1103    #[test]
1104    fn test_parse_grid_placement_start_end() {
1105        let result = parse_grid_placement("1 / 3").unwrap();
1106        assert!(matches!(result.grid_start, GridLine::Line(1)));
1107        assert!(matches!(result.grid_end, GridLine::Line(3)));
1108    }
1109
1110    #[test]
1111    fn test_parse_grid_placement_span_end() {
1112        let result = parse_grid_placement("1 / span 2").unwrap();
1113        assert!(matches!(result.grid_start, GridLine::Line(1)));
1114        assert!(matches!(result.grid_end, GridLine::Span(2)));
1115    }
1116
1117    #[test]
1118    fn test_parse_grid_placement_named_line() {
1119        let result = parse_grid_placement("header-start").unwrap();
1120        assert!(matches!(result.grid_start, GridLine::Named(_)));
1121    }
1122
1123    #[test]
1124    fn test_parse_grid_placement_named_start_end() {
1125        let result = parse_grid_placement("header-start / header-end").unwrap();
1126        assert!(matches!(result.grid_start, GridLine::Named(_)));
1127        assert!(matches!(result.grid_end, GridLine::Named(_)));
1128    }
1129
1130    // Edge cases
1131    #[test]
1132    fn test_parse_grid_template_whitespace() {
1133        let result = parse_grid_template("  100px   200px  ").unwrap();
1134        assert_eq!(result.tracks.len(), 2);
1135    }
1136
1137    #[test]
1138    fn test_parse_grid_placement_whitespace() {
1139        let result = parse_grid_placement("  1  /  3  ").unwrap();
1140        assert!(matches!(result.grid_start, GridLine::Line(1)));
1141        assert!(matches!(result.grid_end, GridLine::Line(3)));
1142    }
1143
1144    #[test]
1145    fn test_parse_grid_template_zero_fr() {
1146        let result = parse_grid_template("0fr").unwrap();
1147        assert!(matches!(result.tracks.as_ref()[0], GridTrackSizing::Fr(0)));
1148    }
1149
1150    #[test]
1151    fn test_parse_grid_placement_zero_line() {
1152        let result = parse_grid_placement("0").unwrap();
1153        assert!(matches!(result.grid_start, GridLine::Line(0)));
1154    }
1155
1156    // repeat() tests
1157    #[test]
1158    fn test_parse_grid_template_repeat_fr() {
1159        let result = parse_grid_template("repeat(3, 1fr)").unwrap();
1160        assert_eq!(result.tracks.len(), 3);
1161        assert!(matches!(
1162            result.tracks.as_ref()[0],
1163            GridTrackSizing::Fr(100)
1164        ));
1165        assert!(matches!(
1166            result.tracks.as_ref()[1],
1167            GridTrackSizing::Fr(100)
1168        ));
1169        assert!(matches!(
1170            result.tracks.as_ref()[2],
1171            GridTrackSizing::Fr(100)
1172        ));
1173    }
1174
1175    #[test]
1176    fn test_parse_grid_template_repeat_px() {
1177        let result = parse_grid_template("repeat(2, 100px)").unwrap();
1178        assert_eq!(result.tracks.len(), 2);
1179        assert!(matches!(
1180            result.tracks.as_ref()[0],
1181            GridTrackSizing::Fixed(_)
1182        ));
1183        assert!(matches!(
1184            result.tracks.as_ref()[1],
1185            GridTrackSizing::Fixed(_)
1186        ));
1187    }
1188
1189    #[test]
1190    fn test_parse_grid_template_repeat_multiple_tracks() {
1191        // repeat(2, 100px 1fr) should expand to [100px, 1fr, 100px, 1fr]
1192        let result = parse_grid_template("repeat(2, 100px 1fr)").unwrap();
1193        assert_eq!(result.tracks.len(), 4);
1194        assert!(matches!(
1195            result.tracks.as_ref()[0],
1196            GridTrackSizing::Fixed(_)
1197        ));
1198        assert!(matches!(
1199            result.tracks.as_ref()[1],
1200            GridTrackSizing::Fr(100)
1201        ));
1202        assert!(matches!(
1203            result.tracks.as_ref()[2],
1204            GridTrackSizing::Fixed(_)
1205        ));
1206        assert!(matches!(
1207            result.tracks.as_ref()[3],
1208            GridTrackSizing::Fr(100)
1209        ));
1210    }
1211
1212    #[test]
1213    fn test_parse_grid_template_repeat_with_other_tracks() {
1214        // "100px repeat(2, 1fr) auto" should produce [100px, 1fr, 1fr, auto]
1215        let result = parse_grid_template("100px repeat(2, 1fr) auto").unwrap();
1216        assert_eq!(result.tracks.len(), 4);
1217        assert!(matches!(
1218            result.tracks.as_ref()[0],
1219            GridTrackSizing::Fixed(_)
1220        ));
1221        assert!(matches!(
1222            result.tracks.as_ref()[1],
1223            GridTrackSizing::Fr(100)
1224        ));
1225        assert!(matches!(
1226            result.tracks.as_ref()[2],
1227            GridTrackSizing::Fr(100)
1228        ));
1229        assert!(matches!(result.tracks.as_ref()[3], GridTrackSizing::Auto));
1230    }
1231
1232    #[test]
1233    fn test_parse_grid_template_repeat_minmax() {
1234        let result = parse_grid_template("repeat(3, minmax(100px, 1fr))").unwrap();
1235        assert_eq!(result.tracks.len(), 3);
1236        assert!(matches!(
1237            result.tracks.as_ref()[0],
1238            GridTrackSizing::MinMax(_)
1239        ));
1240        assert!(matches!(
1241            result.tracks.as_ref()[1],
1242            GridTrackSizing::MinMax(_)
1243        ));
1244        assert!(matches!(
1245            result.tracks.as_ref()[2],
1246            GridTrackSizing::MinMax(_)
1247        ));
1248    }
1249}
1250
1251// --- grid-template-areas ---
1252
1253/// A single named grid area with its row/column bounds (1-based grid line numbers).
1254/// This matches taffy's `GridTemplateArea<String>`.
1255#[repr(C)]
1256#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1257pub struct GridAreaDefinition {
1258    pub name: AzString,
1259    pub row_start: u16,
1260    pub row_end: u16,
1261    pub column_start: u16,
1262    pub column_end: u16,
1263}
1264
1265impl_option!(
1266    GridAreaDefinition,
1267    OptionGridAreaDefinition,
1268    copy = false,
1269    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1270);
1271
1272impl_vec!(
1273    GridAreaDefinition,
1274    GridAreaDefinitionVec,
1275    GridAreaDefinitionVecDestructor,
1276    GridAreaDefinitionVecDestructorType,
1277    GridAreaDefinitionVecSlice,
1278    OptionGridAreaDefinition
1279);
1280impl_vec_clone!(
1281    GridAreaDefinition,
1282    GridAreaDefinitionVec,
1283    GridAreaDefinitionVecDestructor
1284);
1285impl_vec_debug!(GridAreaDefinition, GridAreaDefinitionVec);
1286impl_vec_partialeq!(GridAreaDefinition, GridAreaDefinitionVec);
1287impl_vec_eq!(GridAreaDefinition, GridAreaDefinitionVec);
1288impl_vec_partialord!(GridAreaDefinition, GridAreaDefinitionVec);
1289impl_vec_ord!(GridAreaDefinition, GridAreaDefinitionVec);
1290impl_vec_hash!(GridAreaDefinition, GridAreaDefinitionVec);
1291impl_vec_mut!(GridAreaDefinition, GridAreaDefinitionVec);
1292
1293/// Represents the parsed value of `grid-template-areas`.
1294///
1295/// Example CSS:
1296/// ```css
1297/// grid-template-areas:
1298///     "header header header"
1299///     "sidebar main aside"
1300///     "footer footer footer";
1301/// ```
1302#[repr(C)]
1303#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
1304pub struct GridTemplateAreas {
1305    pub areas: GridAreaDefinitionVec,
1306}
1307
1308impl Default for GridTemplateAreas {
1309    fn default() -> Self {
1310        Self {
1311            areas: GridAreaDefinitionVec::from_vec(Vec::new()),
1312        }
1313    }
1314}
1315
1316impl PrintAsCssValue for GridTemplateAreas {
1317    fn print_as_css_value(&self) -> String {
1318        let areas_slice = self.areas.as_ref();
1319        if areas_slice.is_empty() {
1320            return "none".to_string();
1321        }
1322        // Reconstruct the row strings from the area definitions
1323        let max_row = areas_slice.iter().map(|a| a.row_end).max().unwrap_or(1);
1324        let max_col = areas_slice.iter().map(|a| a.column_end).max().unwrap_or(1);
1325        let num_rows = (max_row - 1) as usize;
1326        let num_cols = (max_col - 1) as usize;
1327        let mut grid: Vec<Vec<String>> = vec![vec![".".to_string(); num_cols]; num_rows];
1328        for area in areas_slice {
1329            let row_start = area.row_start as usize - 1;
1330            let row_end = area.row_end as usize - 1;
1331            let col_start = area.column_start as usize - 1;
1332            let col_end = area.column_end as usize - 1;
1333            for row in grid.iter_mut().take(row_end).skip(row_start) {
1334                for cell in row.iter_mut().take(col_end).skip(col_start) {
1335                    *cell = area.name.as_str().to_string();
1336                }
1337            }
1338        }
1339        grid.iter()
1340            .map(|row| format!("\"{}\"", row.join(" ")))
1341            .collect::<Vec<_>>()
1342            .join(" ")
1343    }
1344}
1345
1346/// Parse `grid-template-areas` CSS value.
1347///
1348/// Accepts quoted row strings like:
1349///   `"header header header" "sidebar main aside" "footer footer footer"`
1350///
1351/// Returns a `GridTemplateAreas` with deduplicated named areas and their
1352/// computed row/column line boundaries (1-based, as taffy expects).
1353#[cfg(feature = "parser")]
1354/// # Errors
1355///
1356/// Returns an error if `input` is not a valid CSS `grid-template-areas` value.
1357pub fn parse_grid_template_areas(input: &str) -> Result<GridTemplateAreas, ()> {
1358    use alloc::collections::BTreeMap;
1359    let input = input.trim();
1360    if input == "none" {
1361        return Ok(GridTemplateAreas::default());
1362    }
1363
1364    // Extract quoted strings: each one is a row
1365    let mut rows: Vec<Vec<String>> = Vec::new();
1366    let mut i = 0;
1367    let bytes = input.as_bytes();
1368    while i < bytes.len() {
1369        if bytes[i] == b'"' || bytes[i] == b'\'' {
1370            let quote = bytes[i];
1371            i += 1;
1372            let start = i;
1373            while i < bytes.len() && bytes[i] != quote {
1374                i += 1;
1375            }
1376            if i >= bytes.len() {
1377                return Err(());
1378            }
1379            let row_str = &input[start..i];
1380            let cells: Vec<String> = row_str
1381                .split_whitespace()
1382                .map(std::string::ToString::to_string)
1383                .collect();
1384            if cells.is_empty() {
1385                return Err(());
1386            }
1387            rows.push(cells);
1388        }
1389        // advance past the closing quote (quoted branch) or the current char (else)
1390        i += 1;
1391    }
1392
1393    if rows.is_empty() {
1394        return Err(());
1395    }
1396
1397    // Validate: all rows must have the same number of columns
1398    let num_cols = rows[0].len();
1399    for row in &rows {
1400        if row.len() != num_cols {
1401            return Err(());
1402        }
1403    }
1404
1405    // Build area map: name -> (min_row, max_row, min_col, max_col) in 0-based indices
1406    let mut area_map: BTreeMap<String, (usize, usize, usize, usize)> = BTreeMap::new();
1407
1408    for (row_idx, row) in rows.iter().enumerate() {
1409        for (col_idx, cell) in row.iter().enumerate() {
1410            if cell == "." {
1411                continue; // skip null cell tokens
1412            }
1413            let entry = area_map
1414                .entry(cell.clone())
1415                .or_insert((row_idx, row_idx, col_idx, col_idx));
1416            entry.0 = entry.0.min(row_idx);
1417            entry.1 = entry.1.max(row_idx);
1418            entry.2 = entry.2.min(col_idx);
1419            entry.3 = entry.3.max(col_idx);
1420        }
1421    }
1422
1423    // Convert to 1-based grid line numbers (taffy convention)
1424    let mut areas = Vec::new();
1425    for (name, (min_row, max_row, min_col, max_col)) in area_map {
1426        areas.push(GridAreaDefinition {
1427            name: name.into(),
1428            row_start: u16::try_from(min_row + 1).unwrap_or(u16::MAX),
1429            row_end: u16::try_from(max_row + 2).unwrap_or(u16::MAX), // end line is one past the last cell
1430            column_start: u16::try_from(min_col + 1).unwrap_or(u16::MAX),
1431            column_end: u16::try_from(max_col + 2).unwrap_or(u16::MAX),
1432        });
1433    }
1434
1435    Ok(GridTemplateAreas {
1436        areas: GridAreaDefinitionVec::from_vec(areas),
1437    })
1438}
1439
1440#[cfg(all(test, feature = "parser"))]
1441mod autotest_generated {
1442    use super::*;
1443
1444    // Every assertion below pins the *observed* behaviour of the code as it stands.
1445    // Where that behaviour deviates from the CSS Grid spec or loses information, the
1446    // test is named `..._is_lax` / `..._is_lossy` / `..._saturates` and carries a
1447    // BUG/DEVIATION comment. Those comments are the deliverable: they mark the places
1448    // where a fix would have to change the assertion, not the code under test.
1449
1450    // ---------------------------------------------------------------------
1451    // NamedGridLine::create / NamedGridLine::span
1452    // ---------------------------------------------------------------------
1453
1454    #[test]
1455    fn named_grid_line_span_roundtrips_every_nonzero_i32() {
1456        for span in [1_i32, -1, 7, -7, i32::MAX, i32::MIN, i32::MIN + 1] {
1457            let line = NamedGridLine::create("area".to_string().into(), Some(span));
1458            assert_eq!(line.span_count, span);
1459            assert_eq!(line.span(), Some(span), "span {span} must survive create()");
1460        }
1461    }
1462
1463    #[test]
1464    fn named_grid_line_span_of_some_zero_is_lossy() {
1465        // BUG (encoding collision): `span_count == 0` is the sentinel for "no span",
1466        // so an explicitly requested `Some(0)` is indistinguishable from `None` on
1467        // the way out. `create(_, Some(0)).span()` should arguably be `Some(0)` or
1468        // `create` should reject 0; today it silently becomes `None`.
1469        let explicit_zero = NamedGridLine::create("a".to_string().into(), Some(0));
1470        let absent = NamedGridLine::create("a".to_string().into(), None);
1471
1472        assert_eq!(explicit_zero.span(), None);
1473        assert_eq!(absent.span(), None);
1474        assert_eq!(
1475            explicit_zero, absent,
1476            "Some(0) and None collapse to the same value"
1477        );
1478    }
1479
1480    #[test]
1481    fn named_grid_line_create_accepts_empty_and_unicode_names() {
1482        let empty = NamedGridLine::create(String::new().into(), None);
1483        assert_eq!(empty.grid_line_name.as_str(), "");
1484        assert_eq!(empty.span(), None);
1485
1486        let emoji = NamedGridLine::create("\u{1F600}\u{0301}".to_string().into(), Some(3));
1487        assert_eq!(emoji.grid_line_name.as_str(), "\u{1F600}\u{0301}");
1488        assert_eq!(emoji.span(), Some(3));
1489    }
1490
1491    #[test]
1492    fn named_grid_line_span_on_an_extreme_instance_does_not_panic() {
1493        let huge_name = "x".repeat(100_000);
1494        let line = NamedGridLine::create(huge_name.clone().into(), Some(i32::MIN));
1495        assert_eq!(line.span(), Some(i32::MIN));
1496        assert_eq!(line.grid_line_name.as_str().len(), huge_name.len());
1497    }
1498
1499    // ---------------------------------------------------------------------
1500    // split_respecting_parens (private)
1501    // ---------------------------------------------------------------------
1502
1503    #[test]
1504    fn split_respecting_parens_empty_and_whitespace_yield_ok_empty_not_err() {
1505        // DEVIATION: the adversarial expectation is Err/None for empty input, but this
1506        // helper reports "no tokens" as `Ok(vec![])`. That is what makes
1507        // `parse_grid_template("")` succeed (see the parse_grid_template tests below).
1508        assert_eq!(split_respecting_parens(""), Ok(Vec::new()));
1509        assert_eq!(split_respecting_parens("   "), Ok(Vec::new()));
1510        assert_eq!(split_respecting_parens(" \t\n "), Ok(Vec::new()));
1511    }
1512
1513    #[test]
1514    fn split_respecting_parens_valid_minimal_and_nested_calls() {
1515        assert_eq!(
1516            split_respecting_parens("100px 1fr"),
1517            Ok(vec!["100px".to_string(), "1fr".to_string()])
1518        );
1519        // The whole point of the helper: spaces inside parens are NOT separators.
1520        assert_eq!(
1521            split_respecting_parens("repeat(2, 100px 1fr) auto"),
1522            Ok(vec!["repeat(2, 100px 1fr)".to_string(), "auto".to_string()])
1523        );
1524        assert_eq!(
1525            split_respecting_parens("a(b c)d e"),
1526            Ok(vec!["a(b c)d".to_string(), "e".to_string()])
1527        );
1528    }
1529
1530    #[test]
1531    fn split_respecting_parens_rejects_unbalanced_close_paren() {
1532        assert_eq!(split_respecting_parens(")"), Err(()));
1533        assert_eq!(split_respecting_parens("a)b"), Err(()));
1534        assert_eq!(split_respecting_parens("(a))"), Err(()));
1535        assert_eq!(split_respecting_parens(")("), Err(()));
1536    }
1537
1538    #[test]
1539    fn split_respecting_parens_accepts_unbalanced_open_paren_is_lax() {
1540        // Asymmetry: a stray ')' is an error, a stray '(' is not — the depth counter is
1541        // never checked at end-of-input. The malformed token is handed downstream, where
1542        // the track parser happens to reject it, so nothing unsound escapes.
1543        assert_eq!(split_respecting_parens("((("), Ok(vec!["(((".to_string()]));
1544        assert_eq!(
1545            split_respecting_parens("repeat(2, 1fr"),
1546            Ok(vec!["repeat(2, 1fr".to_string()])
1547        );
1548        assert!(parse_grid_template("repeat(2, 1fr").is_err());
1549    }
1550
1551    #[test]
1552    fn split_respecting_parens_does_not_treat_tab_or_newline_as_a_separator() {
1553        // BUG (CSS whitespace): only U+0020 splits tokens. CSS treats \t, \n, \r and \f
1554        // as whitespace too, so a multi-line `grid-template-columns` declaration is
1555        // mis-tokenised into one giant token.
1556        assert_eq!(
1557            split_respecting_parens("100px\t200px"),
1558            Ok(vec!["100px\t200px".to_string()])
1559        );
1560        assert_eq!(
1561            split_respecting_parens("100px\n200px"),
1562            Ok(vec!["100px\n200px".to_string()])
1563        );
1564        // ...and the consequence, one layer up:
1565        assert!(parse_grid_template("100px\t200px").is_err());
1566        assert!(parse_grid_template("100px\n200px").is_err());
1567        // Whereas the space-separated form is fine.
1568        assert_eq!(parse_grid_template("100px 200px").unwrap().tracks.len(), 2);
1569    }
1570
1571    #[test]
1572    fn split_respecting_parens_handles_multibyte_unicode() {
1573        // char-based iteration, so no byte-boundary slicing hazard.
1574        assert_eq!(
1575            split_respecting_parens("\u{1F600} e\u{0301}"),
1576            Ok(vec!["\u{1F600}".to_string(), "e\u{0301}".to_string()])
1577        );
1578        assert_eq!(
1579            split_respecting_parens("\u{1F600}(\u{4E2D} \u{6587})"),
1580            Ok(vec!["\u{1F600}(\u{4E2D} \u{6587})".to_string()])
1581        );
1582    }
1583
1584    #[test]
1585    fn split_respecting_parens_survives_a_million_chars_and_deep_nesting() {
1586        // 1M-char single token: linear scan, must not hang.
1587        let long = "a".repeat(1_000_000);
1588        let parts = split_respecting_parens(&long).unwrap();
1589        assert_eq!(parts.len(), 1);
1590        assert_eq!(parts[0].len(), 1_000_000);
1591
1592        // 50k space-separated tokens.
1593        let many = "1fr ".repeat(50_000);
1594        assert_eq!(split_respecting_parens(&many).unwrap().len(), 50_000);
1595
1596        // 10k nested parens: the scanner is iterative, so no stack overflow, and the
1597        // balanced nest is returned as a single (garbage) token that parsing rejects.
1598        let nested = format!("{}{}", "(".repeat(10_000), ")".repeat(10_000));
1599        assert_eq!(split_respecting_parens(&nested).unwrap().len(), 1);
1600        assert!(parse_grid_template(&nested).is_err());
1601    }
1602
1603    // ---------------------------------------------------------------------
1604    // parse_grid_track_owned (private)
1605    // ---------------------------------------------------------------------
1606
1607    #[test]
1608    fn parse_grid_track_owned_empty_and_whitespace_are_err() {
1609        assert_eq!(parse_grid_track_owned(""), Err(()));
1610        assert_eq!(parse_grid_track_owned("   "), Err(()));
1611        assert_eq!(parse_grid_track_owned("\t\n"), Err(()));
1612    }
1613
1614    #[test]
1615    fn parse_grid_track_owned_valid_minimal_keywords() {
1616        assert_eq!(parse_grid_track_owned("auto"), Ok(GridTrackSizing::Auto));
1617        assert_eq!(
1618            parse_grid_track_owned("min-content"),
1619            Ok(GridTrackSizing::MinContent)
1620        );
1621        assert_eq!(
1622            parse_grid_track_owned("max-content"),
1623            Ok(GridTrackSizing::MaxContent)
1624        );
1625        assert_eq!(
1626            parse_grid_track_owned("  auto  "),
1627            Ok(GridTrackSizing::Auto),
1628            "leading/trailing whitespace is trimmed"
1629        );
1630        // DEVIATION: CSS keywords are ASCII case-insensitive; this parser is not.
1631        assert_eq!(parse_grid_track_owned("AUTO"), Err(()));
1632        assert_eq!(parse_grid_track_owned("Min-Content"), Err(()));
1633    }
1634
1635    #[test]
1636    fn parse_grid_track_owned_fr_is_scaled_by_100_and_truncates() {
1637        assert_eq!(parse_grid_track_owned("1fr"), Ok(GridTrackSizing::Fr(100)));
1638        assert_eq!(parse_grid_track_owned("0fr"), Ok(GridTrackSizing::Fr(0)));
1639        assert_eq!(parse_grid_track_owned("-0fr"), Ok(GridTrackSizing::Fr(0)));
1640        assert_eq!(parse_grid_track_owned("+1fr"), Ok(GridTrackSizing::Fr(100)));
1641        assert_eq!(parse_grid_track_owned("0.5fr"), Ok(GridTrackSizing::Fr(50)));
1642
1643        // Truncation, not rounding: anything below 0.01fr collapses to 0fr, which
1644        // taffy reads as "take no free space at all".
1645        assert_eq!(
1646            parse_grid_track_owned("0.005fr"),
1647            Ok(GridTrackSizing::Fr(0))
1648        );
1649        assert_eq!(
1650            parse_grid_track_owned("1.999fr"),
1651            Ok(GridTrackSizing::Fr(199))
1652        );
1653
1654        // DEVIATION: CSS forbids negative <flex> values; this accepts them.
1655        assert_eq!(
1656            parse_grid_track_owned("-1fr"),
1657            Ok(GridTrackSizing::Fr(-100))
1658        );
1659    }
1660
1661    #[test]
1662    fn parse_grid_track_owned_fr_rejects_nan_inf_and_out_of_range() {
1663        // The guard in parse_grid_track_owned checks is_nan() and the i32 bounds
1664        // *before* casting, so none of these can produce a garbage saturated Fr.
1665        for bad in [
1666            "NaNfr",
1667            "nanfr",
1668            "inffr",
1669            "-inffr",
1670            "infinityfr",
1671            "1e8fr",
1672            "-1e8fr",
1673            "1e30fr",
1674            "-1e30fr",
1675            "340282350000000000000000000000000000000fr",
1676        ] {
1677            assert_eq!(
1678                parse_grid_track_owned(bad),
1679                Err(()),
1680                "{bad:?} must be rejected"
1681            );
1682        }
1683
1684        // Largest values that still fit: 1e7fr * 100 == 1e9, exactly representable in f32.
1685        assert_eq!(
1686            parse_grid_track_owned("1e7fr"),
1687            Ok(GridTrackSizing::Fr(1_000_000_000))
1688        );
1689        assert_eq!(
1690            parse_grid_track_owned("-1e7fr"),
1691            Ok(GridTrackSizing::Fr(-1_000_000_000))
1692        );
1693    }
1694
1695    #[test]
1696    fn parse_grid_track_owned_bare_fr_suffix_is_err() {
1697        assert_eq!(parse_grid_track_owned("fr"), Err(()));
1698        assert_eq!(parse_grid_track_owned("  fr"), Err(()));
1699        assert_eq!(parse_grid_track_owned("xfr"), Err(()));
1700    }
1701
1702    #[test]
1703    fn parse_grid_track_owned_minmax_comma_split_is_paren_unaware() {
1704        assert_eq!(
1705            parse_grid_track_owned("minmax(100px, 1fr)"),
1706            Ok(GridTrackSizing::MinMax(GridMinMax {
1707                min: Box::new(GridTrackSizing::Fixed(PixelValue::px(100.0))),
1708                max: Box::new(GridTrackSizing::Fr(100)),
1709            }))
1710        );
1711        // fit-content nests fine (it has no top-level comma)...
1712        assert!(matches!(
1713            parse_grid_track_owned("minmax(fit-content(10px), 2px)"),
1714            Ok(GridTrackSizing::MinMax(_))
1715        ));
1716        // ...but anything with an inner comma blows the `split(',')` arity check.
1717        // DEVIATION: `minmax(minmax(1px,2px), 3px)` is legal-ish CSS shape-wise and is
1718        // rejected here purely because the comma split ignores parens. It fails closed
1719        // (Err, not a mis-parse), and it is also what bounds recursion depth to 2.
1720        assert_eq!(
1721            parse_grid_track_owned("minmax(minmax(1px,2px), 3px)"),
1722            Err(())
1723        );
1724        assert_eq!(parse_grid_track_owned("minmax(1px,2px,3px)"), Err(()));
1725        assert_eq!(parse_grid_track_owned("minmax(1px)"), Err(()));
1726        assert_eq!(parse_grid_track_owned("minmax()"), Err(()));
1727    }
1728
1729    #[test]
1730    fn parse_grid_track_owned_deeply_nested_minmax_cannot_stack_overflow() {
1731        // 10k nested minmax: rejected at the arity check on the first level, so the
1732        // recursive descent never actually descends. Guards against a regression that
1733        // makes the comma split paren-aware without adding a depth limit.
1734        let deep = format!("{}1px{}", "minmax(".repeat(10_000), ")".repeat(10_000));
1735        assert_eq!(parse_grid_track_owned(&deep), Err(()));
1736
1737        let deep_fit = format!("{}1px{}", "fit-content(".repeat(10_000), ")".repeat(10_000));
1738        assert_eq!(parse_grid_track_owned(&deep_fit), Err(()));
1739    }
1740
1741    #[test]
1742    fn parse_grid_track_owned_unclosed_and_truncated_funcs_are_err() {
1743        // The `&input[7..len-1]` / `&input[12..len-1]` slices are only reached when both
1744        // the prefix and the ')' suffix match, which makes len >= 8 / >= 13. These inputs
1745        // probe every shape near that boundary for an out-of-bounds slice panic.
1746        for bad in [
1747            "minmax(",
1748            "minmax",
1749            "minmax)",
1750            "fit-content(",
1751            "fit-content",
1752            "fit-content)",
1753            "repeat(",
1754            ")",
1755            "(",
1756            "()",
1757            "fit-content()",
1758        ] {
1759            assert_eq!(parse_grid_track_owned(bad), Err(()), "{bad:?}");
1760        }
1761    }
1762
1763    #[test]
1764    fn parse_grid_track_owned_fit_content_and_pixel_fallback() {
1765        assert_eq!(
1766            parse_grid_track_owned("fit-content(200px)"),
1767            Ok(GridTrackSizing::FitContent(PixelValue::px(200.0)))
1768        );
1769        assert_eq!(
1770            parse_grid_track_owned("100px"),
1771            Ok(GridTrackSizing::Fixed(PixelValue::px(100.0)))
1772        );
1773        assert_eq!(
1774            parse_grid_track_owned("25%"),
1775            Ok(GridTrackSizing::Fixed(PixelValue::percent(25.0)))
1776        );
1777        // DEVIATION (inherited from parse_pixel_value): a unitless number is accepted
1778        // and silently means px.
1779        assert_eq!(
1780            parse_grid_track_owned("100"),
1781            Ok(GridTrackSizing::Fixed(PixelValue::px(100.0)))
1782        );
1783    }
1784
1785    #[test]
1786    fn parse_grid_track_owned_pixel_fallback_swallows_nan_and_inf() {
1787        // BUG (inherited from parse_pixel_value + FloatValue::new): the fr path guards
1788        // against NaN/inf, the *pixel* path does not. "NaN" is silently coerced to 0px
1789        // and "inf" saturates to isize::MAX rather than being rejected.
1790        assert_eq!(
1791            parse_grid_track_owned("NaNpx"),
1792            Ok(GridTrackSizing::Fixed(PixelValue::px(0.0))),
1793            "NaN silently becomes 0px"
1794        );
1795        assert_eq!(
1796            parse_grid_track_owned("inf"),
1797            Ok(GridTrackSizing::Fixed(PixelValue::px(f32::INFINITY))),
1798            "inf is accepted and saturates"
1799        );
1800        assert_eq!(
1801            parse_grid_track_owned("1e40px"),
1802            Ok(GridTrackSizing::Fixed(PixelValue::px(f32::INFINITY)))
1803        );
1804    }
1805
1806    #[test]
1807    fn parse_grid_track_owned_garbage_and_long_input_never_panic() {
1808        for bad in [
1809            "!!!",
1810            ";;;",
1811            "\u{1F600}",
1812            "e\u{0301}\u{0301}",
1813            "\0",
1814            "100px;garbage",
1815            "auto;",
1816            "1fr 1fr",
1817            "--var(x)",
1818            "calc(1px + 1px)",
1819        ] {
1820            assert_eq!(parse_grid_track_owned(bad), Err(()), "{bad:?}");
1821        }
1822        // 1M chars of a non-numeric token: rejected fast, no hang.
1823        assert_eq!(parse_grid_track_owned(&"a".repeat(1_000_000)), Err(()));
1824    }
1825
1826    // ---------------------------------------------------------------------
1827    // parse_grid_track_or_repeat (private)
1828    // ---------------------------------------------------------------------
1829
1830    #[test]
1831    fn parse_grid_track_or_repeat_valid_minimal() {
1832        let mut tracks = Vec::new();
1833        assert_eq!(
1834            parse_grid_track_or_repeat("repeat(2, 1fr)", &mut tracks),
1835            Ok(())
1836        );
1837        assert_eq!(
1838            tracks,
1839            vec![GridTrackSizing::Fr(100), GridTrackSizing::Fr(100)]
1840        );
1841
1842        // Plain (non-repeat) tracks are appended to whatever is already there.
1843        assert_eq!(parse_grid_track_or_repeat("auto", &mut tracks), Ok(()));
1844        assert_eq!(tracks.len(), 3);
1845        assert_eq!(tracks[2], GridTrackSizing::Auto);
1846    }
1847
1848    #[test]
1849    fn parse_grid_track_or_repeat_count_is_bounded_at_10_000() {
1850        let mut tracks = Vec::new();
1851        assert_eq!(
1852            parse_grid_track_or_repeat("repeat(10000, 1fr)", &mut tracks),
1853            Ok(())
1854        );
1855        assert_eq!(tracks.len(), 10_000, "the documented maximum is accepted");
1856
1857        // One past the cap, zero, negative, and a count that overflows usize.
1858        for bad in [
1859            "repeat(10001, 1fr)",
1860            "repeat(0, 1fr)",
1861            "repeat(-1, 1fr)",
1862            "repeat(18446744073709551616, 1fr)",
1863            "repeat(99999999999999999999999999, 1fr)",
1864            "repeat(1.5, 1fr)",
1865            "repeat(NaN, 1fr)",
1866            "repeat(inf, 1fr)",
1867            "repeat(auto-fill, 1fr)",
1868        ] {
1869            let mut t = Vec::new();
1870            assert_eq!(parse_grid_track_or_repeat(bad, &mut t), Err(()), "{bad:?}");
1871        }
1872    }
1873
1874    #[test]
1875    fn parse_grid_track_or_repeat_expansion_is_amplified_by_the_track_list_length() {
1876        // The 10_000 cap bounds the *count*, not the expansion: `repeat(N, <k tracks>)`
1877        // yields N*k tracks. 10 tracks x 10_000 => 100_000 entries from a 60-byte input.
1878        // Not unsound, but worth pinning — the real bound on memory is N * k, and k is
1879        // limited only by the length of the declaration.
1880        let mut tracks = Vec::new();
1881        let input = format!("repeat(10000, {})", "1fr ".repeat(10).trim());
1882        assert_eq!(parse_grid_track_or_repeat(&input, &mut tracks), Ok(()));
1883        assert_eq!(tracks.len(), 100_000);
1884        assert!(tracks.iter().all(|t| *t == GridTrackSizing::Fr(100)));
1885    }
1886
1887    #[test]
1888    fn parse_grid_track_or_repeat_with_an_empty_track_list_is_lax() {
1889        // DEVIATION: `repeat(2, )` has nothing to repeat. It is accepted and contributes
1890        // zero tracks instead of being rejected.
1891        let mut tracks = Vec::new();
1892        assert_eq!(
1893            parse_grid_track_or_repeat("repeat(2, )", &mut tracks),
1894            Ok(())
1895        );
1896        assert!(tracks.is_empty());
1897        assert_eq!(parse_grid_template("repeat(2, )").unwrap().tracks.len(), 0);
1898    }
1899
1900    #[test]
1901    fn parse_grid_track_or_repeat_rejects_nested_repeat_and_malformed_shapes() {
1902        for bad in [
1903            "repeat(2, repeat(2, 1fr))", // repeat does not recurse
1904            "repeat(2)",                 // no comma
1905            "repeat()",
1906            "repeat(2, 1fr", // unterminated -> falls through to the plain-track path
1907            "",
1908            "   ",
1909            "\u{1F600}",
1910            "repeat(2, garbage)",
1911        ] {
1912            let mut t = Vec::new();
1913            assert_eq!(parse_grid_track_or_repeat(bad, &mut t), Err(()), "{bad:?}");
1914        }
1915    }
1916
1917    #[test]
1918    fn parse_grid_track_or_repeat_appends_nothing_when_it_fails() {
1919        // Atomicity invariant: on Err the caller's Vec must be left exactly as it was.
1920        // The repeat path validates the entire track list (collect::<Result<Vec<_>,_>>)
1921        // *before* it expands, and the plain path propagates with `?` before pushing —
1922        // so a half-expanded repeat can never leak into the caller's tracks.
1923        let mut tracks = vec![GridTrackSizing::Auto];
1924        for bad in [
1925            "repeat(2, 1fr garbage)",  // fails on the 2nd track of the list
1926            "repeat(3, 1fr) trailing", // not a single token: rejected as a plain track
1927            "repeat(0, 1fr)",
1928            "repeat(2, 1fr", // unterminated
1929            "garbage",
1930        ] {
1931            assert_eq!(
1932                parse_grid_track_or_repeat(bad, &mut tracks),
1933                Err(()),
1934                "{bad:?}"
1935            );
1936            assert_eq!(
1937                tracks,
1938                vec![GridTrackSizing::Auto],
1939                "{bad:?} mutated the caller's Vec despite returning Err"
1940            );
1941        }
1942    }
1943
1944    // ---------------------------------------------------------------------
1945    // parse_grid_template
1946    // ---------------------------------------------------------------------
1947
1948    #[test]
1949    fn parse_grid_template_empty_and_whitespace_are_ok_empty_is_lax() {
1950        // DEVIATION: `grid-template-columns: ;` is not valid CSS, but empty / whitespace
1951        // input yields `Ok` with zero tracks — silently identical to `none`.
1952        for input in ["", "   ", "\t\n", "  \r\n  "] {
1953            let parsed = parse_grid_template(input)
1954                .unwrap_or_else(|e| panic!("{input:?} unexpectedly failed: {e:?}"));
1955            assert_eq!(parsed.tracks.len(), 0, "{input:?}");
1956            assert_eq!(parsed, GridTemplate::default(), "{input:?}");
1957        }
1958    }
1959
1960    #[test]
1961    fn parse_grid_template_none_is_case_sensitive() {
1962        assert_eq!(
1963            parse_grid_template("none").unwrap(),
1964            GridTemplate::default()
1965        );
1966        assert_eq!(
1967            parse_grid_template("  none  ").unwrap(),
1968            GridTemplate::default()
1969        );
1970        // DEVIATION: CSS keywords are ASCII case-insensitive.
1971        assert!(parse_grid_template("NONE").is_err());
1972        assert!(parse_grid_template("None").is_err());
1973    }
1974
1975    #[test]
1976    fn parse_grid_template_garbage_is_err_and_reports_the_trimmed_input() {
1977        let err = parse_grid_template("  !!! garbage  ").unwrap_err();
1978        assert_eq!(
1979            err,
1980            GridParseError::InvalidValue("!!! garbage"),
1981            "the error borrows the trimmed input, not the raw one"
1982        );
1983        assert_eq!(format!("{err}"), "Invalid grid value: \"!!! garbage\"");
1984
1985        for bad in [
1986            ")",
1987            "a)b",
1988            "100px;200px",
1989            "1 fr",
1990            "100px, 200px",
1991            "\u{1F600}",
1992            "100px \u{1F600}",
1993            "\0",
1994            "calc(100px)",
1995        ] {
1996            assert!(
1997                parse_grid_template(bad).is_err(),
1998                "{bad:?} must be rejected"
1999            );
2000        }
2001    }
2002
2003    #[test]
2004    fn parse_grid_template_boundary_numbers_do_not_overflow() {
2005        // fr guards against NaN/inf/out-of-range...
2006        for bad in ["NaNfr", "inffr", "-inffr", "1e30fr"] {
2007            assert!(parse_grid_template(bad).is_err(), "{bad:?}");
2008        }
2009        // ...the pixel path saturates instead (see parse_grid_track_owned tests).
2010        assert_eq!(
2011            parse_grid_template("1e40px").unwrap().tracks.as_ref()[0],
2012            GridTrackSizing::Fixed(PixelValue::px(f32::INFINITY))
2013        );
2014        // i64::MAX as a bare number: parsed as an f32 px value, no integer overflow.
2015        assert!(parse_grid_template("9223372036854775807").is_ok());
2016        assert_eq!(
2017            parse_grid_template("0px -0px 0fr").unwrap().tracks.len(),
2018            3,
2019            "zero and negative-zero are accepted"
2020        );
2021    }
2022
2023    #[test]
2024    fn parse_grid_template_extremely_long_input_terminates() {
2025        // 50k tokens.
2026        let many = "1fr ".repeat(50_000);
2027        assert_eq!(parse_grid_template(&many).unwrap().tracks.len(), 50_000);
2028        // 1M-char single garbage token: rejected without hanging.
2029        assert!(parse_grid_template(&"a".repeat(1_000_000)).is_err());
2030    }
2031
2032    #[test]
2033    fn parse_grid_template_deeply_nested_parens_do_not_stack_overflow() {
2034        let nested = format!("{}1px{}", "(".repeat(10_000), ")".repeat(10_000));
2035        assert!(parse_grid_template(&nested).is_err());
2036        // Unbalanced in the *other* direction is caught by split_respecting_parens.
2037        assert!(parse_grid_template(&")".repeat(10_000)).is_err());
2038    }
2039
2040    // ---------------------------------------------------------------------
2041    // parse_grid_line_owned
2042    // ---------------------------------------------------------------------
2043
2044    #[test]
2045    fn parse_grid_line_owned_valid_minimal() {
2046        assert_eq!(parse_grid_line_owned("auto"), Ok(GridLine::Auto));
2047        assert_eq!(parse_grid_line_owned("1"), Ok(GridLine::Line(1)));
2048        assert_eq!(parse_grid_line_owned("-1"), Ok(GridLine::Line(-1)));
2049        assert_eq!(parse_grid_line_owned("+1"), Ok(GridLine::Line(1)));
2050        assert_eq!(parse_grid_line_owned("0"), Ok(GridLine::Line(0)));
2051        assert_eq!(parse_grid_line_owned("-0"), Ok(GridLine::Line(0)));
2052        assert_eq!(parse_grid_line_owned("span 2"), Ok(GridLine::Span(2)));
2053        assert_eq!(parse_grid_line_owned("span   2"), Ok(GridLine::Span(2)));
2054    }
2055
2056    #[test]
2057    fn parse_grid_line_owned_i32_boundaries_saturate_into_a_named_line() {
2058        assert_eq!(
2059            parse_grid_line_owned("2147483647"),
2060            Ok(GridLine::Line(i32::MAX))
2061        );
2062        assert_eq!(
2063            parse_grid_line_owned("-2147483648"),
2064            Ok(GridLine::Line(i32::MIN))
2065        );
2066
2067        // BUG (silent reinterpretation): one past the i32 range, the integer parse fails
2068        // and the input falls through to the "named line" catch-all — so `grid-row:
2069        // 2147483648` becomes a *named* line called "2147483648" instead of an error.
2070        // A typo'd or overflowing line number is silently accepted as a name.
2071        assert_eq!(
2072            parse_grid_line_owned("2147483648"),
2073            Ok(GridLine::Named(NamedGridLine::create(
2074                "2147483648".to_string().into(),
2075                None
2076            )))
2077        );
2078        assert_eq!(
2079            parse_grid_line_owned("-2147483649"),
2080            Ok(GridLine::Named(NamedGridLine::create(
2081                "-2147483649".to_string().into(),
2082                None
2083            )))
2084        );
2085
2086        // The `span` path has no such fallback: it fails closed.
2087        assert_eq!(parse_grid_line_owned("span 2147483648"), Err(()));
2088        assert_eq!(parse_grid_line_owned("span -2147483649"), Err(()));
2089        assert_eq!(parse_grid_line_owned("span abc"), Err(()));
2090
2091        // ...unless the input trims down to bare "span": the leading `trim()` eats the
2092        // trailing space, `strip_prefix("span ")` then misses, and `span` with no count
2093        // (invalid CSS) becomes a grid line *named* "span" instead of an error.
2094        assert_eq!(
2095            parse_grid_line_owned("span "),
2096            Ok(GridLine::Named(NamedGridLine::create(
2097                "span".to_string().into(),
2098                None
2099            )))
2100        );
2101    }
2102
2103    #[test]
2104    fn parse_grid_line_owned_span_accepts_zero_and_negative_is_lax() {
2105        // DEVIATION: CSS requires `span <integer [1,∞]>`. Both of these are invalid CSS
2106        // and both are accepted here; taffy gets a nonsensical span.
2107        assert_eq!(parse_grid_line_owned("span 0"), Ok(GridLine::Span(0)));
2108        assert_eq!(parse_grid_line_owned("span -5"), Ok(GridLine::Span(-5)));
2109        assert_eq!(
2110            parse_grid_line_owned("span -2147483648"),
2111            Ok(GridLine::Span(i32::MIN))
2112        );
2113    }
2114
2115    #[test]
2116    fn parse_grid_line_owned_never_errs_on_garbage_it_names_it() {
2117        // BUG (unbounded catch-all): every input that is not `auto` / an i32 / a bad
2118        // `span` becomes a named grid line. Empty string, punctuation, emoji, an entire
2119        // stylesheet — all `Ok`. This is why parse_grid_placement effectively cannot
2120        // reject anything (see below).
2121        for garbage in [
2122            "",
2123            "   ",
2124            "!!!",
2125            ";;;",
2126            "\u{1F600}",
2127            "\0",
2128            "span",
2129            "100px",
2130            "1 2",
2131        ] {
2132            let parsed = parse_grid_line_owned(garbage)
2133                .unwrap_or_else(|()| panic!("{garbage:?} unexpectedly errored"));
2134            assert!(
2135                matches!(parsed, GridLine::Named(_)),
2136                "{garbage:?} became {parsed:?}, expected a Named catch-all"
2137            );
2138        }
2139        assert_eq!(
2140            parse_grid_line_owned("   "),
2141            Ok(GridLine::Named(NamedGridLine::create(
2142                String::new().into(),
2143                None
2144            ))),
2145            "whitespace-only trims to an EMPTY named line"
2146        );
2147        // No panic on a huge name.
2148        assert!(parse_grid_line_owned(&"x".repeat(100_000)).is_ok());
2149    }
2150
2151    // ---------------------------------------------------------------------
2152    // parse_grid_placement
2153    // ---------------------------------------------------------------------
2154
2155    #[test]
2156    fn parse_grid_placement_valid_minimal() {
2157        assert_eq!(
2158            parse_grid_placement("auto").unwrap(),
2159            GridPlacement::default()
2160        );
2161        assert_eq!(
2162            parse_grid_placement("1 / 3").unwrap(),
2163            GridPlacement {
2164                grid_start: GridLine::Line(1),
2165                grid_end: GridLine::Line(3)
2166            }
2167        );
2168        assert_eq!(
2169            parse_grid_placement("1/3").unwrap(),
2170            GridPlacement {
2171                grid_start: GridLine::Line(1),
2172                grid_end: GridLine::Line(3)
2173            },
2174            "the slash does not need surrounding spaces"
2175        );
2176        assert_eq!(
2177            parse_grid_placement("  1  /  span 2  ").unwrap(),
2178            GridPlacement {
2179                grid_start: GridLine::Line(1),
2180                grid_end: GridLine::Span(2)
2181            }
2182        );
2183    }
2184
2185    #[test]
2186    fn parse_grid_placement_extra_slash_segments_are_silently_dropped() {
2187        // DEVIATION: `1 / 2 / 3` is not valid CSS. Only parts[0] and parts[1] are read;
2188        // everything after the second slash is discarded without an error.
2189        assert_eq!(
2190            parse_grid_placement("1 / 2 / 3").unwrap(),
2191            GridPlacement {
2192                grid_start: GridLine::Line(1),
2193                grid_end: GridLine::Line(2)
2194            }
2195        );
2196        assert_eq!(
2197            parse_grid_placement("1 / 2 / 3 / 4 / garbage").unwrap(),
2198            GridPlacement {
2199                grid_start: GridLine::Line(1),
2200                grid_end: GridLine::Line(2)
2201            }
2202        );
2203    }
2204
2205    #[test]
2206    fn parse_grid_placement_empty_and_garbage_are_ok_named_is_lax() {
2207        // BUG: consequence of the parse_grid_line_owned catch-all — `grid-row: ;` and
2208        // `grid-row: !!!;` both parse successfully into a named line. There is no
2209        // input-validation value in the Result at all except via the `span` path.
2210        let empty = parse_grid_placement("").unwrap();
2211        assert_eq!(
2212            empty,
2213            GridPlacement {
2214                grid_start: GridLine::Named(NamedGridLine::create(String::new().into(), None)),
2215                grid_end: GridLine::Auto,
2216            },
2217            "empty input should be Err, but yields a Named(\"\") start"
2218        );
2219
2220        for garbage in ["   ", "!!!", "\u{1F600}", "/", "///", "\0"] {
2221            assert!(parse_grid_placement(garbage).is_ok(), "{garbage:?}");
2222        }
2223
2224        // The ONLY rejection path: a malformed `span`.
2225        let err = parse_grid_placement("  span x  ").unwrap_err();
2226        assert_eq!(err, GridParseError::InvalidValue("span x"));
2227        assert_eq!(format!("{err}"), "Invalid grid value: \"span x\"");
2228        assert!(parse_grid_placement("1 / span x").is_err());
2229    }
2230
2231    #[test]
2232    fn parse_grid_placement_long_and_nested_input_does_not_panic() {
2233        let long = "a".repeat(200_000);
2234        assert!(parse_grid_placement(&long).is_ok());
2235        assert!(parse_grid_placement(&format!("{long} / {long}")).is_ok());
2236
2237        // No paren handling here at all, so nesting is inert (but must not panic).
2238        let nested = format!("{}x{}", "(".repeat(10_000), ")".repeat(10_000));
2239        assert!(parse_grid_placement(&nested).is_ok());
2240    }
2241
2242    // ---------------------------------------------------------------------
2243    // parse_layout_grid_auto_flow
2244    // ---------------------------------------------------------------------
2245
2246    #[test]
2247    fn parse_layout_grid_auto_flow_accepts_exactly_five_spellings() {
2248        assert_eq!(
2249            parse_layout_grid_auto_flow("row"),
2250            Ok(LayoutGridAutoFlow::Row)
2251        );
2252        assert_eq!(
2253            parse_layout_grid_auto_flow("column"),
2254            Ok(LayoutGridAutoFlow::Column)
2255        );
2256        assert_eq!(
2257            parse_layout_grid_auto_flow("row dense"),
2258            Ok(LayoutGridAutoFlow::RowDense)
2259        );
2260        assert_eq!(
2261            parse_layout_grid_auto_flow("dense"),
2262            Ok(LayoutGridAutoFlow::RowDense)
2263        );
2264        assert_eq!(
2265            parse_layout_grid_auto_flow("column dense"),
2266            Ok(LayoutGridAutoFlow::ColumnDense)
2267        );
2268        assert_eq!(
2269            parse_layout_grid_auto_flow("  row  "),
2270            Ok(LayoutGridAutoFlow::Row)
2271        );
2272    }
2273
2274    #[test]
2275    fn parse_layout_grid_auto_flow_rejects_everything_else() {
2276        for bad in [
2277            "",
2278            "   ",
2279            "\t\n",
2280            "ROW", // DEVIATION: CSS keywords are case-insensitive
2281            "Row",
2282            "dense row", // DEVIATION: CSS allows either order
2283            "dense column",
2284            "row  dense", // DEVIATION: internal whitespace is not collapsed
2285            "row\tdense",
2286            "row dense extra",
2287            "row;",
2288            "\u{1F600}",
2289            "\0",
2290        ] {
2291            assert!(
2292                parse_layout_grid_auto_flow(bad).is_err(),
2293                "{bad:?} must be rejected"
2294            );
2295        }
2296        // Extremely long input: a straight match, no hang.
2297        assert!(parse_layout_grid_auto_flow(&"row ".repeat(250_000)).is_err());
2298    }
2299
2300    #[test]
2301    fn parse_layout_grid_auto_flow_error_borrows_the_untrimmed_input() {
2302        // The match trims, but the error variant is built from the *raw* `input`, so the
2303        // rendered message keeps the caller's padding. Pinned because Display output of
2304        // these errors ends up in user-facing CSS diagnostics.
2305        let err = parse_layout_grid_auto_flow("  bogus  ").unwrap_err();
2306        assert_eq!(err, GridAutoFlowParseError::InvalidValue("  bogus  "));
2307        assert_eq!(
2308            format!("{err}"),
2309            "Invalid grid-auto-flow value: \"  bogus  \""
2310        );
2311    }
2312
2313    // ---------------------------------------------------------------------
2314    // parse_layout_justify_self / parse_layout_justify_items
2315    // ---------------------------------------------------------------------
2316
2317    #[test]
2318    fn parse_layout_justify_self_accepts_the_flex_aliases() {
2319        assert_eq!(
2320            parse_layout_justify_self("auto"),
2321            Ok(LayoutJustifySelf::Auto)
2322        );
2323        assert_eq!(
2324            parse_layout_justify_self("start"),
2325            Ok(LayoutJustifySelf::Start)
2326        );
2327        assert_eq!(
2328            parse_layout_justify_self("flex-start"),
2329            Ok(LayoutJustifySelf::Start)
2330        );
2331        assert_eq!(parse_layout_justify_self("end"), Ok(LayoutJustifySelf::End));
2332        assert_eq!(
2333            parse_layout_justify_self("flex-end"),
2334            Ok(LayoutJustifySelf::End)
2335        );
2336        assert_eq!(
2337            parse_layout_justify_self("center"),
2338            Ok(LayoutJustifySelf::Center)
2339        );
2340        assert_eq!(
2341            parse_layout_justify_self("stretch"),
2342            Ok(LayoutJustifySelf::Stretch)
2343        );
2344        assert_eq!(
2345            parse_layout_justify_self("  center  "),
2346            Ok(LayoutJustifySelf::Center)
2347        );
2348    }
2349
2350    #[test]
2351    fn parse_layout_justify_self_rejects_everything_else() {
2352        for bad in [
2353            "",
2354            "   ",
2355            "Start",
2356            "START",
2357            "space-between",
2358            "normal",
2359            "left",
2360            "right",
2361            "\u{1F600}",
2362            "\0",
2363            "start end",
2364            "flex-start;",
2365        ] {
2366            assert!(parse_layout_justify_self(bad).is_err(), "{bad:?}");
2367        }
2368        let err = parse_layout_justify_self("  bogus  ").unwrap_err();
2369        assert_eq!(err, JustifySelfParseError::InvalidValue("  bogus  "));
2370        assert_eq!(
2371            format!("{err}"),
2372            "Invalid justify-self value: \"  bogus  \""
2373        );
2374        assert!(parse_layout_justify_self(&"x".repeat(1_000_000)).is_err());
2375    }
2376
2377    #[test]
2378    fn parse_layout_justify_items_has_no_auto_and_no_flex_aliases() {
2379        assert_eq!(
2380            parse_layout_justify_items("start"),
2381            Ok(LayoutJustifyItems::Start)
2382        );
2383        assert_eq!(
2384            parse_layout_justify_items("end"),
2385            Ok(LayoutJustifyItems::End)
2386        );
2387        assert_eq!(
2388            parse_layout_justify_items("center"),
2389            Ok(LayoutJustifyItems::Center)
2390        );
2391        assert_eq!(
2392            parse_layout_justify_items("stretch"),
2393            Ok(LayoutJustifyItems::Stretch)
2394        );
2395
2396        // Asymmetry with justify-self, pinned deliberately: `auto` and the `flex-*`
2397        // aliases are accepted by justify-self but rejected by justify-items. `auto` is
2398        // in fact valid CSS for justify-items (it means "legacy"/inherit-ish), so this
2399        // is a DEVIATION, and the two parsers disagree about the same input.
2400        assert!(parse_layout_justify_items("auto").is_err());
2401        assert!(parse_layout_justify_self("auto").is_ok());
2402        assert!(parse_layout_justify_items("flex-start").is_err());
2403        assert!(parse_layout_justify_self("flex-start").is_ok());
2404
2405        for bad in ["", "   ", "Start", "\u{1F600}", "\0", "stretch stretch"] {
2406            assert!(parse_layout_justify_items(bad).is_err(), "{bad:?}");
2407        }
2408        let err = parse_layout_justify_items("  bogus  ").unwrap_err();
2409        assert_eq!(err, JustifyItemsParseError::InvalidValue("  bogus  "));
2410        assert_eq!(
2411            format!("{err}"),
2412            "Invalid justify-items value: \"  bogus  \""
2413        );
2414        assert!(parse_layout_justify_items(&"x".repeat(1_000_000)).is_err());
2415    }
2416
2417    // ---------------------------------------------------------------------
2418    // parse_layout_gap
2419    // ---------------------------------------------------------------------
2420
2421    #[test]
2422    fn parse_layout_gap_valid_minimal_and_units() {
2423        assert_eq!(
2424            parse_layout_gap("10px").unwrap().inner,
2425            PixelValue::px(10.0)
2426        );
2427        assert_eq!(
2428            parse_layout_gap("  10px  ").unwrap().inner,
2429            PixelValue::px(10.0)
2430        );
2431        assert_eq!(
2432            parse_layout_gap("1.5em").unwrap().inner,
2433            PixelValue::em(1.5)
2434        );
2435        assert_eq!(
2436            parse_layout_gap("50%").unwrap().inner,
2437            PixelValue::percent(50.0)
2438        );
2439        assert_eq!(parse_layout_gap("0").unwrap().inner, PixelValue::px(0.0));
2440        assert_eq!(parse_layout_gap("-0").unwrap().inner, PixelValue::px(0.0));
2441    }
2442
2443    #[test]
2444    fn parse_layout_gap_empty_and_garbage_are_err() {
2445        assert!(matches!(
2446            parse_layout_gap("").unwrap_err(),
2447            crate::props::basic::pixel::CssPixelValueParseError::EmptyString
2448        ));
2449        assert!(matches!(
2450            parse_layout_gap("   ").unwrap_err(),
2451            crate::props::basic::pixel::CssPixelValueParseError::EmptyString
2452        ));
2453        for bad in ["px", "10pxpx", "!!!", "\u{1F600}", "\0", "auto", "normal"] {
2454            assert!(parse_layout_gap(bad).is_err(), "{bad:?}");
2455        }
2456    }
2457
2458    #[test]
2459    fn parse_layout_gap_accepts_negative_unitless_and_split_units_is_lax() {
2460        // DEVIATION: `gap` is a <length-percentage [0,∞]> — negatives are invalid CSS,
2461        // and a nonzero unitless number is invalid too. Both are accepted here.
2462        assert_eq!(
2463            parse_layout_gap("-20px").unwrap().inner,
2464            PixelValue::px(-20.0)
2465        );
2466        assert_eq!(parse_layout_gap("10").unwrap().inner, PixelValue::px(10.0));
2467
2468        // DEVIATION (inherited from parse_pixel_value_inner): the unit suffix is stripped
2469        // *before* the remainder is trimmed, so whitespace between the number and its
2470        // unit is silently tolerated. `gap: 10 px` is not valid CSS but parses as 10px.
2471        assert_eq!(
2472            parse_layout_gap("10 px").unwrap().inner,
2473            PixelValue::px(10.0)
2474        );
2475        assert_eq!(
2476            parse_layout_gap("10\tpx").unwrap().inner,
2477            PixelValue::px(10.0)
2478        );
2479        // Same hole in the track parser's fr path, though the tokenizer usually hides it
2480        // by splitting "1 fr" into two tokens first.
2481        assert_eq!(parse_grid_track_owned("1 fr"), Ok(GridTrackSizing::Fr(100)));
2482        assert!(
2483            parse_grid_template("1 fr").is_err(),
2484            "...but not via the tokenizer"
2485        );
2486    }
2487
2488    #[test]
2489    fn parse_layout_gap_nan_and_inf_do_not_panic_but_are_swallowed() {
2490        // BUG (silent coercion, inherited from FloatValue::new): the f32 -> isize cast
2491        // saturates, so `gap: NaN` becomes 0px and `gap: inf` becomes isize::MAX/1000 px.
2492        // Neither is rejected, and neither panics.
2493        assert_eq!(
2494            parse_layout_gap("NaN").unwrap().inner,
2495            PixelValue::px(0.0),
2496            "NaN silently coerces to 0px"
2497        );
2498        assert_eq!(
2499            parse_layout_gap("inf").unwrap().inner,
2500            PixelValue::px(f32::INFINITY),
2501            "inf saturates instead of erroring"
2502        );
2503        assert_eq!(
2504            parse_layout_gap("-inf").unwrap().inner,
2505            PixelValue::px(f32::NEG_INFINITY)
2506        );
2507        assert_eq!(
2508            parse_layout_gap("1e40px").unwrap().inner,
2509            PixelValue::px(f32::INFINITY),
2510            "an f32 overflow saturates rather than wrapping"
2511        );
2512        // 10k-digit number: parses (to inf) without hanging.
2513        assert!(parse_layout_gap(&format!("{}px", "9".repeat(10_000))).is_ok());
2514    }
2515
2516    // ---------------------------------------------------------------------
2517    // parse_grid_template_areas
2518    // ---------------------------------------------------------------------
2519
2520    fn area_named<'a>(areas: &'a GridTemplateAreas, name: &str) -> &'a GridAreaDefinition {
2521        areas
2522            .areas
2523            .as_ref()
2524            .iter()
2525            .find(|a| a.name.as_str() == name)
2526            .unwrap_or_else(|| panic!("no area named {name:?}"))
2527    }
2528
2529    #[test]
2530    fn parse_grid_template_areas_valid_minimal() {
2531        let parsed = parse_grid_template_areas("\"a\"").unwrap();
2532        assert_eq!(parsed.areas.len(), 1);
2533        assert_eq!(
2534            parsed.areas.as_ref()[0],
2535            GridAreaDefinition {
2536                name: "a".to_string().into(),
2537                row_start: 1,
2538                row_end: 2,
2539                column_start: 1,
2540                column_end: 2,
2541            },
2542            "1-based, end line is one past the last cell"
2543        );
2544
2545        assert_eq!(
2546            parse_grid_template_areas("none").unwrap(),
2547            GridTemplateAreas::default()
2548        );
2549        assert_eq!(
2550            parse_grid_template_areas("  none  ").unwrap(),
2551            GridTemplateAreas::default()
2552        );
2553    }
2554
2555    #[test]
2556    fn parse_grid_template_areas_computes_bounds_and_sorts_by_name() {
2557        let parsed =
2558            parse_grid_template_areas("\"header header\" \"sidebar main\" \"footer footer\"")
2559                .unwrap();
2560        assert_eq!(parsed.areas.len(), 4);
2561
2562        // BTreeMap iteration => areas come out alphabetically, NOT in source order.
2563        let names: Vec<&str> = parsed
2564            .areas
2565            .as_ref()
2566            .iter()
2567            .map(|a| a.name.as_str())
2568            .collect();
2569        assert_eq!(names, vec!["footer", "header", "main", "sidebar"]);
2570
2571        let header = area_named(&parsed, "header");
2572        assert_eq!((header.row_start, header.row_end), (1, 2));
2573        assert_eq!(
2574            (header.column_start, header.column_end),
2575            (1, 3),
2576            "spans both columns"
2577        );
2578
2579        let sidebar = area_named(&parsed, "sidebar");
2580        assert_eq!((sidebar.row_start, sidebar.row_end), (2, 3));
2581        assert_eq!((sidebar.column_start, sidebar.column_end), (1, 2));
2582
2583        let footer = area_named(&parsed, "footer");
2584        assert_eq!((footer.row_start, footer.row_end), (3, 4));
2585        assert_eq!((footer.column_start, footer.column_end), (1, 3));
2586    }
2587
2588    #[test]
2589    fn parse_grid_template_areas_null_cells_are_skipped() {
2590        let parsed = parse_grid_template_areas("\". a\" \". a\"").unwrap();
2591        assert_eq!(parsed.areas.len(), 1, "'.' never becomes an area");
2592        let a = area_named(&parsed, "a");
2593        assert_eq!((a.row_start, a.row_end), (1, 3));
2594        assert_eq!((a.column_start, a.column_end), (2, 3));
2595
2596        // An all-null grid is Ok-but-empty, indistinguishable from `none`.
2597        let all_null = parse_grid_template_areas("\". .\" \". .\"").unwrap();
2598        assert_eq!(all_null, GridTemplateAreas::default());
2599    }
2600
2601    #[test]
2602    fn parse_grid_template_areas_rejects_empty_unterminated_and_ragged() {
2603        for bad in [
2604            "", // no rows
2605            "   ",
2606            "\t\n",
2607            "\"\"",   // empty quoted row -> zero cells
2608            "\"  \"", // whitespace-only row -> zero cells
2609            "\"abc",  // unterminated quote
2610            "'abc",
2611            "\"a\" \"b",     // second row unterminated
2612            "\"a b\" \"c\"", // ragged: 2 columns then 1
2613            "\"a\" \"b c\"",
2614            "abc", // no quotes at all
2615            "\u{1F600}",
2616        ] {
2617            assert_eq!(
2618                parse_grid_template_areas(bad),
2619                Err(()),
2620                "{bad:?} must be rejected"
2621            );
2622        }
2623    }
2624
2625    #[test]
2626    fn parse_grid_template_areas_ignores_junk_outside_the_quotes_is_lax() {
2627        // DEVIATION: the scanner only looks at quoted runs; anything between them is
2628        // skipped without complaint. `grid-template-areas: garbage "a" ;;;` parses.
2629        let parsed = parse_grid_template_areas("garbage \"a\" ;;; \u{1F600}").unwrap();
2630        assert_eq!(parsed.areas.len(), 1);
2631        assert_eq!(area_named(&parsed, "a").row_start, 1);
2632
2633        // Single and double quotes are interchangeable, and may be mixed.
2634        let mixed = parse_grid_template_areas("'a' \"b\"").unwrap();
2635        assert_eq!(mixed.areas.len(), 2);
2636        // A double quote inside a single-quoted row is just a name character.
2637        let weird = parse_grid_template_areas("'a\"b'").unwrap();
2638        assert_eq!(weird.areas.as_ref()[0].name.as_str(), "a\"b");
2639    }
2640
2641    #[test]
2642    fn parse_grid_template_areas_uses_tabs_and_newlines_as_cell_separators() {
2643        // Contrast with split_respecting_parens: this path uses split_whitespace(), so
2644        // it *does* handle the whitespace CSS actually allows.
2645        let parsed = parse_grid_template_areas("\"a\tb\" \"c\nd\"").unwrap();
2646        assert_eq!(parsed.areas.len(), 4);
2647        for name in ["a", "b", "c", "d"] {
2648            assert_eq!(area_named(&parsed, name).name.as_str(), name);
2649        }
2650    }
2651
2652    #[test]
2653    fn parse_grid_template_areas_non_rectangular_becomes_a_bounding_box() {
2654        // BUG (spec violation): CSS requires each named area to form a single rectangle;
2655        // a discontiguous name is a parse error. Here the min/max reduction just takes
2656        // the bounding box, so `"b a b"` yields a `b` that spans columns 1..4 and
2657        // *swallows* the `a` sitting between the two halves.
2658        let parsed = parse_grid_template_areas("\"b a b\"").unwrap();
2659        let b = area_named(&parsed, "b");
2660        let a = area_named(&parsed, "a");
2661        assert_eq!(
2662            (b.column_start, b.column_end),
2663            (1, 4),
2664            "bounding box, not a rectangle"
2665        );
2666        assert_eq!((a.column_start, a.column_end), (2, 3));
2667        assert!(
2668            a.column_start >= b.column_start && a.column_end <= b.column_end,
2669            "the two areas overlap, which taffy is never supposed to be handed"
2670        );
2671    }
2672
2673    #[test]
2674    fn parse_grid_template_areas_row_bounds_saturate_past_u16_max() {
2675        // 65_599 rows of "a" then one row of "b". True line numbers exceed u16::MAX, and
2676        // `u16::try_from(..).unwrap_or(u16::MAX)` clamps rather than panicking.
2677        let mut input = String::with_capacity(65_600 * 4);
2678        for _ in 0..65_599 {
2679            input.push_str("\"a\" ");
2680        }
2681        input.push_str("\"b\"");
2682
2683        let parsed = parse_grid_template_areas(&input).unwrap();
2684        assert_eq!(parsed.areas.len(), 2);
2685
2686        let a = area_named(&parsed, "a");
2687        assert_eq!(a.row_start, 1);
2688        assert_eq!(
2689            a.row_end,
2690            u16::MAX,
2691            "true end line is 65_600; it saturates and the grid is silently truncated"
2692        );
2693
2694        // BUG (degenerate output): "b" starts at row 65_600, so BOTH of its line numbers
2695        // clamp to u16::MAX — producing a zero-height area (row_start == row_end). Every
2696        // other area produced by this parser satisfies row_end > row_start.
2697        let b = area_named(&parsed, "b");
2698        assert_eq!(b.row_start, u16::MAX);
2699        assert_eq!(b.row_end, u16::MAX);
2700        assert_eq!(
2701            b.row_start, b.row_end,
2702            "zero-height area from double saturation"
2703        );
2704    }
2705
2706    #[test]
2707    fn parse_grid_template_areas_column_bounds_saturate_past_u16_max() {
2708        let mut row = String::with_capacity(70_000 * 2 + 2);
2709        row.push('"');
2710        for _ in 0..70_000 {
2711            row.push_str("a ");
2712        }
2713        row.push('"');
2714
2715        let parsed = parse_grid_template_areas(&row).unwrap();
2716        assert_eq!(parsed.areas.len(), 1);
2717        let a = area_named(&parsed, "a");
2718        assert_eq!(a.column_start, 1);
2719        assert_eq!(
2720            a.column_end,
2721            u16::MAX,
2722            "true end line is 70_001; clamped, not wrapped"
2723        );
2724        assert_eq!((a.row_start, a.row_end), (1, 2));
2725    }
2726
2727    #[test]
2728    fn parse_grid_template_areas_invariant_end_line_exceeds_start_line() {
2729        // The invariant PrintAsCssValue relies on (it computes `max_row - 1` on u16 and
2730        // would underflow if any row_end were 0). Holds for every non-degenerate input;
2731        // the saturation test above is the one case that violates row_end > row_start.
2732        for input in [
2733            "\"a\"",
2734            "\"a b\" \"c d\"",
2735            "\"h h h\" \"s m a\" \"f f f\"",
2736            "\". a .\" \"b a c\"",
2737            "'x'",
2738        ] {
2739            let parsed = parse_grid_template_areas(input).unwrap();
2740            for area in parsed.areas.as_ref() {
2741                assert!(area.row_start >= 1, "{input:?}: row_start must be 1-based");
2742                assert!(
2743                    area.column_start >= 1,
2744                    "{input:?}: column_start must be 1-based"
2745                );
2746                assert!(area.row_end > area.row_start, "{input:?}: {area:?}");
2747                assert!(area.column_end > area.column_start, "{input:?}: {area:?}");
2748            }
2749        }
2750    }
2751
2752    // ---------------------------------------------------------------------
2753    // Round-trips: print_as_css_value -> parse
2754    // ---------------------------------------------------------------------
2755
2756    #[test]
2757    fn roundtrip_grid_auto_flow_all_variants() {
2758        for value in [
2759            LayoutGridAutoFlow::Row,
2760            LayoutGridAutoFlow::Column,
2761            LayoutGridAutoFlow::RowDense,
2762            LayoutGridAutoFlow::ColumnDense,
2763        ] {
2764            let printed = value.print_as_css_value();
2765            assert_eq!(
2766                parse_layout_grid_auto_flow(&printed),
2767                Ok(value),
2768                "{value:?} printed as {printed:?} must re-parse to itself"
2769            );
2770        }
2771    }
2772
2773    #[test]
2774    fn roundtrip_justify_self_and_justify_items_all_variants() {
2775        for value in [
2776            LayoutJustifySelf::Auto,
2777            LayoutJustifySelf::Start,
2778            LayoutJustifySelf::End,
2779            LayoutJustifySelf::Center,
2780            LayoutJustifySelf::Stretch,
2781        ] {
2782            let printed = value.print_as_css_value();
2783            assert_eq!(parse_layout_justify_self(&printed), Ok(value), "{value:?}");
2784        }
2785        for value in [
2786            LayoutJustifyItems::Start,
2787            LayoutJustifyItems::End,
2788            LayoutJustifyItems::Center,
2789            LayoutJustifyItems::Stretch,
2790        ] {
2791            let printed = value.print_as_css_value();
2792            assert_eq!(parse_layout_justify_items(&printed), Ok(value), "{value:?}");
2793        }
2794    }
2795
2796    #[test]
2797    fn roundtrip_gap_and_fixed_tracks_are_stable() {
2798        for input in [
2799            "0px", "10px", "-20px", "1.5em", "2rem", "50%", "10mm", "1in",
2800        ] {
2801            let gap = parse_layout_gap(input).unwrap();
2802            let printed = gap.print_as_css_value();
2803            assert_eq!(
2804                parse_layout_gap(&printed).unwrap(),
2805                gap,
2806                "gap {input:?} -> {printed:?}"
2807            );
2808        }
2809        for input in [
2810            "auto",
2811            "min-content",
2812            "max-content",
2813            "100px",
2814            "25%",
2815            "fit-content(2px)",
2816        ] {
2817            let track = parse_grid_track_owned(input).unwrap();
2818            let printed = track.print_as_css_value();
2819            assert_eq!(
2820                parse_grid_track_owned(&printed),
2821                Ok(track.clone()),
2822                "track {input:?} -> {printed:?}"
2823            );
2824        }
2825    }
2826
2827    #[test]
2828    fn roundtrip_fr_track_inflates_by_100x_every_cycle() {
2829        // BUG (serialization): GridTrackSizing::Fr stores the value pre-multiplied by
2830        // FR_SCALING_FACTOR (1fr == Fr(100)), but PrintAsCssValue prints the RAW integer
2831        // — `format!("{f}fr")` — instead of dividing it back out. So `1fr` serializes as
2832        // `100fr`, and every print/parse cycle multiplies the track's flex factor by 100.
2833        // Any code path that round-trips a stylesheet (serialize a computed style, then
2834        // re-parse it) corrupts every fr track. Correct output would be "1fr".
2835        let once = parse_grid_track_owned("1fr").unwrap();
2836        assert_eq!(once, GridTrackSizing::Fr(100));
2837        assert_eq!(once.print_as_css_value(), "100fr", "should be \"1fr\"");
2838
2839        let twice = parse_grid_track_owned(&once.print_as_css_value()).unwrap();
2840        assert_eq!(
2841            twice,
2842            GridTrackSizing::Fr(10_000),
2843            "100x inflation per cycle"
2844        );
2845
2846        let thrice = parse_grid_track_owned(&twice.print_as_css_value()).unwrap();
2847        assert_eq!(thrice, GridTrackSizing::Fr(1_000_000));
2848
2849        // Four cycles overflow the i32 guard and the value is dropped entirely.
2850        let four = parse_grid_track_owned(&thrice.print_as_css_value()).unwrap();
2851        assert_eq!(four, GridTrackSizing::Fr(100_000_000));
2852        assert_eq!(
2853            parse_grid_track_owned(&four.print_as_css_value()),
2854            Err(()),
2855            "the fifth cycle exceeds i32 and the declaration is discarded"
2856        );
2857
2858        // Same bug through the minmax and template printers.
2859        assert_eq!(
2860            parse_grid_track_owned("minmax(100px, 1fr)")
2861                .unwrap()
2862                .print_as_css_value(),
2863            "minmax(100px, 100fr)"
2864        );
2865        assert_eq!(
2866            parse_grid_template("1fr 2fr").unwrap().print_as_css_value(),
2867            "100fr 200fr"
2868        );
2869    }
2870
2871    #[test]
2872    fn roundtrip_grid_template_none_and_repeat_expansion() {
2873        assert_eq!(GridTemplate::default().print_as_css_value(), "none");
2874        assert_eq!(
2875            parse_grid_template(&GridTemplate::default().print_as_css_value()).unwrap(),
2876            GridTemplate::default()
2877        );
2878
2879        // repeat() is expanded at parse time and never re-emitted as repeat().
2880        let expanded = parse_grid_template("repeat(3, 100px)").unwrap();
2881        assert_eq!(expanded.print_as_css_value(), "100px 100px 100px");
2882        assert_eq!(
2883            parse_grid_template(&expanded.print_as_css_value()).unwrap(),
2884            expanded
2885        );
2886    }
2887
2888    #[test]
2889    fn roundtrip_grid_auto_tracks_empty_prints_auto_but_reparses_to_one_track() {
2890        // GridAutoTracks::default() is *zero* tracks yet prints as "auto"; re-parsing
2891        // that text yields *one* Auto track. Structurally lossy, semantically equivalent
2892        // (taffy treats a missing grid-auto-* as `auto`), pinned so a future change to
2893        // either side is a deliberate one.
2894        let empty = GridAutoTracks::default();
2895        assert_eq!(empty.tracks.len(), 0);
2896        assert_eq!(empty.print_as_css_value(), "auto");
2897
2898        let reparsed: GridAutoTracks = parse_grid_template(&empty.print_as_css_value())
2899            .unwrap()
2900            .into();
2901        assert_eq!(reparsed.tracks.len(), 1);
2902        assert_eq!(reparsed.tracks.as_ref()[0], GridTrackSizing::Auto);
2903        assert_ne!(reparsed, empty);
2904    }
2905
2906    #[test]
2907    fn roundtrip_grid_placement_hides_a_trailing_auto_end() {
2908        // `GridPlacement { start, end: Auto }` prints only the start (no " / auto"), which
2909        // is what CSS does too. Both directions round-trip.
2910        for input in [
2911            "auto",
2912            "1",
2913            "-1",
2914            "span 2",
2915            "1 / 3",
2916            "1 / span 2",
2917            "auto / 3",
2918        ] {
2919            let placement = parse_grid_placement(input).unwrap();
2920            let printed = placement.print_as_css_value();
2921            assert_eq!(
2922                parse_grid_placement(&printed).unwrap(),
2923                placement,
2924                "{input:?} printed as {printed:?}"
2925            );
2926        }
2927        assert_eq!(
2928            GridPlacement {
2929                grid_start: GridLine::Line(1),
2930                grid_end: GridLine::Auto
2931            }
2932            .print_as_css_value(),
2933            "1"
2934        );
2935        assert_eq!(
2936            GridPlacement {
2937                grid_start: GridLine::Auto,
2938                grid_end: GridLine::Line(3)
2939            }
2940            .print_as_css_value(),
2941            "auto / 3"
2942        );
2943    }
2944
2945    #[test]
2946    fn roundtrip_named_grid_line_with_a_span_is_lossy() {
2947        // BUG (unparseable output): `GridLine::Named` with a span prints as "name N"
2948        // (e.g. "header 2"), but parse_grid_line_owned has no production for that shape —
2949        // it falls into the named catch-all and produces a line literally *named*
2950        // "header 2" with span 0. The span is silently lost. Nothing in this file can
2951        // construct a spanned Named line from CSS text, so the only way to hit this is to
2952        // build one via NamedGridLine::create and serialize it.
2953        let spanned = GridLine::Named(NamedGridLine::create("header".to_string().into(), Some(2)));
2954        assert_eq!(spanned.print_as_css_value(), "header 2");
2955
2956        let reparsed = parse_grid_line_owned(&spanned.print_as_css_value()).unwrap();
2957        assert_eq!(
2958            reparsed,
2959            GridLine::Named(NamedGridLine::create("header 2".to_string().into(), None))
2960        );
2961        assert_ne!(reparsed, spanned);
2962
2963        // Without a span it round-trips cleanly.
2964        let plain = GridLine::Named(NamedGridLine::create("header".to_string().into(), None));
2965        assert_eq!(plain.print_as_css_value(), "header");
2966        assert_eq!(
2967            parse_grid_line_owned(&plain.print_as_css_value()),
2968            Ok(plain)
2969        );
2970    }
2971
2972    #[test]
2973    fn roundtrip_grid_template_areas_rectangular_is_stable() {
2974        for input in [
2975            "\"a\"",
2976            "\"a a\"",
2977            "\"h h\" \"s m\"",
2978            "\"a a\" \"a a\"",
2979            "\". a\" \". a\"",
2980        ] {
2981            let parsed = parse_grid_template_areas(input).unwrap();
2982            let printed = parsed.print_as_css_value();
2983            assert_eq!(
2984                parse_grid_template_areas(&printed).unwrap(),
2985                parsed,
2986                "{input:?} printed as {printed:?}"
2987            );
2988        }
2989        assert_eq!(GridTemplateAreas::default().print_as_css_value(), "none");
2990        assert_eq!(
2991            parse_grid_template_areas(&GridTemplateAreas::default().print_as_css_value()).unwrap(),
2992            GridTemplateAreas::default()
2993        );
2994    }
2995
2996    #[test]
2997    fn roundtrip_grid_template_areas_non_rectangular_loses_a_whole_area() {
2998        // BUG (data loss), the printing half of the bounding-box bug above: areas are
2999        // repainted in alphabetical order, so a later name overwrites the cells of an
3000        // earlier one that its bounding box happens to cover. `"b a b"` -> `"b b b"`:
3001        // the `a` area vanishes on serialization.
3002        let parsed = parse_grid_template_areas("\"b a b\"").unwrap();
3003        assert_eq!(parsed.areas.len(), 2);
3004        assert_eq!(
3005            parsed.print_as_css_value(),
3006            "\"b b b\"",
3007            "the `a` area is erased"
3008        );
3009
3010        let reparsed = parse_grid_template_areas(&parsed.print_as_css_value()).unwrap();
3011        assert_eq!(reparsed.areas.len(), 1);
3012        assert_ne!(reparsed, parsed);
3013    }
3014
3015    // ---------------------------------------------------------------------
3016    // Error types: to_contained / to_shared
3017    // ---------------------------------------------------------------------
3018
3019    #[test]
3020    fn grid_parse_error_to_contained_and_back_is_identity() {
3021        for payload in ["", "   ", "bogus", "\u{1F600}\u{0301}", "\0", "a\nb\tc"] {
3022            let shared = GridParseError::InvalidValue(payload);
3023            let owned = shared.to_contained();
3024            assert_eq!(
3025                owned,
3026                GridParseErrorOwned::InvalidValue(payload.to_string().into())
3027            );
3028            assert_eq!(
3029                owned.to_shared(),
3030                shared,
3031                "{payload:?} must survive the round-trip"
3032            );
3033        }
3034        // A 100k-char payload: no truncation, no panic.
3035        let huge = "x".repeat(100_000);
3036        let owned = GridParseError::InvalidValue(&huge).to_contained();
3037        assert_eq!(
3038            owned.to_shared(),
3039            GridParseError::InvalidValue(huge.as_str())
3040        );
3041    }
3042
3043    #[test]
3044    fn grid_auto_flow_parse_error_to_contained_and_back_is_identity() {
3045        for payload in ["", "bogus", "\u{1F600}", "\0"] {
3046            let shared = GridAutoFlowParseError::InvalidValue(payload);
3047            let owned = shared.to_contained();
3048            assert_eq!(
3049                owned,
3050                GridAutoFlowParseErrorOwned::InvalidValue(payload.to_string().into())
3051            );
3052            assert_eq!(owned.to_shared(), shared);
3053        }
3054        let huge = "x".repeat(100_000);
3055        let owned = GridAutoFlowParseError::InvalidValue(&huge).to_contained();
3056        assert_eq!(
3057            owned.to_shared(),
3058            GridAutoFlowParseError::InvalidValue(huge.as_str())
3059        );
3060    }
3061
3062    #[test]
3063    fn justify_self_parse_error_to_contained_and_back_is_identity() {
3064        for payload in ["", "bogus", "\u{1F600}", "\0"] {
3065            let shared = JustifySelfParseError::InvalidValue(payload);
3066            let owned = shared.to_contained();
3067            assert_eq!(
3068                owned,
3069                JustifySelfParseErrorOwned::InvalidValue(payload.to_string().into())
3070            );
3071            assert_eq!(owned.to_shared(), shared);
3072        }
3073        let huge = "x".repeat(100_000);
3074        let owned = JustifySelfParseError::InvalidValue(&huge).to_contained();
3075        assert_eq!(
3076            owned.to_shared(),
3077            JustifySelfParseError::InvalidValue(huge.as_str())
3078        );
3079    }
3080
3081    #[test]
3082    fn justify_items_parse_error_to_contained_and_back_is_identity() {
3083        for payload in ["", "bogus", "\u{1F600}", "\0"] {
3084            let shared = JustifyItemsParseError::InvalidValue(payload);
3085            let owned = shared.to_contained();
3086            assert_eq!(
3087                owned,
3088                JustifyItemsParseErrorOwned::InvalidValue(payload.to_string().into())
3089            );
3090            assert_eq!(owned.to_shared(), shared);
3091        }
3092        let huge = "x".repeat(100_000);
3093        let owned = JustifyItemsParseError::InvalidValue(&huge).to_contained();
3094        assert_eq!(
3095            owned.to_shared(),
3096            JustifyItemsParseError::InvalidValue(huge.as_str())
3097        );
3098    }
3099
3100    #[test]
3101    fn error_types_round_trip_through_a_real_parse_failure() {
3102        // The realistic path: borrow an error out of a parser, own it, hand it back.
3103        let input = String::from("  span x  ");
3104        let owned = parse_grid_placement(&input).unwrap_err().to_contained();
3105        drop(input); // the owned form must not borrow the parsed input
3106        assert_eq!(owned.to_shared(), GridParseError::InvalidValue("span x"));
3107
3108        let flow = String::from("bogus-flow");
3109        let owned_flow = parse_layout_grid_auto_flow(&flow)
3110            .unwrap_err()
3111            .to_contained();
3112        drop(flow);
3113        assert_eq!(
3114            owned_flow.to_shared(),
3115            GridAutoFlowParseError::InvalidValue("bogus-flow")
3116        );
3117    }
3118
3119    // ---------------------------------------------------------------------
3120    // Debug / display invariants
3121    // ---------------------------------------------------------------------
3122
3123    #[test]
3124    fn debug_impls_match_print_as_css_value() {
3125        // GridTrackSizing / GridTemplate / GridPlacement all forward Debug to the CSS
3126        // printer, so a Debug regression is a serialization regression.
3127        let track = parse_grid_track_owned("minmax(100px, max-content)").unwrap();
3128        assert_eq!(format!("{track:?}"), track.print_as_css_value());
3129
3130        let template = parse_grid_template("100px auto").unwrap();
3131        assert_eq!(format!("{template:?}"), template.print_as_css_value());
3132
3133        let placement = parse_grid_placement("1 / span 2").unwrap();
3134        assert_eq!(format!("{placement:?}"), placement.print_as_css_value());
3135
3136        let minmax = GridMinMax {
3137            min: Box::new(GridTrackSizing::Fixed(PixelValue::px(1.0))),
3138            max: Box::new(GridTrackSizing::Auto),
3139        };
3140        assert_eq!(format!("{minmax:?}"), "minmax(1px, auto)");
3141    }
3142}