gioser

tawasuyu/taffy

A high performance rust-powered UI layout library

HTML 231Rust 8TOML 3CSS 1317 Definitions

⎇ 0.5.x scripts/gentest/src/main.rs Rust · 903 lines · 36.9 KB History Who touched what raw

Rename Taffy struct to TaffyTree (#580) 17235ffa2 · Nico Burns ·

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