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