gioser

tawasuyu/taffy

A high performance rust-powered UI layout library

HTML 5544Rust 88Markdown 11TOML 91926 Definitions

⎇ 3bcce12d282670bd9d88e07b85aa9bda278832b1 scripts/gentest/src/main.rs Rust · 945 lines · 38.2 KB History Who touched what raw

Add test_fixtures/_scratch dir to gitignore (#739) 3bcce12d2 · Nico Burns ·

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