gioser

tawasuyu/taffy

A high performance rust-powered UI layout library

HTML 5544Rust 88Markdown 11TOML 91926 Definiciones

⎇ a94b7ba043c5bd359e23f1fabc88aba00fa255c7 scripts/gentest/src/main.rs Rust · 962 líneas · 38.6 KB Historia Quién tocó qué crudo

Implement Floats (#866) 25759dad4 · 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::{Layout, prelude::*};
226            let mut taffy = crate::new_test_tree();
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 float = match style["cssFloat"] {
424        Value::String(ref value) => match value.as_ref() {
425            "left" => quote!(float: taffy::style::Float::Left,),
426            "right" => quote!(float: taffy::style::Float::Right,),
427            _ => quote!(),
428        },
429        _ => quote!(),
430    };
431
432    let clear = match style["clear"] {
433        Value::String(ref value) => match value.as_ref() {
434            "left" => quote!(clear: taffy::style::Clear::Left,),
435            "right" => quote!(clear: taffy::style::Clear::Right,),
436            "both" => quote!(clear: taffy::style::Clear::Right,),
437            _ => quote!(),
438        },
439        _ => quote!(),
440    };
441
442    let flex_direction = match style["flexDirection"] {
443        Value::String(ref value) => match value.as_ref() {
444            "row-reverse" => quote!(flex_direction: taffy::style::FlexDirection::RowReverse,),
445            "column" => quote!(flex_direction: taffy::style::FlexDirection::Column,),
446            "column-reverse" => quote!(flex_direction: taffy::style::FlexDirection::ColumnReverse,),
447            _ => quote!(),
448        },
449        _ => quote!(),
450    };
451
452    let flex_wrap = match style["flexWrap"] {
453        Value::String(ref value) => match value.as_ref() {
454            "wrap" => quote!(flex_wrap: taffy::style::FlexWrap::Wrap,),
455            "wrap-reverse" => quote!(flex_wrap: taffy::style::FlexWrap::WrapReverse,),
456            _ => quote!(),
457        },
458        _ => quote!(),
459    };
460
461    fn quote_overflow(overflow: &Value) -> Option<TokenStream> {
462        match overflow {
463            Value::String(ref value) => match value.as_ref() {
464                "hidden" => Some(quote!(taffy::style::Overflow::Hidden)),
465                "scroll" => Some(quote!(taffy::style::Overflow::Scroll)),
466                "auto" => Some(quote!(taffy::style::Overflow::Auto)),
467                _ => None,
468            },
469            _ => None,
470        }
471    }
472    let overflow_x = quote_overflow(&style["overflowX"]);
473    let overflow_y = quote_overflow(&style["overflowY"]);
474    let (overflow, scrollbar_width) = if overflow_x.is_some() || overflow_y.is_some() {
475        let overflow_x = overflow_x.unwrap_or(quote!(taffy::style::Overflow::Visible));
476        let overflow_y = overflow_y.unwrap_or(quote!(taffy::style::Overflow::Visible));
477        let overflow = quote!(overflow: taffy::geometry::Point { x: #overflow_x, y: #overflow_y },);
478        let scrollbar_width = quote_number_prop("scrollbar_width", style, |value: f32| quote!(#value));
479        (overflow, scrollbar_width)
480    } else {
481        (quote!(), quote!())
482    };
483
484    let text_align = match style["textAlign"] {
485        Value::String(ref value) => match value.as_ref() {
486            "-webkit-left" => quote!(text_align: taffy::style::TextAlign::LegacyLeft,),
487            "-webkit-right" => quote!(text_align: taffy::style::TextAlign::LegacyRight,),
488            "-webkit-center" => quote!(text_align: taffy::style::TextAlign::LegacyCenter,),
489            _ => quote!(),
490        },
491        _ => quote!(),
492    };
493
494    let align_items = match style["alignItems"] {
495        Value::String(ref value) => match value.as_ref() {
496            "start" => quote!(align_items: Some(taffy::style::AlignItems::Start),),
497            "end" => quote!(align_items: Some(taffy::style::AlignItems::End),),
498            "flex-start" => quote!(align_items: Some(taffy::style::AlignItems::FlexStart),),
499            "flex-end" => quote!(align_items: Some(taffy::style::AlignItems::FlexEnd),),
500            "center" => quote!(align_items: Some(taffy::style::AlignItems::Center),),
501            "baseline" => quote!(align_items: Some(taffy::style::AlignItems::Baseline),),
502            "stretch" => quote!(align_items: Some(taffy::style::AlignItems::Stretch),),
503            _ => quote!(),
504        },
505        _ => quote!(),
506    };
507
508    let align_self = match style["alignSelf"] {
509        Value::String(ref value) => match value.as_ref() {
510            "start" => quote!(align_self: Some(taffy::style::AlignSelf::Start),),
511            "end" => quote!(align_self: Some(taffy::style::AlignSelf::End),),
512            "flex-start" => quote!(align_self: Some(taffy::style::AlignSelf::FlexStart),),
513            "flex-end" => quote!(align_self: Some(taffy::style::AlignSelf::FlexEnd),),
514            "center" => quote!(align_self: Some(taffy::style::AlignSelf::Center),),
515            "baseline" => quote!(align_self: Some(taffy::style::AlignSelf::Baseline),),
516            "stretch" => quote!(align_self: Some(taffy::style::AlignSelf::Stretch),),
517            _ => quote!(),
518        },
519        _ => quote!(),
520    };
521
522    let justify_items = match style["justifyItems"] {
523        Value::String(ref value) => match value.as_ref() {
524            "start" => quote!(justify_items: Some(taffy::style::JustifyItems::Start),),
525            "end" => quote!(justify_items: Some(taffy::style::JustifyItems::End),),
526            "flex-start" => quote!(justify_items: Some(taffy::style::JustifyItems::FlexStart),),
527            "flex-end" => quote!(justify_items: Some(taffy::style::JustifyItems::FlexEnd),),
528            "center" => quote!(justify_items: Some(taffy::style::JustifyItems::Center),),
529            "baseline" => quote!(justify_items: Some(taffy::style::JustifyItems::Baseline),),
530            "stretch" => quote!(justify_items: Some(taffy::style::JustifyItems::Stretch),),
531            _ => quote!(),
532        },
533        _ => quote!(),
534    };
535
536    let justify_self = match style["justifySelf"] {
537        Value::String(ref value) => match value.as_ref() {
538            "start" => quote!(justify_self: Some(taffy::style::JustifySelf::Start),),
539            "end" => quote!(justify_self: Some(taffy::style::JustifySelf::End),),
540            "flex-start" => quote!(justify_self: Some(taffy::style::JustifySelf::FlexStart),),
541            "flex-end" => quote!(justify_self: Some(taffy::style::JustifySelf::FlexEnd),),
542            "center" => quote!(justify_self: Some(taffy::style::JustifySelf::Center),),
543            "baseline" => quote!(justify_self: Some(taffy::style::JustifySelf::Baseline),),
544            "stretch" => quote!(justify_self: Some(taffy::style::JustifySelf::Stretch),),
545            _ => quote!(),
546        },
547        _ => quote!(),
548    };
549
550    let align_content = match style["alignContent"] {
551        Value::String(ref value) => match value.as_ref() {
552            "start" => quote!(align_content: Some(taffy::style::AlignContent::Start),),
553            "end" => quote!(align_content: Some(taffy::style::AlignContent::End),),
554            "flex-start" => quote!(align_content: Some(taffy::style::AlignContent::FlexStart),),
555            "flex-end" => quote!(align_content: Some(taffy::style::AlignContent::FlexEnd),),
556            "center" => quote!(align_content: Some(taffy::style::AlignContent::Center),),
557            "stretch" => quote!(align_content: Some(taffy::style::AlignContent::Stretch),),
558            "space-between" => quote!(align_content: Some(taffy::style::AlignContent::SpaceBetween),),
559            "space-around" => quote!(align_content: Some(taffy::style::AlignContent::SpaceAround),),
560            "space-evenly" => quote!(align_content: Some(taffy::style::AlignContent::SpaceEvenly),),
561            _ => quote!(),
562        },
563        _ => quote!(),
564    };
565
566    let justify_content = match style["justifyContent"] {
567        Value::String(ref value) => match value.as_ref() {
568            "start" => quote!(justify_content: Some(taffy::style::JustifyContent::Start),),
569            "end" => quote!(justify_content: Some(taffy::style::JustifyContent::End),),
570            "flex-start" => quote!(justify_content: Some(taffy::style::JustifyContent::FlexStart),),
571            "flex-end" => quote!(justify_content: Some(taffy::style::JustifyContent::FlexEnd),),
572            "center" => quote!(justify_content: Some(taffy::style::JustifyContent::Center),),
573            "stretch" => quote!(justify_content: Some(taffy::style::AlignContent::Stretch),),
574            "space-between" => quote!(justify_content: Some(taffy::style::JustifyContent::SpaceBetween),),
575            "space-around" => quote!(justify_content: Some(taffy::style::JustifyContent::SpaceAround),),
576            "space-evenly" => quote!(justify_content: Some(taffy::style::JustifyContent::SpaceEvenly),),
577            _ => quote!(),
578        },
579        _ => quote!(),
580    };
581
582    let flex_grow = quote_number_prop("flex_grow", style, |value: f32| quote!(#value));
583    let flex_shrink = quote_number_prop("flex_shrink", style, |value: f32| quote!(#value));
584
585    let flex_basis = quote_object_prop("flex_basis", style, generate_dimension);
586    let size = quote_object_prop("size", style, generate_size);
587    let min_size = quote_object_prop("min_size", style, generate_size);
588    let max_size = quote_object_prop("max_size", style, generate_size);
589    let aspect_ratio = quote_number_prop("aspect_ratio", style, |value: f32| quote!(Some(#value)));
590
591    let gap = quote_object_prop("gap", style, generate_gap);
592
593    let grid_template_rows = quote_array_prop("grid_template_rows", style, generate_track_definition_list);
594    let grid_template_columns = quote_array_prop("grid_template_columns", style, generate_track_definition_list);
595    let grid_auto_rows = quote_array_prop("grid_auto_rows", style, generate_track_definition_list);
596    let grid_auto_columns = quote_array_prop("grid_auto_columns", style, generate_track_definition_list);
597    let grid_auto_flow = quote_object_prop("grid_auto_flow", style, generate_grid_auto_flow);
598
599    let default_grid_placement = quote!(taffy::style::GridPlacement::Auto);
600
601    let grid_row_start = quote_object_value("grid_row_start", style, generate_grid_position);
602    let grid_row_end = quote_object_value("grid_row_end", style, generate_grid_position);
603    let grid_row = if grid_row_start.is_some() || grid_row_end.is_some() {
604        quote_prop(
605            "grid_row",
606            generate_line(
607                grid_row_start.unwrap_or(default_grid_placement.clone()),
608                grid_row_end.unwrap_or(default_grid_placement.clone()),
609            ),
610        )
611    } else {
612        quote!()
613    };
614
615    let grid_column_start = quote_object_value("grid_column_start", style, generate_grid_position);
616    let grid_column_end = quote_object_value("grid_column_end", style, generate_grid_position);
617    let grid_column = if grid_column_start.is_some() || grid_column_end.is_some() {
618        quote_prop(
619            "grid_column",
620            generate_line(
621                grid_column_start.unwrap_or(default_grid_placement.clone()),
622                grid_column_end.unwrap_or(default_grid_placement),
623            ),
624        )
625    } else {
626        quote!()
627    };
628
629    let text_content = get_string_value("text_content", node);
630    let writing_mode = get_string_value("writingMode", style);
631    let raw_aspect_ratio = get_number_value("aspect_ratio", style);
632    let node_context: Option<_> = text_content.map(|text| generate_node_context(text, writing_mode, raw_aspect_ratio));
633
634    edges_quoted!(style, margin, generate_length_percentage_auto, quote!(zero()));
635    edges_quoted!(style, padding, generate_length_percentage, quote!(zero()));
636    edges_quoted!(style, border, generate_length_percentage, quote!(zero()));
637    edges_quoted!(style, inset, generate_length_percentage_auto, quote!(auto()));
638
639    // Quote children
640    let child_descriptions: Vec<Value> = match node["children"] {
641        Value::Array(ref value) => value.clone(),
642        _ => vec![],
643    };
644    let has_children = !child_descriptions.is_empty();
645    let (children_body, children) = if has_children {
646        let body = child_descriptions
647            .iter()
648            .enumerate()
649            .map(|(i, child)| generate_node(&format!("{ident}{i}"), child))
650            .collect();
651        let idents = child_descriptions
652            .iter()
653            .enumerate()
654            .map(|(i, _)| Ident::new(&format!("{ident}{i}"), Span::call_site()))
655            .collect::<Vec<_>>();
656        (body, quote!(&[#(#idents),*]))
657    } else {
658        (quote!(), quote!())
659    };
660
661    let ident = Ident::new(ident, Span::call_site());
662
663    let style = quote!(taffy::style::Style {
664        #display
665        #box_sizing
666        #direction
667        #position
668        #float
669        #clear
670        #text_align
671        #flex_direction
672        #flex_wrap
673        #overflow
674        #scrollbar_width
675        #align_items
676        #align_self
677        #justify_items
678        #justify_self
679        #align_content
680        #justify_content
681        #flex_grow
682        #flex_shrink
683        #flex_basis
684        #gap
685        #grid_template_rows
686        #grid_template_columns
687        #grid_auto_rows
688        #grid_auto_columns
689        #grid_auto_flow
690        #grid_row
691        #grid_column
692        #size
693        #min_size
694        #max_size
695        #aspect_ratio
696        #margin
697        #padding
698        #inset
699        #border
700        ..Default::default()
701    });
702
703    if has_children {
704        quote!(
705            #children_body
706            let #ident = taffy.new_with_children(#style,#children).unwrap();
707        )
708    } else if node_context.is_some() {
709        quote!(let #ident = taffy.new_leaf_with_context(#style,#node_context,).unwrap();)
710    } else {
711        quote!(let #ident = taffy.new_leaf(#style).unwrap();)
712    }
713}
714
715fn generate_size(size: &serde_json::Map<String, Value>) -> TokenStream {
716    dim_quoted!(size, width, generate_dimension, quote!(auto()));
717    dim_quoted!(size, height, generate_dimension, quote!(auto()));
718    quote!(
719        taffy::geometry::Size {
720            #width #height
721        }
722    )
723}
724
725fn generate_gap(size: &serde_json::Map<String, Value>) -> TokenStream {
726    dim_quoted_renamed!(size, column, width, generate_length_percentage, quote!(zero()));
727    dim_quoted_renamed!(size, row, height, generate_length_percentage, quote!(zero()));
728    quote!(
729        taffy::geometry::Size {
730            #width #height
731        }
732    )
733}
734
735fn generate_length_percentage(dimen: &serde_json::Map<String, Value>) -> TokenStream {
736    let unit = dimen.get("unit").unwrap();
737    let value = || dimen.get("value").unwrap().as_f64().unwrap() as f32;
738
739    match unit {
740        Value::String(ref unit) => match unit.as_ref() {
741            "px" => {
742                let value = value();
743                quote!(length(#value))
744            }
745            "percent" => {
746                let value = value();
747                quote!(percent(#value))
748            }
749            _ => unreachable!(),
750        },
751        _ => unreachable!(),
752    }
753}
754
755fn generate_length_percentage_auto(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::LengthPercentageAuto::AUTO),
762            "px" => {
763                let value = value();
764                quote!(length(#value))
765            }
766            "percent" => {
767                let value = value();
768                quote!(percent(#value))
769            }
770            _ => unreachable!(),
771        },
772        _ => unreachable!(),
773    }
774}
775
776fn generate_dimension(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            "auto" => quote!(taffy::style::Dimension::AUTO),
783            "px" => {
784                let value = value();
785                quote!(taffy::style::Dimension::from_length(#value))
786            }
787            "percent" => {
788                let value = value();
789                quote!(taffy::style::Dimension::from_percent(#value))
790            }
791            _ => unreachable!(),
792        },
793        _ => unreachable!(),
794    }
795}
796
797fn generate_available_space(dimen: &serde_json::Map<String, Value>) -> TokenStream {
798    let unit = dimen.get("unit").unwrap();
799    let value = || dimen.get("value").unwrap().as_f64().unwrap() as f32;
800
801    match unit {
802        Value::String(ref unit) => match unit.as_ref() {
803            "max-content" => quote!(taffy::style::AvailableSpace::MaxContent),
804            "min-content" => quote!(taffy::style::AvailableSpace::MaxContent),
805            "px" => {
806                let value = value();
807                quote!(taffy::style::AvailableSpace::Definite(#value))
808            }
809            _ => unreachable!(),
810        },
811        _ => unreachable!(),
812    }
813}
814
815fn generate_grid_auto_flow(auto_flow: &serde_json::Map<String, Value>) -> TokenStream {
816    let direction = auto_flow.get("direction").unwrap().as_str().unwrap();
817    let algorithm = auto_flow.get("algorithm").unwrap().as_str().unwrap();
818
819    match (direction, algorithm) {
820        ("row", "sparse") => quote!(taffy::style::GridAutoFlow::Row),
821        ("column", "sparse") => quote!(taffy::style::GridAutoFlow::Column),
822        ("row", "dense") => quote!(taffy::style::GridAutoFlow::RowDense),
823        ("column", "dense") => quote!(taffy::style::GridAutoFlow::ColumnDense),
824        _ => unreachable!(),
825    }
826}
827
828fn generate_line(start: TokenStream, end: TokenStream) -> TokenStream {
829    quote!(taffy::geometry::Line { start:#start, end:#end })
830}
831
832fn generate_grid_position(grid_position: &serde_json::Map<String, Value>) -> TokenStream {
833    let kind = grid_position.get("kind").unwrap();
834    let value = || grid_position.get("value").unwrap().as_f64().unwrap() as f32;
835
836    match kind {
837        Value::String(ref kind) => match kind.as_ref() {
838            "auto" => quote!(taffy::style::GridPlacement::Auto),
839            "span" => {
840                let value = value() as u16;
841                quote!(taffy::style::GridPlacement::Span(#value))
842            }
843            "line" => {
844                let value = value() as i16;
845                quote!(line(#value))
846            }
847            _ => unreachable!(),
848        },
849        _ => unreachable!(),
850    }
851}
852
853fn generate_track_definition_list(raw_list: &[Value]) -> TokenStream {
854    let list = raw_list.iter().map(|obj| match obj {
855        Value::Object(inner) => generate_track_definition(inner),
856        _ => unreachable!(),
857    });
858
859    quote!(vec![#(#list),*])
860}
861
862fn generate_track_definition(track_definition: &serde_json::Map<String, Value>) -> TokenStream {
863    let kind = track_definition.get("kind").unwrap().as_str().unwrap();
864    let name = || track_definition.get("name").unwrap().as_str().unwrap();
865    let arguments = || track_definition.get("arguments").unwrap();
866
867    match kind {
868        "scalar" => generate_scalar_definition(track_definition),
869        "function" => match (name(), arguments()) {
870            ("fit-content", Value::Array(arguments)) => {
871                if arguments.len() != 1 {
872                    panic!("fit-content function with the wrong number of arguments");
873                }
874                let argument = match arguments[0] {
875                    Value::Object(ref arg) => generate_scalar_definition(arg),
876                    _ => unreachable!(),
877                };
878                quote!(fit_content(#argument))
879            }
880            ("minmax", Value::Array(arguments)) => {
881                if arguments.len() != 2 {
882                    panic!("minmax function with the wrong number of arguments");
883                }
884                let min = match arguments[0] {
885                    Value::Object(ref arg) => generate_scalar_definition(arg),
886                    _ => unreachable!(),
887                };
888                let max = match arguments[1] {
889                    Value::Object(ref arg) => generate_scalar_definition(arg),
890                    _ => unreachable!(),
891                };
892                quote!(minmax(#min, #max))
893            }
894            ("repeat", Value::Array(arguments)) => {
895                if arguments.len() < 2 {
896                    panic!("repeat function with the wrong number of arguments");
897                }
898                let repetition = match arguments[0] {
899                    Value::Object(ref arg) => {
900                        let unit = arg.get("unit").unwrap().as_str().unwrap();
901                        let value = || arg.get("value").unwrap().as_u64().unwrap() as u16;
902
903                        match unit {
904                            "auto-fill" => quote!(RepetitionCount::AutoFill),
905                            "auto-fit" => quote!(RepetitionCount::AutoFit),
906                            "integer" => {
907                                let repetition_count = value();
908                                quote!(RepetitionCount::Count(#repetition_count))
909                            }
910                            _ => unreachable!(),
911                        }
912                    }
913                    _ => unreachable!(),
914                };
915                let track_list = generate_track_definition_list(&arguments[1..]);
916                quote!(repeat(#repetition, #track_list))
917            }
918            // TODO: Add support for fit-content
919            _ => unreachable!(),
920        },
921        _ => unreachable!(),
922    }
923}
924
925fn generate_scalar_definition(track_definition: &serde_json::Map<String, Value>) -> TokenStream {
926    let unit = || track_definition.get("unit").unwrap().as_str().unwrap();
927    let value = || track_definition.get("value").unwrap().as_f64().unwrap() as f32;
928
929    match unit() {
930        "auto" => quote!(auto()),
931        "min-content" => quote!(min_content()),
932        "max-content" => quote!(max_content()),
933        "px" | "percent" | "fraction" => {
934            let value = value();
935            match unit() {
936                "px" => quote!(length(#value)),
937                "percent" => quote!(percent(#value)),
938                "fraction" => quote!(fr(#value)),
939                _ => unreachable!(),
940            }
941        }
942        _ => unreachable!(),
943    }
944}
945
946fn generate_node_context(text_content: &str, writing_mode: Option<&str>, aspect_ratio: Option<f32>) -> TokenStream {
947    let trimmed_text_content = text_content.trim();
948
949    let writing_mode_token = match writing_mode {
950        Some("vertical-rl" | "vertical-lr") => quote!(crate::WritingMode::Vertical),
951        _ => quote!(crate::WritingMode::Horizontal),
952    };
953
954    let _aspect_ratio_token = match aspect_ratio {
955        Some(ratio) => quote!(Some(#ratio)),
956        None => quote!(None),
957    };
958
959    quote!(
960        crate::TestNodeContext::ahem_text(#trimmed_text_content, #writing_mode_token)
961    )
962}