1use taffy::prelude::*;
8use taffy::style::{
9 GridPlacement, GridTemplateArea, GridTemplateComponent, GridTemplateRepetition,
10 MaxTrackSizingFunction, MinTrackSizingFunction, RepetitionCount, TrackSizingFunction,
11};
12
13fn split_top_level(s: &str, sep_comma: bool) -> Vec<String> {
15 let mut out = Vec::new();
16 let mut cur = String::new();
17 let mut depth = 0i32;
18 for ch in s.chars() {
19 match ch {
20 '(' => {
21 depth += 1;
22 cur.push(ch);
23 }
24 ')' => {
25 depth -= 1;
26 cur.push(ch);
27 }
28 ',' if sep_comma && depth == 0 => {
29 if !cur.trim().is_empty() {
30 out.push(cur.trim().to_string());
31 }
32 cur.clear();
33 }
34 c if c.is_whitespace() && depth == 0 && !sep_comma => {
35 if !cur.trim().is_empty() {
36 out.push(cur.trim().to_string());
37 }
38 cur.clear();
39 }
40 _ => cur.push(ch),
41 }
42 }
43 if !cur.trim().is_empty() {
44 out.push(cur.trim().to_string());
45 }
46 out
47}
48
49fn number_prefix(s: &str, suffix: &str) -> Option<f32> {
50 s.strip_suffix(suffix).and_then(|n| n.trim().parse::<f32>().ok())
51}
52
53fn min_fn(tok: &str) -> Result<MinTrackSizingFunction, String> {
54 Ok(match tok {
55 "auto" => MinTrackSizingFunction::auto(),
56 "min-content" => MinTrackSizingFunction::min_content(),
57 "max-content" => MinTrackSizingFunction::max_content(),
58 t => {
59 if let Some(v) = number_prefix(t, "px") {
60 MinTrackSizingFunction::length(v)
61 } else if let Some(v) = number_prefix(t, "%") {
62 MinTrackSizingFunction::percent(v / 100.0)
63 } else if let Ok(v) = t.parse::<f32>() {
64 MinTrackSizingFunction::length(v)
65 } else {
66 return Err(format!("unsupported min track size {t:?}"));
67 }
68 }
69 })
70}
71
72fn max_fn(tok: &str) -> Result<MaxTrackSizingFunction, String> {
73 Ok(match tok {
74 "auto" => MaxTrackSizingFunction::auto(),
75 "min-content" => MaxTrackSizingFunction::min_content(),
76 "max-content" => MaxTrackSizingFunction::max_content(),
77 t => {
78 if let Some(v) = number_prefix(t, "fr") {
79 MaxTrackSizingFunction::fr(v)
80 } else if let Some(v) = number_prefix(t, "px") {
81 MaxTrackSizingFunction::length(v)
82 } else if let Some(v) = number_prefix(t, "%") {
83 MaxTrackSizingFunction::percent(v / 100.0)
84 } else if let Ok(v) = t.parse::<f32>() {
85 MaxTrackSizingFunction::length(v)
86 } else {
87 return Err(format!("unsupported max track size {t:?}"));
88 }
89 }
90 })
91}
92
93fn track(tok: &str) -> Result<TrackSizingFunction, String> {
95 if let Some(inner) = tok.strip_prefix("minmax(").and_then(|s| s.strip_suffix(')')) {
96 let parts = split_top_level(inner, true);
97 if parts.len() != 2 {
98 return Err(format!("minmax needs two arguments: {tok:?}"));
99 }
100 return Ok(minmax(min_fn(&parts[0])?, max_fn(&parts[1])?));
101 }
102 if tok.ends_with("fr") {
103 return Ok(minmax(MinTrackSizingFunction::auto(), max_fn(tok)?));
104 }
105 Ok(minmax(min_fn(tok)?, max_fn(tok)?))
106}
107
108pub fn parse_tracks(s: &str) -> Result<Vec<GridTemplateComponent<String>>, String> {
110 let mut out = Vec::new();
111 for tok in split_top_level(s, false) {
112 if let Some(inner) = tok.strip_prefix("repeat(").and_then(|s| s.strip_suffix(')')) {
113 let parts = split_top_level(inner, true);
114 if parts.len() != 2 {
115 return Err(format!("repeat needs two arguments: {tok:?}"));
116 }
117 let count = match parts[0].as_str() {
118 "auto-fill" => RepetitionCount::AutoFill,
119 "auto-fit" => RepetitionCount::AutoFit,
120 n => RepetitionCount::Count(n.parse::<u16>().map_err(|_| format!("bad repeat count {n:?}"))?),
121 };
122 let tracks = split_top_level(&parts[1], false).iter().map(|t| track(t)).collect::<Result<Vec<_>, _>>()?;
123 let line_names = vec![Vec::new(); tracks.len() + 1];
124 out.push(GridTemplateComponent::Repeat(GridTemplateRepetition { count, tracks, line_names }));
125 } else {
126 out.push(GridTemplateComponent::Single(track(&tok)?));
127 }
128 }
129 Ok(out)
130}
131
132pub fn parse_areas(rows: &[String]) -> Result<Vec<GridTemplateArea<String>>, String> {
135 let mut areas: Vec<GridTemplateArea<String>> = Vec::new();
136 for (r, row) in rows.iter().enumerate() {
137 for (c, name) in row.split_whitespace().enumerate() {
138 if name == "." {
139 continue;
140 }
141 let row_line = (r + 1) as u16;
142 let col_line = (c + 1) as u16;
143 if let Some(a) = areas.iter_mut().find(|a| a.name == name) {
144 a.row_start = a.row_start.min(row_line);
145 a.row_end = a.row_end.max(row_line + 1);
146 a.column_start = a.column_start.min(col_line);
147 a.column_end = a.column_end.max(col_line + 1);
148 } else {
149 areas.push(GridTemplateArea {
150 name: name.to_string(),
151 row_start: row_line,
152 row_end: row_line + 1,
153 column_start: col_line,
154 column_end: col_line + 1,
155 });
156 }
157 }
158 }
159 for a in &areas {
160 for r in a.row_start..a.row_end {
161 let cells: Vec<&str> = rows.get((r - 1) as usize).map(|row| row.split_whitespace().collect()).unwrap_or_default();
162 for c in a.column_start..a.column_end {
163 if cells.get((c - 1) as usize).copied() != Some(a.name.as_str()) {
164 return Err(format!("grid area {:?} is not rectangular", a.name));
165 }
166 }
167 }
168 }
169 Ok(areas)
170}
171
172fn placement(tok: &str) -> Result<GridPlacement<String>, String> {
173 let tok = tok.trim();
174 if tok == "auto" {
175 return Ok(GridPlacement::Auto);
176 }
177 if let Some(n) = tok.strip_prefix("span ") {
178 return Ok(GridPlacement::Span(n.trim().parse::<u16>().map_err(|_| format!("bad span {tok:?}"))?));
179 }
180 if let Ok(n) = tok.parse::<i16>() {
181 return Ok(line(n));
182 }
183 Ok(GridPlacement::NamedLine(tok.to_string(), 0))
184}
185
186pub fn parse_placement(s: &str) -> Result<Line<GridPlacement<String>>, String> {
188 let parts: Vec<&str> = s.split('/').map(str::trim).collect();
189 match parts.as_slice() {
190 [one] => {
191 let start = placement(one)?;
192 let end = match &start {
193 GridPlacement::NamedLine(name, i) => GridPlacement::NamedLine(name.clone(), *i),
194 GridPlacement::Span(_) => start.clone(),
195 _ => GridPlacement::Auto,
196 };
197 Ok(Line { start, end })
198 }
199 [a, b] => Ok(Line { start: placement(a)?, end: placement(b)? }),
200 _ => Err(format!("unsupported grid placement {s:?}")),
201 }
202}
203
204pub fn parse_area_placement(name: &str) -> Line<GridPlacement<String>> {
206 Line { start: GridPlacement::NamedLine(name.to_string(), 0), end: GridPlacement::NamedLine(name.to_string(), 0) }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn parses_mixed_tracks() {
215 let t = parse_tracks("minmax(180px, 1fr) 2fr auto 120 repeat(2, 1fr 40px)").unwrap();
216 assert_eq!(t.len(), 5);
217 assert!(matches!(t[4], GridTemplateComponent::Repeat(_)));
218 let t = parse_tracks("repeat(3, minmax(0, 1fr))").unwrap();
219 assert_eq!(t.len(), 1);
220 }
221
222 #[test]
223 fn parses_areas() {
224 let a = parse_areas(&["nav main".into(), "nav footer".into()]).unwrap();
225 let nav = a.iter().find(|x| x.name == "nav").unwrap();
226 assert_eq!((nav.row_start, nav.row_end, nav.column_start, nav.column_end), (1, 3, 1, 2));
227 }
228
229 #[test]
230 fn rejects_l_shapes() {
231 assert!(parse_areas(&["a a".into(), "a b".into()]).is_err());
232 }
233
234 #[test]
235 fn parses_placements() {
236 assert!(matches!(parse_placement("1 / 3").unwrap().end, GridPlacement::Line(_)));
237 assert!(matches!(parse_placement("1 / -1").unwrap().end, GridPlacement::Line(_)));
238 assert!(matches!(parse_placement("span 2").unwrap().start, GridPlacement::Span(2)));
239 assert!(matches!(parse_placement("nav").unwrap().end, GridPlacement::NamedLine(_, 0)));
240 }
241}