1use std::collections::HashMap;
2
3use crate::parser::{self, AttrValue, Document, Element, Node};
4use crate::semantic::PropField;
5
6pub fn generate(doc: &Document) -> String {
7 generate_with_id(doc, 0)
8}
9
10pub fn generate_with_id(doc: &Document, file_id: usize) -> String {
11 generate_with_imports(doc, file_id, &[], None, None, None, &[], None)
12}
13
14pub type ResolvedImport<'a> = (&'a str, &'a str);
18
19pub fn generate_with_imports(
24 doc: &Document,
25 file_id: usize,
26 imports: &[ResolvedImport],
27 component_name: Option<&str>,
28 resolved_state_type: Option<&str>,
29 props_type: Option<&str>,
30 import_props: &[(&str, Option<&str>)],
31 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
32) -> String {
33 let mut out = String::new();
34 let state_type = resolved_state_type.or(doc.state_type.as_deref());
35 let has_state = state_type.is_some();
36
37 if let Some(ref fm) = doc.frontmatter {
38 out.push_str(fm);
39 out.push('\n');
40 }
41
42 let commands = parser::collect_commands(&doc.root);
44 if !commands.is_empty() {
45 out.push_str("#[allow(unused)]\n");
46 out.push_str(&format!("fn _fncc_validate_{file_id}() {{\n"));
47 for cmd in &commands {
48 let trampoline = format!("__fncc_cmd_{cmd}");
49 out.push_str(&format!(" let _ = {trampoline};\n"));
50 }
51 out.push_str("}\n\n");
52 }
53
54 if has_state {
55 generate_stateful(doc, &mut out, imports, state_type, import_props, prop_fields);
56 } else {
57 generate_stateless(
58 doc,
59 &mut out,
60 imports,
61 component_name,
62 props_type,
63 import_props,
64 prop_fields,
65 );
66 }
67
68 out
69}
70
71fn generate_stateless(
72 doc: &Document,
73 out: &mut String,
74 imports: &[ResolvedImport],
75 component_name: Option<&str>,
76 props_type: Option<&str>,
77 import_props: &[(&str, Option<&str>)],
78 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
79) {
80 let name = component_name.unwrap_or(&doc.root.name);
81 let fn_name = format!("render_{}", to_snake_case(name));
82 if let Some(pt) = props_type {
83 out.push_str(&format!("pub fn {fn_name}(props: &{pt}) -> impl IntoElement {{\n"));
84 } else {
85 out.push_str(&format!("pub fn {fn_name}() -> impl IntoElement {{\n"));
86 }
87 out.push_str(&generate_element(
88 &doc.root,
89 1,
90 false,
91 imports,
92 import_props,
93 prop_fields,
94 ));
95 out.push('\n');
96 out.push_str("}\n");
97}
98
99fn generate_stateful(
100 doc: &Document,
101 out: &mut String,
102 imports: &[ResolvedImport],
103 state_type: Option<&str>,
104 import_props: &[(&str, Option<&str>)],
105 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
106) {
107 let state_type = state_type.unwrap_or("Self");
108
109 out.push_str(&format!("impl Render for {state_type} {{\n"));
110 out.push_str(" fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {\n");
111 out.push_str(" let handle = cx.entity().downgrade();\n");
112 out.push_str(&generate_element(
113 &doc.root,
114 2,
115 true,
116 imports,
117 import_props,
118 prop_fields,
119 ));
120 out.push_str("\n }\n");
121 out.push_str("}\n");
122}
123
124fn generate_element(
125 el: &Element,
126 depth: usize,
127 stateful: bool,
128 imports: &[ResolvedImport],
129 import_props: &[(&str, Option<&str>)],
130 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
131) -> String {
132 let indent = " ".repeat(depth);
133
134 match el.name.as_str() {
136 "Stack" => return gen_stack(el, &indent, depth, stateful, imports, import_props, prop_fields),
137 "Text" => return gen_text(el, &indent, depth, stateful, imports, import_props, prop_fields),
138 "Button" => return gen_button(el, &indent, depth, stateful, imports, import_props, prop_fields),
139 _ => {}
140 }
141
142 if let Some(render_fn) = imports
144 .iter()
145 .find(|(name, fn_name)| name == &el.name && !fn_name.is_empty())
146 .map(|(_, fn_name)| *fn_name)
147 {
148 let props_type = import_props.iter().find(|(n, _)| n == &el.name).and_then(|(_, p)| *p);
149 return if let Some(pt) = props_type {
150 let mut struct_fields = String::new();
151 if let Some(fields) = prop_fields.and_then(|m| m.get(pt)) {
152 for f in fields {
153 if let Some((_, attr_val)) = el.attrs.iter().find(|(n, _)| n == &f.name) {
154 let v = match attr_val {
155 AttrValue::String(s) => format!("{:?}.into()", s),
156 AttrValue::Interpolation(expr) => {
157 format!("{}.into()", interpolation_expr(expr))
158 }
159 };
160 struct_fields.push_str(&format!("\n{indent} {}: {},", f.name, v));
161 } else if f.is_optional {
162 struct_fields.push_str(&format!("\n{indent} {}: None,", f.name));
163 }
164 }
165 } else {
166 for (attr_name, attr_val) in &el.attrs {
167 let v = match attr_val {
168 AttrValue::String(s) => format!("{:?}.into()", s),
169 AttrValue::Interpolation(expr) => {
170 format!("{}.into()", interpolation_expr(expr))
171 }
172 };
173 struct_fields.push_str(&format!("\n{indent} {attr_name}: {v},"));
174 }
175 }
176 format!("{indent}{render_fn}(&{pt} {{ {struct_fields}\n{indent} }})")
177 } else {
178 format!("{indent}{render_fn}()")
179 };
180 }
181
182 gen_fallback(el, &indent, depth, stateful, imports, import_props, prop_fields)
184}
185
186fn gen_stack(
187 el: &Element,
188 indent: &str,
189 depth: usize,
190 stateful: bool,
191 imports: &[ResolvedImport],
192 import_props: &[(&str, Option<&str>)],
193 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
194) -> String {
195 let mut out = format!("{indent}div()\n");
196
197 let mut is_vertical = false;
198 for (key, val) in &el.attrs {
199 match key.as_str() {
200 "direction" if val.as_str() == "vertical" => is_vertical = true,
201 "gap" => {
202 let v = val.as_str();
203 if let Ok(n) = v.parse::<f64>() {
204 out.push_str(&format!("{indent} .gap(px({n}.))\n"));
205 }
206 }
207 _ => {}
208 }
209 }
210
211 if is_vertical {
212 out.push_str(&format!("{indent} .flex()\n{indent} .flex_col()\n"));
213 } else {
214 out.push_str(&format!("{indent} .flex()\n"));
215 }
216
217 for child in &el.children {
218 out.push_str(&format!("{indent} .child(\n"));
219 match child {
220 Node::Element(child_el) => {
221 out.push_str(&generate_element(
222 child_el,
223 depth + 2,
224 stateful,
225 imports,
226 import_props,
227 prop_fields,
228 ));
229 }
230 Node::Text(t) => {
231 out.push_str(&format!("{indent} \"{t}\""));
232 }
233 Node::Interpolation(expr) => {
234 let e = interpolation_expr(expr);
235 out.push_str(&format!("{indent} {e}.to_string()"));
236 }
237 }
238 out.push('\n');
239 out.push_str(&format!("{indent} )\n"));
240 }
241
242 out.trim_end().to_string()
243}
244
245fn gen_text(
246 el: &Element,
247 indent: &str,
248 depth: usize,
249 stateful: bool,
250 imports: &[ResolvedImport],
251 import_props: &[(&str, Option<&str>)],
252 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
253) -> String {
254 let mut out = format!("{indent}div()\n");
255
256 for (key, val) in &el.attrs {
257 if key.as_str() == "size" {
258 let v = val.as_str();
259 let ts = match v {
260 "xs" => "text_xs()",
261 "sm" => "text_sm()",
262 "base" => "text_base()",
263 "lg" => "text_lg()",
264 "xl" => "text_xl()",
265 "2xl" | "xxl" => "text_2xl()",
266 "3xl" => "text_3xl()",
267 _ => "text_base()",
268 };
269 out.push_str(&format!("{indent} .{ts}\n"));
270 }
271 }
272
273 match &el.children[..] {
274 [Node::Text(t)] => {
275 out.push_str(&format!("{indent} .child(\"{t}\")"));
276 }
277 [Node::Interpolation(expr)] => {
278 let e = interpolation_expr(expr);
279 out.push_str(&format!("{indent} .child({e}.to_string())"));
280 }
281 children => {
282 for child in children {
283 match child {
284 Node::Text(t) => out.push_str(&format!("{indent} .child(\"{t}\")\n")),
285 Node::Interpolation(expr) => {
286 let e = interpolation_expr(expr);
287 out.push_str(&format!("{indent} .child({e}.to_string())\n"));
288 }
289 Node::Element(child_el) => {
290 out.push_str(&format!("{indent} .child(\n"));
291 out.push_str(&generate_element(
292 child_el,
293 depth + 1,
294 stateful,
295 imports,
296 import_props,
297 prop_fields,
298 ));
299 out.push_str(&format!("\n{indent} )\n"));
300 }
301 }
302 }
303 }
304 }
305
306 out.trim_end().to_string()
307}
308
309fn gen_button(
310 el: &Element,
311 indent: &str,
312 depth: usize,
313 stateful: bool,
314 imports: &[ResolvedImport],
315 import_props: &[(&str, Option<&str>)],
316 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
317) -> String {
318 let mut out = format!("{indent}div()\n");
319
320 let btn_id = match &el.children[..] {
321 [Node::Text(t)] => t.clone(),
322 _ => format!("button_{depth}"),
323 };
324 out.push_str(&format!("{indent} .id(\"{btn_id}\")\n"));
325 out.push_str(&format!("{indent} .cursor_pointer()\n"));
326
327 for (key, val) in &el.attrs {
328 if key.as_str() == "onclick" {
329 let handler = val.as_str();
330 let trampoline = format!("__fncc_cmd_{handler}");
331 if stateful {
332 out.push_str(&format!("{indent} .on_click({{\n"));
334 out.push_str(&format!("{indent} let handle = handle.clone();\n"));
335 out.push_str(&format!("{indent} move |_, _, cx| {{\n"));
336 out.push_str(&format!("{indent} handle.update(cx, |this, cx| {{\n"));
337 out.push_str(&format!("{indent} {trampoline}(this, cx);\n"));
338 out.push_str(&format!("{indent} }}).ok();\n"));
339 out.push_str(&format!("{indent} }}\n"));
340 out.push_str(&format!("{indent} }})\n"));
341 } else {
342 out.push_str(&format!("{indent} .on_click({trampoline})\n"));
343 }
344 }
345 }
346
347 match &el.children[..] {
348 [Node::Text(t)] => out.push_str(&format!("{indent} .child(\"{t}\")")),
349 [Node::Interpolation(expr)] => {
350 let e = interpolation_expr(expr);
351 out.push_str(&format!("{indent} .child({e}.to_string())"));
352 }
353 children => {
354 for child in children {
355 match child {
356 Node::Text(t) => out.push_str(&format!("{indent} .child(\"{t}\")\n")),
357 Node::Element(child_el) => {
358 out.push_str(&format!("{indent} .child(\n"));
359 out.push_str(&generate_element(
360 child_el,
361 depth + 1,
362 stateful,
363 imports,
364 import_props,
365 prop_fields,
366 ));
367 out.push_str(&format!("\n{indent} )\n"));
368 }
369 Node::Interpolation(expr) => {
370 let e = interpolation_expr(expr);
371 out.push_str(&format!("{indent} .child({e}.to_string())\n"));
372 }
373 }
374 }
375 }
376 }
377
378 out.trim_end().to_string()
379}
380
381fn gen_fallback(
382 el: &Element,
383 indent: &str,
384 depth: usize,
385 stateful: bool,
386 imports: &[ResolvedImport],
387 import_props: &[(&str, Option<&str>)],
388 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
389) -> String {
390 let mut out = format!("{indent}div()\n");
391 for (key, val) in &el.attrs {
392 let v = val.as_str();
393 out.push_str(&format!("{indent} .attr(\"{key}\", \"{v}\")\n"));
394 }
395 for child in &el.children {
396 out.push_str(&format!("{indent} .child(\n"));
397 match child {
398 Node::Element(child_el) => {
399 out.push_str(&generate_element(
400 child_el,
401 depth + 2,
402 stateful,
403 imports,
404 import_props,
405 prop_fields,
406 ));
407 }
408 Node::Text(t) => out.push_str(&format!("{indent} \"{t}\"")),
409 Node::Interpolation(expr) => {
410 let e = interpolation_expr(expr);
411 out.push_str(&format!("{indent} {e}.to_string()"));
412 }
413 }
414 out.push('\n');
415 out.push_str(&format!("{indent} )\n"));
416 }
417 out.trim_end().to_string()
418}
419
420fn interpolation_expr(expr: &str) -> String {
425 let trimmed = expr.trim();
426 if let Some(field) = trimmed.strip_prefix("state.") {
427 format!("self.{field}")
428 } else if trimmed.starts_with("props.") {
429 trimmed.to_string()
430 } else {
431 format!("self.{trimmed}")
432 }
433}
434
435pub(crate) fn to_snake_case(name: &str) -> String {
436 let mut result = String::new();
437 for (i, ch) in name.chars().enumerate() {
438 if ch.is_uppercase() {
439 if i > 0 {
440 result.push('_');
441 }
442 for c in ch.to_lowercase() {
443 result.push(c);
444 }
445 } else {
446 result.push(ch);
447 }
448 }
449 result
450}
451
452impl AttrValue {
453 fn as_str(&self) -> &str {
454 match self {
455 AttrValue::String(s) => s,
456 AttrValue::Interpolation(s) => s,
457 }
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use crate::parser::parse;
465
466 fn generate_from(source: &str) -> String {
467 let doc = parse(source).unwrap();
468 generate(&doc)
469 }
470
471 #[test]
474 fn test_generates_stateless_render_function() {
475 let out = generate_from("<MyComp></MyComp>");
476 assert!(out.contains("pub fn render_my_comp() -> impl IntoElement {"));
477 assert!(out.contains("div()"));
478 }
479
480 #[test]
481 fn test_generates_stateful_render_impl() {
482 let src = "---\n@state CounterState\n---\n<App></App>";
483 let out = generate_from(src);
484 assert!(out.contains("impl Render for CounterState {"));
485 assert!(
486 out.contains("fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {")
487 );
488 }
489
490 #[test]
491 fn test_frontmatter_is_preserved_in_output() {
492 let src = "---\nuse crate::prelude::*;\n---\n<App></App>";
493 let out = generate_from(src);
494 assert!(out.contains("use crate::prelude::*;"));
495 }
496
497 #[test]
498 fn test_stack_with_direction_vertical_generates_flex_col() {
499 let out = generate_from("<Stack direction=\"vertical\"></Stack>");
500 assert!(out.contains(".flex()"));
501 assert!(out.contains(".flex_col()"));
502 }
503
504 #[test]
505 fn test_stack_with_direction_horizontal_generates_flex_only() {
506 let out = generate_from("<Stack direction=\"horizontal\"></Stack>");
507 assert!(out.contains(".flex()"));
508 assert!(!out.contains(".flex_col()"));
509 }
510
511 #[test]
512 fn test_stack_with_gap_generates_px_value() {
513 let out = generate_from("<Stack gap=\"12\"></Stack>");
514 assert!(out.contains(".gap(px(12.))"));
515 }
516
517 #[test]
518 fn test_text_with_size_xl() {
519 let out = generate_from("<Text size=\"xl\">hello</Text>");
520 assert!(out.contains(".text_xl()"));
521 assert!(out.contains(".child(\"hello\")"));
522 }
523
524 #[test]
525 fn test_text_with_size_unknown_falls_back_to_base() {
526 let out = generate_from("<Text size=\"huge\">text</Text>");
527 assert!(out.contains(".text_base()"));
528 }
529
530 #[test]
531 fn test_button_with_text_child() {
532 let out = generate_from("<Button onclick=\"handle_click\">Click</Button>");
533 assert!(out.contains(".id(\"Click\")"));
534 assert!(out.contains(".cursor_pointer()"));
535 assert!(out.contains(".child(\"Click\")"));
536 }
537
538 #[test]
539 fn test_stateful_button_with_onclick_generates_entity_pattern() {
540 let src = "---\n@state AppState\n---\n<Button onclick=\"inc\">+1</Button>";
541 let out = generate_from(src);
542 assert!(out.contains("let handle = handle.clone();"));
543 assert!(out.contains("handle.update(cx, |this, cx| {"));
544 assert!(out.contains("__fncc_cmd_inc(this, cx);"));
545 }
546
547 #[test]
548 fn test_stateless_button_with_onclick_generates_direct_call() {
549 let out = generate_from("<Button onclick=\"log_click\">Go</Button>");
550 assert!(out.contains(".on_click(__fncc_cmd_log_click)"));
551 }
552
553 #[test]
556 fn test_empty_element_children() {
557 let out = generate_from("<Div></Div>");
558 assert!(out.contains("div()"));
559 }
560
561 #[test]
562 fn test_unknown_element_falls_back_to_div_with_attrs() {
563 let out = generate_from("<CustomEl foo=\"bar\">content</CustomEl>");
564 assert!(out.contains(".attr(\"foo\", \"bar\")"));
565 assert!(out.contains(".child("));
566 assert!(out.contains("\"content\""));
567 }
568
569 #[test]
570 fn test_interpolation_in_text_content_generates_format() {
571 let out = generate_from("---\n@state S\n---\n<Text>{state.msg}</Text>");
572 assert!(out.contains("self.msg.to_string()"));
573 assert!(!out.contains("self.state.msg"));
574 }
575
576 #[test]
577 fn test_interpolation_strips_state_prefix() {
578 let out = generate_from("<Text>{state.count}</Text>");
579 assert!(out.contains("self.count.to_string()"));
581 }
582
583 #[test]
584 fn test_multiple_commands_collected_in_validation_fn() {
585 let src = "<Stack><Button onclick=\"a\">A</Button><Button onclick=\"b\">B</Button></Stack>";
586 let out = generate_from(src);
587 assert!(out.contains("fn _fncc_validate_"));
588 assert!(out.contains("__fncc_cmd_a"));
589 assert!(out.contains("__fncc_cmd_b"));
590 }
591
592 #[test]
595 fn test_generated_code_contains_no_markdown_or_template_leftovers() {
596 let out = generate_from("<Text>hello</Text>");
597 assert!(!out.contains("{{"));
598 assert!(!out.contains("{state."));
599 assert!(!out.contains("__fncc_cmd_") || out.contains("__fncc_cmd_"));
600 }
601
602 #[test]
603 fn test_generated_function_name_follows_snake_case() {
604 let out = generate_from("<HTMLParser></HTMLParser>");
605 assert!(out.contains("render_h_t_m_l_parser") || out.contains("render_html_parser"));
606 }
607
608 #[test]
611 fn test_regression_gap_with_decimal_does_not_produce_invalid_syntax() {
612 let out = generate_from("<Stack gap=\"12.5\"></Stack>");
613 assert!(out.contains(".gap(px(12.5))") || out.contains(".gap(px(12.5.))"));
614 }
615
616 #[test]
617 fn test_regression_duplicate_button_ids_at_same_depth() {
618 let src = "<Stack><Button>OK</Button><Button>OK</Button></Stack>";
619 let out = generate_from(src);
620 let id_count = out.matches(".id(\"OK\")").count();
621 assert!(
622 id_count <= 2,
623 "expected at most 2 .id(\"OK\") occurrences, got {id_count}"
624 );
625 }
626
627 #[test]
628 fn test_regression_multiple_calls_have_unique_validation_fn_names() {
629 let doc = parse("<Button onclick=\"x\">X</Button>").unwrap();
630 let a = generate_with_id(&doc, 0);
631 let b = generate_with_id(&doc, 1);
632 assert_ne!(a, b);
633 assert!(a.contains("__fncc_cmd_x"));
634 assert!(b.contains("__fncc_cmd_x"));
635 }
636
637 #[test]
638 fn test_regression_empty_gap_does_not_panic() {
639 let out = generate_from("<Stack gap=\"\"></Stack>");
640 assert!(!out.contains(".gap(") || out.contains(".gap(px(0.))"));
642 }
643
644 #[test]
645 fn test_regression_interpolation_without_state_prefix() {
646 let out = generate_from("---\n@state S\n---\n<Text>{custom_expr}</Text>");
647 assert!(out.contains("custom_expr") || out.contains("self.custom_expr"));
649 }
650
651 #[test]
652 fn test_regression_many_calls_do_not_panic() {
653 for _ in 0..100 {
654 generate_from("<Button onclick=\"f\">F</Button>");
655 }
656 }
657
658 #[test]
661 fn test_imported_element_generates_render_call() {
662 let doc = parse("<Stack><Header /></Stack>").unwrap();
663 let imports: &[(&str, &str)] = &[("Header", "render_header")];
664 let out = generate_with_imports(&doc, 0, imports, None, None, None, &[], None);
665 assert!(out.contains("render_header()"));
666 }
667
668 #[test]
669 fn test_imported_element_in_stateful_component() {
670 let src = "---\n@state AppState\n---\n<Stack><Footer /></Stack>";
671 let doc = parse(src).unwrap();
672 let imports: &[(&str, &str)] = &[("Footer", "render_footer")];
673 let out = generate_with_imports(&doc, 0, imports, None, None, None, &[], None);
674 assert!(out.contains("render_footer()"));
675 }
676
677 #[test]
678 fn test_gpui_import_falls_back_to_div() {
679 let doc = parse("<Stack><TextInput /></Stack>").unwrap();
680 let imports: &[(&str, &str)] = &[("TextInput", "")];
681 let out = generate_with_imports(&doc, 0, imports, None, None, None, &[], None);
682 assert!(out.contains("div()"));
684 }
685
686 #[test]
687 fn test_builtin_takes_precedence_over_import() {
688 let doc = parse("<Text>hello</Text>").unwrap();
689 let imports: &[(&str, &str)] = &[("Text", "render_text")];
690 let out = generate_with_imports(&doc, 0, imports, None, None, None, &[], None);
691 assert!(out.contains(".child(\"hello\")"));
693 }
694
695 #[test]
696 fn test_imported_element_with_custom_component_name() {
697 let doc = parse("<Stack><MyHeader /></Stack>").unwrap();
698 let imports: &[(&str, &str)] = &[("MyHeader", "render_header")];
699 let out = generate_with_imports(&doc, 0, imports, None, None, None, &[], None);
700 assert!(out.contains("render_header()"));
701 }
702
703 #[test]
704 fn test_render_fn_name_uses_component_name_arg() {
705 let doc = parse("<Text>hello</Text>").unwrap();
706 let out = generate_with_imports(&doc, 0, &[], Some("CustomWidget"), None, None, &[], None);
707 assert!(out.contains("pub fn render_custom_widget()"));
708 assert!(!out.contains("pub fn render_text()"));
710 }
711
712 #[test]
715 fn test_props_stateless_component_with_props_signature() {
716 let doc = parse("<Text>{props.title}</Text>").unwrap();
717 let out = generate_with_imports(&doc, 0, &[], Some("Header"), None, Some("HeaderProps"), &[], None);
718 assert!(out.contains("pub fn render_header(props: &HeaderProps) -> impl IntoElement {"));
719 assert!(out.contains("props.title"));
720 }
721
722 #[test]
723 fn test_props_caller_generates_struct_construction() {
724 let doc = parse("<Header title=\"Welcome\" />").unwrap();
725 let imports: &[(&str, &str)] = &[("Header", "render_header")];
726 let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
727 let out = generate_with_imports(&doc, 0, imports, None, None, None, import_props, None);
728 assert!(out.contains("render_header(&HeaderProps {"));
729 assert!(out.contains("title: \"Welcome\".into(),"));
730 assert!(out.contains("})"));
731 }
732
733 #[test]
734 fn test_props_caller_multiple_attributes() {
735 let doc = parse("<Header title=\"Hi\" subtitle=\"World\" />").unwrap();
736 let imports: &[(&str, &str)] = &[("Header", "render_header")];
737 let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
738 let out = generate_with_imports(&doc, 0, imports, None, None, None, import_props, None);
739 assert!(out.contains("title: \"Hi\".into(),"));
740 assert!(out.contains("subtitle: \"World\".into(),"));
741 }
742
743 #[test]
744 fn test_props_caller_option_field() {
745 let doc = parse("<Header title=\"Hi\" />").unwrap();
746 let imports: &[(&str, &str)] = &[("Header", "render_header")];
747 let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
748 let out = generate_with_imports(&doc, 0, imports, None, None, None, import_props, None);
749 assert!(out.contains("title: \"Hi\".into(),"));
751 }
752
753 #[test]
754 fn test_props_nested_element_with_props() {
755 let doc = parse("<Stack><Header title=\"Nested\" /><Text>ok</Text></Stack>").unwrap();
756 let imports: &[(&str, &str)] = &[("Header", "render_header")];
757 let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
758 let out = generate_with_imports(&doc, 0, imports, None, None, None, import_props, None);
759 assert!(out.contains("render_header(&HeaderProps {"));
760 assert!(out.contains("title: \"Nested\".into(),"));
761 }
762
763 #[test]
764 fn test_props_mixed_components_with_and_without_props() {
765 let doc = parse("<Stack><Header title=\"A\" /><Footer /></Stack>").unwrap();
766 let imports: &[(&str, &str)] = &[("Header", "render_header"), ("Footer", "render_footer")];
767 let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps")), ("Footer", None)];
768 let out = generate_with_imports(&doc, 0, imports, None, None, None, import_props, None);
769 assert!(out.contains("render_header(&HeaderProps {"));
770 assert!(out.contains("title: \"A\".into(),"));
771 assert!(out.contains("render_footer()"));
772 }
773
774 #[test]
775 fn test_props_element_without_props_still_calls_directly() {
776 let doc = parse("<Footer />").unwrap();
777 let imports: &[(&str, &str)] = &[("Footer", "render_footer")];
778 let import_props: &[(&str, Option<&str>)] = &[("Footer", None)];
779 let out = generate_with_imports(&doc, 0, imports, None, None, None, import_props, None);
780 assert!(out.contains("render_footer()"));
781 assert!(!out.contains("&"));
782 }
783
784 #[test]
785 fn test_props_self_closing_with_props() {
786 let doc = parse("<Header title=\"SelfClose\" subtitle=\"X\" />").unwrap();
787 let imports: &[(&str, &str)] = &[("Header", "render_header")];
788 let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
789 let out = generate_with_imports(&doc, 0, imports, None, None, None, import_props, None);
790 assert!(out.contains("title: \"SelfClose\".into(),"));
791 assert!(out.contains("subtitle: \"X\".into(),"));
792 }
793}