tawasuyu

tawasuyu/taffy

A high performance rust-powered UI layout library

Rust 1061HTML 997Markdown 12TOML 83120 Sutichaykuna

⎇ 08376be245a487c3d3438dfd5108a67f48222739 scripts/gentest/src/main.rs Rust · 922 siq'ikuna · 36.8 KB Ñawpa willay Pi imata llamkarqan ch'uya

Format inline css in text fixtures (#465) 08376be24 · Nico Burns ·

1use std::fs;
2use std::path::{Path, PathBuf};
3use std::process::Command;
4
5use convert_case::{Case, Casing};
6use fantoccini::{Client, ClientBuilder};
7use log::*;
8use proc_macro2::{Span, TokenStream};
9use quote::{format_ident, quote};
10use serde_json::Value;
11use syn::Ident;
12
13#[tokio::main]
14async fn main() {
15    env_logger::init();
16    let root_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
17    let repo_root = root_dir.parent().and_then(Path::parent).unwrap();
18
19    let fixtures_root = repo_root.join("test_fixtures");
20    let fixtures = fs::read_dir(fixtures_root).unwrap();
21
22    info!("reading test fixtures from disk");
23    let mut fixtures: Vec<_> = fixtures
24        .into_iter()
25        .filter_map(|a| a.ok())
26        .filter(|f| !f.file_name().to_string_lossy().starts_with('x')) // ignore tests beginning with x
27        .filter(|f| f.path().is_file() && f.path().extension().map(|p| p == "html").unwrap_or(false))
28        .map(|f| {
29            let fixture_path = f.path();
30            let name = fixture_path.file_stem().unwrap().to_str().unwrap().to_string();
31            (name, fixture_path)
32        })
33        .collect();
34    fixtures.sort_unstable_by_key(|f| f.0.clone());
35
36    info!("starting webdriver instance");
37    let webdriver_url = "http://localhost:4444";
38    let mut webdriver_handle = Command::new("chromedriver")
39        .arg("--port=4444")
40        .spawn()
41        .expect("ChromeDriver not found: Make sure you have it installed and added to your PATH.");
42
43    // this is silly, but it works
44    std::thread::sleep(std::time::Duration::from_secs(1));
45
46    let mut caps = serde_json::map::Map::new();
47    let chrome_opts = serde_json::json!({ "args": ["--headless", "--disable-gpu"] });
48    caps.insert("goog:chromeOptions".to_string(), chrome_opts.clone());
49
50    info!("spawning webdriver client and collecting test descriptions");
51    let client = ClientBuilder::native().capabilities(caps.clone()).connect(webdriver_url).await.unwrap();
52
53    asserts_non_zero_width_scrollbars(client.clone()).await;
54
55    let mut test_descs = vec![];
56    for (name, fixture_path) in fixtures {
57        test_descs.push(test_root_element(client.clone(), name, fixture_path).await);
58    }
59
60    info!("killing webdriver instance...");
61    webdriver_handle.kill().unwrap();
62
63    info!("generating test sources and concatenating...");
64
65    let test_descs: Vec<_> = test_descs
66        .iter()
67        .map(|(name, description)| {
68            debug!("generating test contents for {}", &name);
69            (name.clone(), generate_test(name, description))
70        })
71        .collect();
72
73    let mod_statemnts = test_descs
74        .iter()
75        .map(|(name, _)| {
76            let name_ident = Ident::new(name, Span::call_site());
77            if name.starts_with("grid") {
78                quote!(#[cfg(feature = "grid")] mod #name_ident;)
79            } else {
80                quote!(mod #name_ident;)
81            }
82        })
83        .fold(quote!(), |a, b| quote!(#a #b));
84    let generic_measure_function = generate_generic_measure_function();
85
86    let test_mod_file = quote!(
87        #generic_measure_function
88        #mod_statemnts
89    );
90
91    info!("writing generated test file to disk...");
92    let tests_base_path = repo_root.join("tests").join("generated");
93    fs::remove_dir_all(&tests_base_path).unwrap();
94    fs::create_dir(&tests_base_path).unwrap();
95    for (name, test_body) in test_descs {
96        let mut test_filename = tests_base_path.join(&name);
97        test_filename.set_extension("rs");
98        debug!("writing {} to disk...", &name);
99        fs::write(test_filename, test_body.to_string()).unwrap();
100    }
101    fs::write(tests_base_path.join("mod.rs"), test_mod_file.to_string()).unwrap();
102
103    info!("formatting the source directory");
104    Command::new("cargo").arg("fmt").current_dir(repo_root).status().unwrap();
105}
106
107async fn asserts_non_zero_width_scrollbars(client: Client) {
108    // Load minimal test page defined in the string
109    const TEST_PAGE: &str = r#"data:text/html;charset=utf-8,<html><body><div style="overflow:scroll" /></body></html>"#;
110    client.goto(TEST_PAGE).await.unwrap();
111
112    // Determine the width of the scrollbar
113    let scrollbar_width = client
114        .execute("return document.body.firstChild.clientWidth - document.body.firstChild.offsetWidth;", vec![])
115        .await
116        .unwrap();
117    let Value::Number(scrollbar_width) = scrollbar_width else { panic!("Error retrieving scrollbar_width") };
118    let scrollbar_width = scrollbar_width.as_f64().unwrap();
119
120    if scrollbar_width == 0.0 {
121        panic!(concat!(
122            "\n\n    Error: Scrollbar width of zero detected. This test generation script must be run with scrollbars set to take up space.\n",
123            "    On macOS this can be done by setting Show Scrollbars to 'always' in the Appearance section of the System Settings app.\n\n"
124        ))
125    }
126}
127
128async fn test_root_element(client: Client, name: String, fixture_path: impl AsRef<Path>) -> (String, Value) {
129    let fixture_path = fixture_path.as_ref();
130
131    let url = format!("file://{}", fixture_path.display());
132
133    client.goto(&url).await.unwrap();
134    let description = client
135        .execute("return JSON.stringify(describeElement(document.getElementById('test-root')))", vec![])
136        .await
137        .unwrap();
138    let description_string = description.as_str().unwrap();
139    let description = serde_json::from_str(description_string).unwrap();
140    (name, description)
141}
142
143fn generate_test(name: impl AsRef<str>, description: &Value) -> TokenStream {
144    let name = name.as_ref();
145    let name = Ident::new(name, Span::call_site());
146    let use_rounding = description["useRounding"].as_bool().unwrap();
147    let assertions = generate_assertions("node", description, use_rounding);
148    let node_description = generate_node("node", description);
149
150    let set_rounding_mode = if use_rounding { quote!() } else { quote!(taffy.disable_rounding();) };
151
152    quote!(
153        #[test]
154        fn #name() {
155            #[allow(unused_imports)]
156            use taffy::{tree::Layout, prelude::*};
157            let mut taffy = taffy::Taffy::new();
158            #set_rounding_mode
159            #node_description
160            taffy.compute_layout(node, taffy::geometry::Size::MAX_CONTENT).unwrap();
161
162            println!("\nComputed tree:");
163            taffy::util::print_tree(&taffy, node);
164            println!();
165
166            #assertions
167        }
168    )
169}
170
171fn generate_assertions(ident: &str, node: &Value, use_rounding: bool) -> TokenStream {
172    let layout = if use_rounding { &node["smartRoundedLayout"] } else { &node["unroundedLayout"] };
173
174    let read_f32 = |s: &str| layout[s].as_f64().unwrap() as f32;
175    let width = read_f32("width");
176    let height = read_f32("height");
177    let x = read_f32("x");
178    let y = read_f32("y");
179
180    let children = {
181        let mut c = Vec::new();
182        if let Value::Array(ref value) = node["children"] {
183            for (idx, child) in value.iter().enumerate() {
184                c.push(generate_assertions(&format!("{ident}{idx}"), child, use_rounding));
185            }
186        };
187        c.into_iter().fold(quote!(), |a, b| quote!(#a #b))
188    };
189
190    let ident = Ident::new(ident, Span::call_site());
191
192    if use_rounding {
193        quote!(
194            let Layout { size, location, .. } = taffy.layout(#ident).unwrap();
195            assert_eq!(size.width, #width, "width of node {:?}. Expected {}. Actual {}", #ident,  #width, size.width);
196            assert_eq!(size.height, #height, "height of node {:?}. Expected {}. Actual {}", #ident,  #height, size.height);
197            assert_eq!(location.x, #x, "x of node {:?}. Expected {}. Actual {}", #ident,  #x, location.x);
198            assert_eq!(location.y, #y, "y of node {:?}. Expected {}. Actual {}", #ident,  #y, location.y);
199
200            #children
201        )
202    } else {
203        quote!(
204            let Layout { size, location, .. } = taffy.layout(#ident).unwrap();
205            assert!(size.width - #width < 0.1, "width of node {:?}. Expected {}. Actual {}", #ident,  #width, size.width);
206            assert!(size.height - #height < 0.1, "height of node {:?}. Expected {}. Actual {}", #ident,  #height, size.height);
207            assert!(location.x - #x < 0.1, "x of node {:?}. Expected {}. Actual {}", #ident,  #x, location.x);
208            assert!(location.y - #y < 0.1, "y of node {:?}. Expected {}. Actual {}", #ident,  #y, location.y);
209
210            #children
211        )
212    }
213}
214
215macro_rules! dim_quoted_renamed {
216    ($obj:ident, $in_name:ident, $out_name:ident, $value_mapper:ident, $default:expr) => {
217        let $out_name = match $obj.get(stringify!($in_name)) {
218            Some(Value::Object(ref value)) => {
219                let dim = $value_mapper(value);
220                quote!($out_name: #dim,)
221            }
222            _ => {
223                let dim = $default;
224                quote!($out_name: #dim,)
225            }
226        };
227    };
228}
229
230macro_rules! dim_quoted {
231    ($obj:ident, $dim_name:ident, $value_mapper: ident, $default:expr) => {
232        dim_quoted_renamed!($obj, $dim_name, $dim_name, $value_mapper, $default)
233    };
234}
235
236macro_rules! edges_quoted {
237    ($style:ident, $val:ident, $value_mapper:ident, $default_value: expr) => {
238        let $val = match $style[stringify!($val)] {
239            Value::Object(ref value) => {
240                dim_quoted!(value, left, $value_mapper, $default_value);
241                dim_quoted!(value, right, $value_mapper, $default_value);
242                dim_quoted!(value, top, $value_mapper, $default_value);
243                dim_quoted!(value, bottom, $value_mapper, $default_value);
244
245                let edges = quote!(taffy::geometry::Rect {
246                    #left #right #top #bottom
247                });
248
249                quote!($val: #edges,)
250            },
251            _ => quote!(),
252        };
253    };
254}
255
256fn generate_node(ident: &str, node: &Value) -> TokenStream {
257    let style = &node["style"];
258
259    fn snake_case_ident(ident_name: &str) -> Ident {
260        let name_snake_case = ident_name.to_case(Case::Snake);
261        format_ident!("{}", name_snake_case)
262    }
263
264    fn quote_object_value(
265        prop_name: &str,
266        style: &Value,
267        quoter: impl Fn(&serde_json::Map<String, Value>) -> TokenStream,
268    ) -> Option<TokenStream> {
269        match style[prop_name.to_case(Case::Camel)] {
270            Value::Object(ref value) => Some(quoter(value)),
271            _ => None,
272        }
273    }
274
275    fn quote_prop(prop_name: &str, value: TokenStream) -> TokenStream {
276        let ident = snake_case_ident(prop_name);
277        quote!(#ident: #value,)
278    }
279
280    fn quote_object_prop(
281        prop_name: &str,
282        style: &Value,
283        quoter: impl Fn(&serde_json::Map<String, Value>) -> TokenStream,
284    ) -> TokenStream {
285        match quote_object_value(prop_name, style, quoter) {
286            Some(prop_value) => quote_prop(prop_name, prop_value),
287            None => quote!(),
288        }
289    }
290
291    fn quote_array_prop(prop_name: &str, style: &Value, quoter: impl Fn(&[Value]) -> TokenStream) -> TokenStream {
292        let prop_name_snake_case = prop_name.to_case(Case::Snake);
293        let prop_name_camel_case = prop_name.to_case(Case::Camel);
294        let prop_name_ident = format_ident!("{}", prop_name_snake_case);
295        match style[prop_name_camel_case] {
296            Value::Array(ref value) => {
297                let prop_value = quoter(value);
298                quote!(#prop_name_ident: #prop_value,)
299            }
300            _ => quote!(),
301        }
302    }
303
304    fn get_string_value<'a, 'b, 'c: 'b>(prop_name: &'a str, style: &'c Value) -> Option<&'b str> {
305        match style[prop_name.to_case(Case::Camel)] {
306            Value::String(ref value) => Some(value),
307            _ => None,
308        }
309    }
310
311    fn get_number_value<'a, 'b, 'c: 'b>(prop_name: &'a str, style: &'c Value) -> Option<f32> {
312        match style[prop_name.to_case(Case::Camel)] {
313            Value::Number(ref value) => Some(value.as_f64().unwrap() as f32),
314            _ => None,
315        }
316    }
317
318    fn quote_number_prop(prop_name: &str, style: &Value, quoter: impl Fn(f32) -> TokenStream) -> TokenStream {
319        let prop_name_snake_case = prop_name.to_case(Case::Snake);
320        let prop_name_camel_case = prop_name.to_case(Case::Camel);
321        let prop_name_ident = format_ident!("{}", prop_name_snake_case);
322        match style[prop_name_camel_case] {
323            Value::Number(ref value) => {
324                let prop_value = quoter(value.as_f64().unwrap() as f32);
325                quote!(#prop_name_ident: #prop_value,)
326            }
327            _ => quote!(),
328        }
329    }
330
331    let display = match style["display"] {
332        Value::String(ref value) => match value.as_ref() {
333            "none" => quote!(display: taffy::style::Display::None,),
334            "grid" => quote!(display: taffy::style::Display::Grid,),
335            _ => quote!(display: taffy::style::Display::Flex,),
336        },
337        _ => quote!(),
338    };
339
340    let position = match style["position"] {
341        Value::String(ref value) => match value.as_ref() {
342            "absolute" => quote!(position: taffy::style::Position::Absolute,),
343            _ => quote!(),
344        },
345        _ => quote!(),
346    };
347
348    let direction = match style["direction"] {
349        Value::String(ref value) => match value.as_ref() {
350            "rtl" => quote!(direction: taffy::style::Direction::RTL,),
351            "ltr" => quote!(direction: taffy::style::Direction::LTR,),
352            _ => quote!(),
353        },
354        _ => quote!(),
355    };
356
357    let flex_direction = match style["flexDirection"] {
358        Value::String(ref value) => match value.as_ref() {
359            "row-reverse" => quote!(flex_direction: taffy::style::FlexDirection::RowReverse,),
360            "column" => quote!(flex_direction: taffy::style::FlexDirection::Column,),
361            "column-reverse" => quote!(flex_direction: taffy::style::FlexDirection::ColumnReverse,),
362            _ => quote!(),
363        },
364        _ => quote!(),
365    };
366
367    let flex_wrap = match style["flexWrap"] {
368        Value::String(ref value) => match value.as_ref() {
369            "wrap" => quote!(flex_wrap: taffy::style::FlexWrap::Wrap,),
370            "wrap-reverse" => quote!(flex_wrap: taffy::style::FlexWrap::WrapReverse,),
371            _ => quote!(),
372        },
373        _ => quote!(),
374    };
375
376    fn quote_overflow(overflow: &Value) -> Option<TokenStream> {
377        match overflow {
378            Value::String(ref value) => match value.as_ref() {
379                "hidden" => Some(quote!(taffy::style::Overflow::Hidden)),
380                "scroll" => Some(quote!(taffy::style::Overflow::Scroll)),
381                "auto" => Some(quote!(taffy::style::Overflow::Auto)),
382                _ => None,
383            },
384            _ => None,
385        }
386    }
387    let overflow_x = quote_overflow(&style["overflowX"]);
388    let overflow_y = quote_overflow(&style["overflowY"]);
389    let (overflow, scrollbar_width) = if overflow_x.is_some() || overflow_y.is_some() {
390        let overflow_x = overflow_x.unwrap_or(quote!(taffy::style::Overflow::Visible));
391        let overflow_y = overflow_y.unwrap_or(quote!(taffy::style::Overflow::Visible));
392        let overflow = quote!(overflow: taffy::geometry::Point { x: #overflow_x, y: #overflow_y },);
393        let scrollbar_width = quote_number_prop("scrollbar_width", style, |value: f32| quote!(#value));
394        (overflow, scrollbar_width)
395    } else {
396        (quote!(), quote!())
397    };
398
399    let align_items = match style["alignItems"] {
400        Value::String(ref value) => match value.as_ref() {
401            "start" => quote!(align_items: Some(taffy::style::AlignItems::Start),),
402            "end" => quote!(align_items: Some(taffy::style::AlignItems::End),),
403            "flex-start" => quote!(align_items: Some(taffy::style::AlignItems::FlexStart),),
404            "flex-end" => quote!(align_items: Some(taffy::style::AlignItems::FlexEnd),),
405            "center" => quote!(align_items: Some(taffy::style::AlignItems::Center),),
406            "baseline" => quote!(align_items: Some(taffy::style::AlignItems::Baseline),),
407            "stretch" => quote!(align_items: Some(taffy::style::AlignItems::Stretch),),
408            _ => quote!(),
409        },
410        _ => quote!(),
411    };
412
413    let align_self = match style["alignSelf"] {
414        Value::String(ref value) => match value.as_ref() {
415            "start" => quote!(align_self: Some(taffy::style::AlignSelf::Start),),
416            "end" => quote!(align_self: Some(taffy::style::AlignSelf::End),),
417            "flex-start" => quote!(align_self: Some(taffy::style::AlignSelf::FlexStart),),
418            "flex-end" => quote!(align_self: Some(taffy::style::AlignSelf::FlexEnd),),
419            "center" => quote!(align_self: Some(taffy::style::AlignSelf::Center),),
420            "baseline" => quote!(align_self: Some(taffy::style::AlignSelf::Baseline),),
421            "stretch" => quote!(align_self: Some(taffy::style::AlignSelf::Stretch),),
422            _ => quote!(),
423        },
424        _ => quote!(),
425    };
426
427    let justify_items = match style["justifyItems"] {
428        Value::String(ref value) => match value.as_ref() {
429            "start" => quote!(justify_items: Some(taffy::style::JustifyItems::Start),),
430            "end" => quote!(justify_items: Some(taffy::style::JustifyItems::End),),
431            "flex-start" => quote!(justify_items: Some(taffy::style::JustifyItems::FlexStart),),
432            "flex-end" => quote!(justify_items: Some(taffy::style::JustifyItems::FlexEnd),),
433            "center" => quote!(justify_items: Some(taffy::style::JustifyItems::Center),),
434            "baseline" => quote!(justify_items: Some(taffy::style::JustifyItems::Baseline),),
435            "stretch" => quote!(justify_items: Some(taffy::style::JustifyItems::Stretch),),
436            _ => quote!(),
437        },
438        _ => quote!(),
439    };
440
441    let justify_self = match style["justifySelf"] {
442        Value::String(ref value) => match value.as_ref() {
443            "start" => quote!(justify_self: Some(taffy::style::JustifySelf::Start),),
444            "end" => quote!(justify_self: Some(taffy::style::JustifySelf::End),),
445            "flex-start" => quote!(justify_self: Some(taffy::style::JustifySelf::FlexStart),),
446            "flex-end" => quote!(justify_self: Some(taffy::style::JustifySelf::FlexEnd),),
447            "center" => quote!(justify_self: Some(taffy::style::JustifySelf::Center),),
448            "baseline" => quote!(justify_self: Some(taffy::style::JustifySelf::Baseline),),
449            "stretch" => quote!(justify_self: Some(taffy::style::JustifySelf::Stretch),),
450            _ => quote!(),
451        },
452        _ => quote!(),
453    };
454
455    let align_content = match style["alignContent"] {
456        Value::String(ref value) => match value.as_ref() {
457            "start" => quote!(align_content: Some(taffy::style::AlignContent::Start),),
458            "end" => quote!(align_content: Some(taffy::style::AlignContent::End),),
459            "flex-start" => quote!(align_content: Some(taffy::style::AlignContent::FlexStart),),
460            "flex-end" => quote!(align_content: Some(taffy::style::AlignContent::FlexEnd),),
461            "center" => quote!(align_content: Some(taffy::style::AlignContent::Center),),
462            "stretch" => quote!(align_content: Some(taffy::style::AlignContent::Stretch),),
463            "space-between" => quote!(align_content: Some(taffy::style::AlignContent::SpaceBetween),),
464            "space-around" => quote!(align_content: Some(taffy::style::AlignContent::SpaceAround),),
465            "space-evenly" => quote!(align_content: Some(taffy::style::AlignContent::SpaceEvenly),),
466            _ => quote!(),
467        },
468        _ => quote!(),
469    };
470
471    let justify_content = match style["justifyContent"] {
472        Value::String(ref value) => match value.as_ref() {
473            "start" => quote!(justify_content: Some(taffy::style::JustifyContent::Start),),
474            "end" => quote!(justify_content: Some(taffy::style::JustifyContent::End),),
475            "flex-start" => quote!(justify_content: Some(taffy::style::JustifyContent::FlexStart),),
476            "flex-end" => quote!(justify_content: Some(taffy::style::JustifyContent::FlexEnd),),
477            "center" => quote!(justify_content: Some(taffy::style::JustifyContent::Center),),
478            "stretch" => quote!(justify_content: Some(taffy::style::AlignContent::Stretch),),
479            "space-between" => quote!(justify_content: Some(taffy::style::JustifyContent::SpaceBetween),),
480            "space-around" => quote!(justify_content: Some(taffy::style::JustifyContent::SpaceAround),),
481            "space-evenly" => quote!(justify_content: Some(taffy::style::JustifyContent::SpaceEvenly),),
482            _ => quote!(),
483        },
484        _ => quote!(),
485    };
486
487    let flex_grow = quote_number_prop("flex_grow", style, |value: f32| quote!(#value));
488    let flex_shrink = quote_number_prop("flex_shrink", style, |value: f32| quote!(#value));
489
490    let flex_basis = quote_object_prop("flex_basis", style, generate_dimension);
491    let size = quote_object_prop("size", style, generate_size);
492    let min_size = quote_object_prop("min_size", style, generate_size);
493    let max_size = quote_object_prop("max_size", style, generate_size);
494    let aspect_ratio = quote_number_prop("aspect_ratio", style, |value: f32| quote!(Some(#value)));
495
496    let gap = quote_object_prop("gap", style, generate_gap);
497
498    let grid_template_rows = quote_array_prop("grid_template_rows", style, generate_track_definition_list);
499    let grid_template_columns = quote_array_prop("grid_template_columns", style, generate_track_definition_list);
500    let grid_auto_rows = quote_array_prop("grid_auto_rows", style, generate_track_definition_list);
501    let grid_auto_columns = quote_array_prop("grid_auto_columns", style, generate_track_definition_list);
502    let grid_auto_flow = quote_object_prop("grid_auto_flow", style, generate_grid_auto_flow);
503
504    let default_grid_placement = quote!(taffy::style::GridPlacement::Auto);
505
506    let grid_row_start = quote_object_value("grid_row_start", style, generate_grid_position);
507    let grid_row_end = quote_object_value("grid_row_end", style, generate_grid_position);
508    let grid_row = if grid_row_start.is_some() || grid_row_end.is_some() {
509        quote_prop(
510            "grid_row",
511            generate_line(
512                grid_row_start.unwrap_or(default_grid_placement.clone()),
513                grid_row_end.unwrap_or(default_grid_placement.clone()),
514            ),
515        )
516    } else {
517        quote!()
518    };
519
520    let grid_column_start = quote_object_value("grid_column_start", style, generate_grid_position);
521    let grid_column_end = quote_object_value("grid_column_end", style, generate_grid_position);
522    let grid_column = if grid_column_start.is_some() || grid_column_end.is_some() {
523        quote_prop(
524            "grid_column",
525            generate_line(
526                grid_column_start.unwrap_or(default_grid_placement.clone()),
527                grid_column_end.unwrap_or(default_grid_placement),
528            ),
529        )
530    } else {
531        quote!()
532    };
533
534    let text_content = get_string_value("text_content", node);
535    let writing_mode = get_string_value("writingMode", style);
536    let raw_aspect_ratio = get_number_value("aspect_ratio", style);
537    let measure_func: Option<_> =
538        text_content.map(|text| generate_measure_function(text, writing_mode, raw_aspect_ratio));
539
540    edges_quoted!(style, margin, generate_length_percentage_auto, quote!(zero()));
541    edges_quoted!(style, padding, generate_length_percentage, quote!(zero()));
542    edges_quoted!(style, border, generate_length_percentage, quote!(zero()));
543    edges_quoted!(style, inset, generate_length_percentage_auto, quote!(auto()));
544
545    // Quote children
546    let child_descriptions: Vec<Value> = match node["children"] {
547        Value::Array(ref value) => value.clone(),
548        _ => vec![],
549    };
550    let has_children = !child_descriptions.is_empty();
551    let (children_body, children) = if has_children {
552        let body = child_descriptions
553            .iter()
554            .enumerate()
555            .map(|(i, child)| generate_node(&format!("{ident}{i}"), child))
556            .collect();
557        let idents = child_descriptions
558            .iter()
559            .enumerate()
560            .map(|(i, _)| Ident::new(&format!("{ident}{i}"), Span::call_site()))
561            .collect::<Vec<_>>();
562        (body, quote!(&[#(#idents),*]))
563    } else {
564        (quote!(), quote!())
565    };
566
567    let ident = Ident::new(ident, Span::call_site());
568
569    let style = quote!(taffy::style::Style {
570        #display
571        #direction
572        #position
573        #flex_direction
574        #flex_wrap
575        #overflow
576        #scrollbar_width
577        #align_items
578        #align_self
579        #justify_items
580        #justify_self
581        #align_content
582        #justify_content
583        #flex_grow
584        #flex_shrink
585        #flex_basis
586        #gap
587        #grid_template_rows
588        #grid_template_columns
589        #grid_auto_rows
590        #grid_auto_columns
591        #grid_auto_flow
592        #grid_row
593        #grid_column
594        #size
595        #min_size
596        #max_size
597        #aspect_ratio
598        #margin
599        #padding
600        #inset
601        #border
602        ..Default::default()
603    });
604
605    if has_children {
606        quote!(
607            #children_body
608            let #ident = taffy.new_with_children(#style,#children).unwrap();
609        )
610    } else if measure_func.is_some() {
611        quote!(let #ident = taffy.new_leaf_with_measure(#style,#measure_func,).unwrap();)
612    } else {
613        quote!(let #ident = taffy.new_leaf(#style).unwrap();)
614    }
615}
616
617fn generate_size(size: &serde_json::Map<String, Value>) -> TokenStream {
618    dim_quoted!(size, width, generate_dimension, quote!(auto()));
619    dim_quoted!(size, height, generate_dimension, quote!(auto()));
620    quote!(
621        taffy::geometry::Size {
622            #width #height
623        }
624    )
625}
626
627fn generate_gap(size: &serde_json::Map<String, Value>) -> TokenStream {
628    dim_quoted_renamed!(size, column, width, generate_length_percentage, quote!(zero()));
629    dim_quoted_renamed!(size, row, height, generate_length_percentage, quote!(zero()));
630    quote!(
631        taffy::geometry::Size {
632            #width #height
633        }
634    )
635}
636
637fn generate_length_percentage(dimen: &serde_json::Map<String, Value>) -> TokenStream {
638    let unit = dimen.get("unit").unwrap();
639    let value = || dimen.get("value").unwrap().as_f64().unwrap() as f32;
640
641    match unit {
642        Value::String(ref unit) => match unit.as_ref() {
643            "px" => {
644                let value = value();
645                quote!(taffy::style::LengthPercentage::Length(#value))
646            }
647            "percent" => {
648                let value = value();
649                quote!(taffy::style::LengthPercentage::Percent(#value))
650            }
651            _ => unreachable!(),
652        },
653        _ => unreachable!(),
654    }
655}
656
657fn generate_length_percentage_auto(dimen: &serde_json::Map<String, Value>) -> TokenStream {
658    let unit = dimen.get("unit").unwrap();
659    let value = || dimen.get("value").unwrap().as_f64().unwrap() as f32;
660
661    match unit {
662        Value::String(ref unit) => match unit.as_ref() {
663            "auto" => quote!(taffy::style::LengthPercentageAuto::Auto),
664            "px" => {
665                let value = value();
666                quote!(taffy::style::LengthPercentageAuto::Length(#value))
667            }
668            "percent" => {
669                let value = value();
670                quote!(taffy::style::LengthPercentageAuto::Percent(#value))
671            }
672            _ => unreachable!(),
673        },
674        _ => unreachable!(),
675    }
676}
677
678fn generate_dimension(dimen: &serde_json::Map<String, Value>) -> TokenStream {
679    let unit = dimen.get("unit").unwrap();
680    let value = || dimen.get("value").unwrap().as_f64().unwrap() as f32;
681
682    match unit {
683        Value::String(ref unit) => match unit.as_ref() {
684            "auto" => quote!(taffy::style::Dimension::Auto),
685            "px" => {
686                let value = value();
687                quote!(taffy::style::Dimension::Length(#value))
688            }
689            "percent" => {
690                let value = value();
691                quote!(taffy::style::Dimension::Percent(#value))
692            }
693            _ => unreachable!(),
694        },
695        _ => unreachable!(),
696    }
697}
698
699fn generate_grid_auto_flow(auto_flow: &serde_json::Map<String, Value>) -> TokenStream {
700    let direction = auto_flow.get("direction").unwrap().as_str().unwrap();
701    let algorithm = auto_flow.get("algorithm").unwrap().as_str().unwrap();
702
703    match (direction, algorithm) {
704        ("row", "sparse") => quote!(taffy::style::GridAutoFlow::Row),
705        ("column", "sparse") => quote!(taffy::style::GridAutoFlow::Column),
706        ("row", "dense") => quote!(taffy::style::GridAutoFlow::RowDense),
707        ("column", "dense") => quote!(taffy::style::GridAutoFlow::ColumnDense),
708        _ => unreachable!(),
709    }
710}
711
712fn generate_line(start: TokenStream, end: TokenStream) -> TokenStream {
713    quote!(taffy::geometry::Line { start:#start, end:#end })
714}
715
716fn generate_grid_position(grid_position: &serde_json::Map<String, Value>) -> TokenStream {
717    let kind = grid_position.get("kind").unwrap();
718    let value = || grid_position.get("value").unwrap().as_f64().unwrap() as f32;
719
720    match kind {
721        Value::String(ref kind) => match kind.as_ref() {
722            "auto" => quote!(taffy::style::GridPlacement::Auto),
723            "span" => {
724                let value = value() as u16;
725                quote!(taffy::style::GridPlacement::Span(#value))
726            }
727            "line" => {
728                let value = value() as i16;
729                quote!(line(#value))
730            }
731            _ => unreachable!(),
732        },
733        _ => unreachable!(),
734    }
735}
736
737fn generate_track_definition_list(raw_list: &[Value]) -> TokenStream {
738    let list = raw_list.iter().map(|obj| match obj {
739        Value::Object(inner) => generate_track_definition(inner),
740        _ => unreachable!(),
741    });
742
743    quote!(vec![#(#list),*])
744}
745
746fn generate_track_definition(track_definition: &serde_json::Map<String, Value>) -> TokenStream {
747    let kind = track_definition.get("kind").unwrap().as_str().unwrap();
748    let name = || track_definition.get("name").unwrap().as_str().unwrap();
749    let arguments = || track_definition.get("arguments").unwrap();
750
751    match kind {
752        "scalar" => generate_scalar_definition(track_definition),
753        "function" => match (name(), arguments()) {
754            ("fit-content", Value::Array(arguments)) => {
755                if arguments.len() != 1 {
756                    panic!("fit-content function with the wrong number of arguments");
757                }
758                let argument = match arguments[0] {
759                    Value::Object(ref arg) => generate_scalar_definition(arg),
760                    _ => unreachable!(),
761                };
762                quote!(fit_content(#argument))
763            }
764            ("minmax", Value::Array(arguments)) => {
765                if arguments.len() != 2 {
766                    panic!("minmax function with the wrong number of arguments");
767                }
768                let min = match arguments[0] {
769                    Value::Object(ref arg) => generate_scalar_definition(arg),
770                    _ => unreachable!(),
771                };
772                let max = match arguments[1] {
773                    Value::Object(ref arg) => generate_scalar_definition(arg),
774                    _ => unreachable!(),
775                };
776                quote!(minmax(#min, #max))
777            }
778            ("repeat", Value::Array(arguments)) => {
779                if arguments.len() < 2 {
780                    panic!("repeat function with the wrong number of arguments");
781                }
782                let repetition = match arguments[0] {
783                    Value::Object(ref arg) => {
784                        let unit = arg.get("unit").unwrap().as_str().unwrap();
785                        let value = || arg.get("value").unwrap().as_u64().unwrap() as u16;
786
787                        match unit {
788                            "auto-fill" => quote!(GridTrackRepetition::AutoFill),
789                            "auto-fit" => quote!(GridTrackRepetition::AutoFit),
790                            "integer" => {
791                                let repetition_count = value();
792                                quote!(GridTrackRepetition::Count(#repetition_count))
793                            }
794                            _ => unreachable!(),
795                        }
796                    }
797                    _ => unreachable!(),
798                };
799                let track_list = generate_track_definition_list(&arguments[1..]);
800                quote!(repeat(#repetition, #track_list))
801            }
802            // TODO: Add support for fit-content
803            _ => unreachable!(),
804        },
805        _ => unreachable!(),
806    }
807}
808
809fn generate_scalar_definition(track_definition: &serde_json::Map<String, Value>) -> TokenStream {
810    let unit = || track_definition.get("unit").unwrap().as_str().unwrap();
811    let value = || track_definition.get("value").unwrap().as_f64().unwrap() as f32;
812
813    match unit() {
814        "auto" => quote!(auto()),
815        "min-content" => quote!(min_content()),
816        "max-content" => quote!(max_content()),
817        "px" | "percent" | "fraction" => {
818            let value = value();
819            match unit() {
820                "px" => quote!(length(#value)),
821                "percent" => quote!(percent(#value)),
822                "fraction" => quote!(fr(#value)),
823                _ => unreachable!(),
824            }
825        }
826        _ => unreachable!(),
827    }
828}
829
830fn generate_generic_measure_function() -> TokenStream {
831    quote!(
832        // WARNING: This enum is generated by the gentest script. Do not edit directly
833        #[allow(dead_code)]
834        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
835        enum WritingMode {
836            Horizontal,
837            Vertical,
838        }
839
840        // WARNING: This function is generated by the gentest script. Do not edit directly
841        #[allow(dead_code)]
842        fn measure_standard_text(
843            known_dimensions: taffy::geometry::Size<Option<f32>>,
844            available_space: taffy::geometry::Size<taffy::style::AvailableSpace>,
845            text_content: &str,
846            writing_mode: WritingMode,
847            _aspect_ratio: Option<f32>,
848        ) -> taffy::geometry::Size<f32> {
849            use taffy::geometry::AbsoluteAxis;
850            use taffy::prelude::*;
851            const ZWS: char = '\u{200B}';
852            const H_WIDTH: f32 = 10.0;
853            const H_HEIGHT: f32 = 10.0;
854
855            if let Size { width: Some(width), height: Some(height) } = known_dimensions {
856                return Size { width, height };
857            }
858
859            let inline_axis = match writing_mode {
860                WritingMode::Horizontal => AbsoluteAxis::Horizontal,
861                WritingMode::Vertical => AbsoluteAxis::Vertical,
862            };
863            let block_axis = inline_axis.other_axis();
864
865            let lines: Vec<&str> = text_content.split(ZWS).collect();
866            if lines.is_empty() {
867                return Size::ZERO;
868            }
869
870            let min_line_length: usize = lines.iter().map(|line| line.len()).max().unwrap_or(0);
871            let max_line_length: usize = lines.iter().map(|line| line.len()).sum();
872            let inline_size =
873                known_dimensions.get_abs(inline_axis).unwrap_or_else(|| match available_space.get_abs(inline_axis) {
874                    AvailableSpace::MinContent => min_line_length as f32 * H_WIDTH,
875                    AvailableSpace::MaxContent => max_line_length as f32 * H_WIDTH,
876                    AvailableSpace::Definite(inline_size) => {
877                        inline_size.min(max_line_length as f32 * H_WIDTH).max(min_line_length as f32 * H_WIDTH)
878                    }
879                });
880            let block_size = known_dimensions.get_abs(block_axis).unwrap_or_else(|| {
881                let inline_line_length = (inline_size / H_WIDTH).floor() as usize;
882                let mut line_count = 1;
883                let mut current_line_length = 0;
884                for line in &lines {
885                    if current_line_length + line.len() > inline_line_length {
886                        if current_line_length > 0 {
887                            line_count += 1
888                        };
889                        current_line_length = line.len();
890                    } else {
891                        current_line_length += line.len();
892                    };
893                }
894                (line_count as f32) * H_HEIGHT
895            });
896
897            match writing_mode {
898                WritingMode::Horizontal => Size { width: inline_size, height: block_size },
899                WritingMode::Vertical => Size { width: block_size, height: inline_size },
900            }
901        }
902    )
903}
904
905fn generate_measure_function(text_content: &str, writing_mode: Option<&str>, aspect_ratio: Option<f32>) -> TokenStream {
906    let writing_mode_token = match writing_mode {
907        Some("vertical-rl" | "vertical-lr") => quote!(super::WritingMode::Vertical),
908        _ => quote!(super::WritingMode::Horizontal),
909    };
910
911    let aspect_ratio_token = match aspect_ratio {
912        Some(ratio) => quote!(Some(#ratio)),
913        None => quote!(None),
914    };
915
916    quote!(
917        taffy::tree::MeasureFunc::Raw(|known_dimensions, available_space| {
918            const TEXT : &str = #text_content;
919            super::measure_standard_text(known_dimensions, available_space, TEXT, #writing_mode_token, #aspect_ratio_token)
920        })
921    )
922}