Add test_fixtures/_scratch dir to gitignore (#739)
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}