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