1use crate::yui::{Element, RAW_CALL, err};
29
30const ATTACHED: &[&str] = &["Grid.Row", "Grid.Column", "Grid.RowSpan", "Grid.ColumnSpan"];
31
32fn num(v: &str) -> Option<f32> {
33 v.trim()
34 .parse::<f32>()
35 .ok()
36 .filter(|n| n.is_finite() && *n >= 0.0)
37}
38
39fn length(v: &str) -> Option<String> {
41 let v = v.trim();
42 if v.eq_ignore_ascii_case("auto") {
43 return Some("::yog_api::ui::Track::auto()".into());
44 }
45 if let Some(w) = v.strip_suffix('*') {
46 let w = if w.trim().is_empty() {
47 Some(1.0)
48 } else {
49 num(w)
50 }?;
51 return Some(format!("::yog_api::ui::Track::star({w:?})"));
52 }
53 num(v).map(|px| format!("::yog_api::ui::Track::px({px:?})"))
54}
55
56fn compact(el: &Element, key: &str, value: &str, what: &str) -> Result<Vec<String>, String> {
58 if value.trim_start().starts_with('{') {
59 return err(
60 el,
61 format_args!("{key}=\"{value}\": grid definitions can't be bound"),
62 );
63 }
64 value
65 .split(',')
66 .map(|part| {
67 length(part).ok_or_else(|| {
68 format!(
69 "line {}: <{}> {key}=\"{value}\": `{}` isn't a {what} size — expected Auto, *, 2* or pixels (26)",
70 el.line,
71 el.name,
72 part.trim()
73 )
74 })
75 })
76 .collect()
77}
78
79fn elements(prop: &Element, row: bool) -> Result<Vec<String>, String> {
81 let (def, size, min, max) = if row {
82 ("RowDefinition", "height", "min_height", "max_height")
83 } else {
84 ("ColumnDefinition", "width", "min_width", "max_width")
85 };
86 if !prop.attrs.is_empty() {
87 return err(prop, "takes no attributes");
88 }
89 let mut out = Vec::new();
90 for d in &prop.children {
91 if d.name != def {
92 return err(
93 d,
94 format_args!("isn't allowed in <{}> (expected <{def}>)", prop.name),
95 );
96 }
97 if !d.children.is_empty() {
98 return err(d, "can't have children");
99 }
100 for (k, _) in &d.attrs {
101 if ![size, min, max].contains(&k.as_str()) {
102 let lower = k
103 .chars()
104 .next()
105 .is_some_and(char::is_uppercase)
106 .then(|| crate::yui::snake(k));
107 return match lower.filter(|l| [size, min, max].contains(&l.as_str())) {
108 Some(l) => err(
109 d,
110 format_args!("has no attribute `{k}` — .yui attributes are lowercase: {l}"),
111 ),
112 None => err(
113 d,
114 format_args!("has no attribute `{k}` (expected {size}, {min}, {max})"),
115 ),
116 };
117 }
118 }
119 let v = d.attr(size).unwrap_or("*");
120 let mut t = length(v).ok_or_else(|| {
121 format!(
122 "line {}: <{def}> {size}=\"{v}\": expected Auto, *, 2* or pixels (26)",
123 d.line
124 )
125 })?;
126 for (key, call) in [(min, "min"), (max, "max")] {
127 if let Some(v) = d.attr(key) {
128 let n = num(v).ok_or_else(|| {
129 format!(
130 "line {}: <{def}> {key}=\"{v}\": expected a number of pixels",
131 d.line
132 )
133 })?;
134 t.push_str(&format!(".{call}({n:?})"));
135 }
136 }
137 out.push(t);
138 }
139 Ok(out)
140}
141
142fn index(el: &Element, key: &str, v: &str, min: u16) -> Result<u16, String> {
143 v.trim()
144 .parse::<u16>()
145 .ok()
146 .filter(|n| *n >= min)
147 .ok_or_else(|| {
148 let what = if min == 0 {
149 "a whole number (0 = the first)"
150 } else {
151 "a whole number, at least 1"
152 };
153 format!(
154 "line {}: <{}> {key}=\"{v}\": expected {what}",
155 el.line, el.name
156 )
157 })
158}
159
160fn plural(n: usize, what: &str) -> String {
161 if n == 1 {
162 format!("1 {what}")
163 } else {
164 format!("{n} {what}s")
165 }
166}
167
168pub fn normalize(el: &mut Element) -> Result<(), String> {
171 let is_grid = el.name == "Grid";
172 let (mut rows, mut cols): (Option<Vec<String>>, Option<Vec<String>>) = (None, None);
173 let mut kept = Vec::with_capacity(el.children.len());
174 for child in std::mem::take(&mut el.children) {
175 let which = match child.name.as_str() {
176 "Grid.RowDefinitions" => Some(true),
177 "Grid.ColumnDefinitions" => Some(false),
178 n if n.starts_with("Grid.") => {
179 return err(
180 &child,
181 "isn't a Grid property (expected <Grid.RowDefinitions> or <Grid.ColumnDefinitions>)",
182 );
183 }
184 _ => None,
185 };
186 let Some(row) = which else {
187 kept.push(child);
188 continue;
189 };
190 if !is_grid {
191 return err(
192 &child,
193 format_args!("only goes directly inside a <Grid>, not <{}>", el.name),
194 );
195 }
196 let slot = if row { &mut rows } else { &mut cols };
197 if slot.is_some() {
198 return err(&child, "is given twice");
199 }
200 *slot = Some(elements(&child, row)?);
201 }
202 el.children = kept;
203
204 let mut calls = Vec::new();
205 if is_grid {
206 let mut attrs = Vec::new();
207 for (k, v) in std::mem::take(&mut el.attrs) {
208 match k.as_str() {
209 "rows" | "columns" => {
210 let row = k == "rows";
211 let slot = if row { &mut rows } else { &mut cols };
212 if slot.is_some() {
213 let prop = if row {
214 "Grid.RowDefinitions"
215 } else {
216 "Grid.ColumnDefinitions"
217 };
218 return err(
219 el,
220 format_args!("{k}=\"...\" and <{prop}> both define the {k} — keep one"),
221 );
222 }
223 *slot = Some(compact(el, &k, &v, if row { "row" } else { "column" })?);
224 }
225 "row_spacing" | "column_spacing" => {
226 let n = num(&v).ok_or_else(|| {
227 format!(
228 "line {}: <Grid> {k}=\"{v}\": expected a number of pixels",
229 el.line
230 )
231 })?;
232 let m = if k == "row_spacing" {
233 "row_spacing"
234 } else {
235 "col_spacing"
236 };
237 calls.push(format!("{m}({n:?})"));
238 }
239 _ => attrs.push((k, v)),
240 }
241 }
242 el.attrs = attrs;
243 if let Some(r) = &rows {
244 calls.push(format!("rows([{}])", r.join(", ")));
245 }
246 if let Some(c) = &cols {
247 calls.push(format!("cols([{}])", c.join(", ")));
248 }
249 }
250 push_calls(el, calls, "g");
251
252 let (n_rows, n_cols) = (
253 rows.as_ref().map_or(1, Vec::len),
254 cols.as_ref().map_or(1, Vec::len),
255 );
256 for child in &mut el.children {
257 let mut cell = [0u16, 0, 1, 1];
258 let mut any = false;
259 let mut attrs = Vec::new();
260 for (k, v) in std::mem::take(&mut child.attrs) {
261 let Some(i) = ATTACHED.iter().position(|a| *a == k) else {
262 if k.starts_with("Grid.") {
263 return err(
264 child,
265 format_args!("has no attribute `{k}` (attached: {})", ATTACHED.join(", ")),
266 );
267 }
268 attrs.push((k, v));
269 continue;
270 };
271 if !is_grid {
272 return err(
273 child,
274 format_args!(
275 "{k}: only children of a <Grid> have grid cells (this is in a <{}>)",
276 el.name
277 ),
278 );
279 }
280 if v.trim_start().starts_with('{') {
281 return err(
282 child,
283 format_args!("{k}=\"{v}\": grid cells can't be bound"),
284 );
285 }
286 cell[i] = index(child, &k, &v, if i < 2 { 0 } else { 1 })?;
287 any = true;
288 }
289 child.attrs = attrs;
290 if !is_grid {
291 continue;
292 }
293 let [r, c, rs, cs] = cell;
294 for (start, span, n, what, key) in [
295 (r, rs, n_rows, "row", "Grid.Row"),
296 (c, cs, n_cols, "column", "Grid.Column"),
297 ] {
298 let (start, span) = (start as usize, span as usize);
299 if start >= n {
300 return err(
301 child,
302 format_args!(
303 "{key}=\"{start}\" is past the grid's last {what} (it has {}, numbered from 0)",
304 plural(n, what)
305 ),
306 );
307 }
308 if start + span > n {
309 return err(
310 child,
311 format_args!(
312 "{key}Span=\"{span}\" from {what} {start} runs past the grid's last {what} (it has {})",
313 plural(n, what)
314 ),
315 );
316 }
317 }
318 if any {
319 let mut calls = vec![format!("cell({r}, {c})")];
320 if (rs, cs) != (1, 1) {
321 calls.push(format!("span({rs}, {cs})"));
322 }
323 push_calls(child, calls, "c");
324 }
325 }
326 for child in &mut el.children {
327 normalize(child)?;
328 }
329 Ok(())
330}
331
332fn push_calls(el: &mut Element, calls: Vec<String>, tag: &str) {
334 for (i, c) in calls.into_iter().enumerate() {
335 el.attrs.push((format!("{RAW_CALL}{tag}{i}"), c));
336 }
337}
338
339#[cfg(test)]
340mod tests {
341 use crate::yui;
342
343 fn codegen(body: &str) -> Result<String, String> {
344 yui::generate(
345 &format!("<Screen id=\"m:s\" width=\"100\" height=\"100\">{body}</Screen>"),
346 "s",
347 )
348 }
349
350 #[test]
351 fn compact_and_element_forms() {
352 let code = codegen(
353 r#"<Grid rows="Auto, *, 26" row_spacing="2" column_spacing="4">
354 <Grid.ColumnDefinitions>
355 <ColumnDefinition width="2*" min_width="80"/>
356 <ColumnDefinition max_width="200"/>
357 </Grid.ColumnDefinitions>
358 <Label text="a" Grid.Row="1" Grid.Column="1" halign="right" margin="2 4"/>
359 <Label text="b" Grid.RowSpan="3"/>
360 <Label text="c"/>
361 </Grid>"#,
362 )
363 .unwrap();
364 assert!(code.contains("widget::grid()"), "{code}");
365 assert!(code.contains(".row_spacing(2.0)"), "{code}");
366 assert!(code.contains(".col_spacing(4.0)"), "{code}");
367 assert!(
368 code.contains(".rows([::yog_api::ui::Track::auto(), ::yog_api::ui::Track::star(1.0), ::yog_api::ui::Track::px(26.0)])"),
369 "{code}"
370 );
371 assert!(
372 code.contains(".cols([::yog_api::ui::Track::star(2.0).min(80.0), ::yog_api::ui::Track::star(1.0).max(200.0)])"),
373 "{code}"
374 );
375 assert!(code.contains(".cell(1, 1)"), "{code}");
376 assert!(
377 code.contains(".halign(::yog_api::ui::HAlign::Right)"),
378 "{code}"
379 );
380 assert!(code.contains(".margin(2.0, 4.0, 2.0, 4.0)"), "{code}");
381 assert!(
382 code.contains(".cell(0, 0)\n") && code.contains(".span(3, 1)"),
383 "{code}"
384 );
385 }
386
387 #[test]
388 fn errors() {
389 let e = |body: &str| codegen(body).unwrap_err();
390 assert!(e(r#"<Grid rows="abc"/>"#).contains("`abc` isn't a row size"));
391 assert!(e(r#"<Grid><Grid.RowDefinitions><RowDefinition Height="Auto"/></Grid.RowDefinitions></Grid>"#)
392 .contains("lowercase: height"));
393 assert!(e(
394 r#"<Grid><Grid.RowDefinitions><RowDefinition height="x"/></Grid.RowDefinitions></Grid>"#
395 )
396 .contains("height=\"x\""));
397 assert!(
398 e(r#"<Grid rows="*,*"><Label Grid.Row="2"/></Grid>"#)
399 .contains("past the grid's last row (it has 2 rows")
400 );
401 assert!(
402 e(r#"<Grid columns="*,*"><Label Grid.Column="1" Grid.ColumnSpan="2"/></Grid>"#)
403 .contains("Grid.ColumnSpan=\"2\" from column 1")
404 );
405 assert!(e(r#"<Grid><Label Grid.Row="1"/></Grid>"#).contains("it has 1 row,"));
406 assert!(
407 e(r#"<Column><Label Grid.Row="0"/></Column>"#).contains("only children of a <Grid>")
408 );
409 assert!(e(r#"<Grid rows="*"><Grid.RowDefinitions/></Grid>"#).contains("keep one"));
410 assert!(
411 e(r#"<Column><Grid.RowDefinitions/></Column>"#).contains("directly inside a <Grid>")
412 );
413 assert!(e(r#"<Grid><Label Grid.RowSpan="0"/></Grid>"#).contains("at least 1"));
414 assert!(e(r#"<Grid><Label HorizontalAlignment="Left"/></Grid>"#).contains("halign"));
415 assert!(
416 e(r#"<Grid><Label halign="middle"/></Grid>"#).contains("left, center, right, stretch")
417 );
418 }
419}