1#![allow(clippy::too_many_arguments)]
2
3use std::collections::HashMap;
4
5use crate::parser::{self, AttrValue, Document, Element, Node};
6use crate::semantic::PropField;
7
8pub fn generate(doc: &Document) -> String {
9 generate_with_id(doc, 0)
10}
11
12pub fn generate_with_id(doc: &Document, file_id: usize) -> String {
13 generate_with_imports(doc, file_id, &[], None, None, None, &[], None, &[])
14}
15
16pub type ResolvedImport<'a> = (&'a str, &'a str);
20
21pub fn generate_with_imports(
26 doc: &Document,
27 file_id: usize,
28 imports: &[ResolvedImport],
29 component_name: Option<&str>,
30 resolved_state_type: Option<&str>,
31 props_type: Option<&str>,
32 import_props: &[(&str, Option<&str>)],
33 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
34 import_has_slots: &[(&str, bool)],
35) -> String {
36 let mut out = String::new();
37 let state_type = resolved_state_type.or(doc.state_type.as_deref());
38 let has_state = state_type.is_some();
39 let has_slot = parser::has_slot(&doc.root);
40
41 if let Some(ref fm) = doc.frontmatter {
42 out.push_str(fm);
43 out.push('\n');
44 }
45
46 let commands = parser::collect_commands(&doc.root);
48 if !commands.is_empty() {
49 out.push_str("#[allow(unused)]\n");
50 out.push_str(&format!("fn _fncc_validate_{file_id}() {{\n"));
51 for cmd in &commands {
52 let trampoline = format!("__fncc_cmd_{cmd}");
53 out.push_str(&format!(" let _ = {trampoline};\n"));
54 }
55 out.push_str("}\n\n");
56 }
57
58 if has_state {
59 generate_stateful(
60 doc,
61 &mut out,
62 imports,
63 state_type,
64 import_props,
65 prop_fields,
66 import_has_slots,
67 );
68 } else {
69 generate_stateless(
70 doc,
71 &mut out,
72 imports,
73 component_name,
74 props_type,
75 import_props,
76 prop_fields,
77 import_has_slots,
78 has_slot,
79 );
80 }
81
82 out
83}
84
85fn generate_stateless(
86 doc: &Document,
87 out: &mut String,
88 imports: &[ResolvedImport],
89 component_name: Option<&str>,
90 props_type: Option<&str>,
91 import_props: &[(&str, Option<&str>)],
92 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
93 import_has_slots: &[(&str, bool)],
94 has_slot: bool,
95) {
96 let name = component_name.unwrap_or(&doc.root.name);
97 let fn_name = format!("render_{}", to_snake_case(name));
98
99 let params = match (props_type, has_slot) {
101 (Some(pt), true) => format!("props: &{pt}, children: impl IntoElement"),
102 (Some(pt), false) => format!("props: &{pt}"),
103 (None, true) => "children: impl IntoElement".to_string(),
104 (None, false) => String::new(),
105 };
106
107 let sig = if params.is_empty() {
108 format!("pub fn {fn_name}() -> impl IntoElement {{\n")
109 } else {
110 format!("pub fn {fn_name}({params}) -> impl IntoElement {{\n")
111 };
112 out.push_str(&sig);
113 out.push_str(&generate_element(
114 &doc.root,
115 1,
116 false,
117 imports,
118 import_props,
119 prop_fields,
120 import_has_slots,
121 ));
122 out.push('\n');
123 out.push_str("}\n");
124}
125
126fn generate_stateful(
127 doc: &Document,
128 out: &mut String,
129 imports: &[ResolvedImport],
130 state_type: Option<&str>,
131 import_props: &[(&str, Option<&str>)],
132 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
133 _import_has_slots: &[(&str, bool)],
134) {
135 let state_type = state_type.unwrap_or("Self");
136
137 out.push_str(&format!("impl Render for {state_type} {{\n"));
138 out.push_str(" fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {\n");
139 out.push_str(" let handle = cx.entity().downgrade();\n");
140 out.push_str(&generate_element(
141 &doc.root,
142 2,
143 true,
144 imports,
145 import_props,
146 prop_fields,
147 _import_has_slots,
148 ));
149 out.push_str("\n }\n");
150 out.push_str("}\n");
151}
152
153fn collect_if_chain(nodes: &[Node], start: usize) -> Vec<&Element> {
157 let mut branches = Vec::new();
158 for node in &nodes[start..] {
159 match node {
160 Node::Element(el) if el.name == "If" => {
161 if branches.is_empty() {
162 branches.push(el);
163 } else {
164 break; }
166 }
167 Node::Element(el) if el.name == "ElseIf" || el.name == "Else" => {
168 if branches.is_empty() {
169 break; }
171 branches.push(el);
172 }
173 _ => break,
174 }
175 }
176 branches
177}
178
179fn gen_if_expr(
182 branches: &[&Element],
183 stateful: bool,
184 imports: &[ResolvedImport],
185 import_props: &[(&str, Option<&str>)],
186 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
187 _import_has_slots: &[(&str, bool)],
188) -> String {
189 let mut expr = String::new();
190 for (i, el) in branches.iter().enumerate() {
191 if i == 0 {
192 expr.push_str("if ");
193 } else if el.name == "Else" {
194 expr.push_str(" else ");
195 } else {
196 expr.push_str(" else if ");
197 }
198 if el.name != "Else" {
199 if let Some(cond) = parser::get_condition_attr(el) {
200 let e = interpolation_expr(cond);
201 expr.push_str(&e);
202 expr.push(' ');
203 } else {
204 expr.push_str("compile_error!(\"<");
205 expr.push_str(&el.name);
206 expr.push_str("> element is missing `condition` attribute\") ");
207 }
208 }
209 expr.push_str("{ ");
210 match &el.children[..] {
211 [] => expr.push_str("div()"),
212 [single] => match single {
213 Node::Text(t) => expr.push_str(&format!("div().child({:?})", t)),
214 Node::Interpolation(interp) => {
215 let e = interpolation_expr(interp);
216 expr.push_str(&format!("div().child({e}.to_string())"));
217 }
218 Node::Element(child_el) => {
219 let child_code = generate_element(
220 child_el,
221 0,
222 stateful,
223 imports,
224 import_props,
225 prop_fields,
226 _import_has_slots,
227 );
228 expr.push_str(&format!("div().child({})", clean_inline(&child_code)));
229 }
230 },
231 children => {
232 expr.push_str("div()");
233 for child in children {
234 match child {
235 Node::Text(t) => expr.push_str(&format!(".child({:?})", t)),
236 Node::Interpolation(interp) => {
237 let e = interpolation_expr(interp);
238 expr.push_str(&format!(".child({e}.to_string())"));
239 }
240 Node::Element(child_el) => {
241 let child_code = generate_element(
242 child_el,
243 0,
244 stateful,
245 imports,
246 import_props,
247 prop_fields,
248 _import_has_slots,
249 );
250 expr.push_str(&format!(".child({})", clean_inline(&child_code)));
251 }
252 }
253 }
254 }
255 }
256 expr.push_str(" }");
257 }
258 if branches.last().map(|b| b.name.as_str()) != Some("Else") {
260 expr.push_str(" else { div() }");
261 }
262 expr
263}
264
265fn gen_for_expr(
267 el: &Element,
268 stateful: bool,
269 imports: &[ResolvedImport],
270 import_props: &[(&str, Option<&str>)],
271 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
272 _import_has_slots: &[(&str, bool)],
273) -> String {
274 let each = match parser::get_each_attr(el) {
275 Some(e) => e,
276 None => {
277 return "compile_error!(\"<For> element is missing required `each` attribute\")".to_string();
278 }
279 };
280 let let_var = parser::get_let_attr(el).unwrap_or("item");
281 let index_var = parser::get_index_attr(el);
282 let iter_expr = interpolation_expr(each);
283
284 let mut body = String::new();
285 match &el.children[..] {
286 [] => body.push_str("div()"),
287 [single] => match single {
288 Node::Text(t) => body.push_str(&format!("div().child({:?})", t)),
289 Node::Interpolation(interp) => {
290 let e = interpolation_expr(interp);
291 body.push_str(&format!("div().child({e}.to_string())"));
292 }
293 Node::Element(child_el) => {
294 let child_code = generate_element(
295 child_el,
296 1,
297 stateful,
298 imports,
299 import_props,
300 prop_fields,
301 _import_has_slots,
302 );
303 body.push_str(&child_code);
304 }
305 },
306 children => {
307 body.push_str("div()");
308 for child in children {
309 match child {
310 Node::Text(t) => body.push_str(&format!(".child({:?})", t)),
311 Node::Interpolation(interp) => {
312 let e = interpolation_expr(interp);
313 body.push_str(&format!(".child({e}.to_string())"));
314 }
315 Node::Element(child_el) => {
316 let child_code = generate_element(
317 child_el,
318 1,
319 stateful,
320 imports,
321 import_props,
322 prop_fields,
323 _import_has_slots,
324 );
325 body.push_str(&format!(".child({})", clean_inline(&child_code)));
326 }
327 }
328 }
329 }
330 }
331
332 let var_names: Vec<&str> = index_var.iter().chain(std::iter::once(&let_var)).copied().collect();
336 for var in &var_names {
337 body = replace_self_prefix(&body, var);
338 }
339
340 if let Some(index) = index_var {
341 format!("{iter_expr}.iter().enumerate().map(|({index}, {let_var})| {{\n {body}\n}})")
342 } else {
343 format!("{iter_expr}.iter().map(|{let_var}| {{\n {body}\n}})")
344 }
345}
346
347fn generate_element(
348 el: &Element,
349 depth: usize,
350 stateful: bool,
351 imports: &[ResolvedImport],
352 import_props: &[(&str, Option<&str>)],
353 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
354 _import_has_slots: &[(&str, bool)],
355) -> String {
356 let indent = " ".repeat(depth);
357
358 match el.name.as_str() {
360 "Stack" => {
361 return gen_stack(
362 el,
363 &indent,
364 depth,
365 stateful,
366 imports,
367 import_props,
368 prop_fields,
369 _import_has_slots,
370 );
371 }
372 "Text" => {
373 return gen_text(
374 el,
375 &indent,
376 depth,
377 stateful,
378 imports,
379 import_props,
380 prop_fields,
381 _import_has_slots,
382 );
383 }
384 "Button" => {
385 return gen_button(
386 el,
387 &indent,
388 depth,
389 stateful,
390 imports,
391 import_props,
392 prop_fields,
393 _import_has_slots,
394 );
395 }
396 "Fragment" => {
397 return gen_fragment(
398 el,
399 &indent,
400 depth,
401 stateful,
402 imports,
403 import_props,
404 prop_fields,
405 _import_has_slots,
406 );
407 }
408 "For" => {
409 return format!(
410 "div().children({})",
411 gen_for_expr(el, stateful, imports, import_props, prop_fields, _import_has_slots)
412 );
413 }
414 "If" | "ElseIf" | "Else" => {
415 let branches = [el];
417 return gen_if_expr(
418 &branches,
419 stateful,
420 imports,
421 import_props,
422 prop_fields,
423 _import_has_slots,
424 );
425 }
426 "Slot" => return "children".to_string(),
427 _ => {}
428 }
429
430 if let Some(render_fn) = imports
432 .iter()
433 .find(|(name, fn_name)| name == &el.name && !fn_name.is_empty())
434 .map(|(_, fn_name)| *fn_name)
435 {
436 let props_type = import_props.iter().find(|(n, _)| n == &el.name).and_then(|(_, p)| *p);
437 let has_slot = _import_has_slots
438 .iter()
439 .find(|(n, _)| n == &el.name)
440 .map(|(_, h)| *h)
441 .unwrap_or(false);
442
443 let children_expr = if has_slot {
444 if el.children.is_empty() {
445 "div()".to_string()
446 } else {
447 let child_code = generate_children_code(
448 &el.children,
449 &indent,
450 depth + 1,
451 stateful,
452 imports,
453 import_props,
454 prop_fields,
455 _import_has_slots,
456 );
457 clean_inline(&format!("div(){child_code}"))
458 }
459 } else {
460 String::new()
461 };
462
463 return if let Some(pt) = props_type {
464 let mut struct_fields = String::new();
465 if let Some(fields) = prop_fields.and_then(|m| m.get(pt)) {
466 for f in fields {
467 if let Some((_, attr_val)) = el.attrs.iter().find(|(n, _)| n == &f.name) {
468 let v = match attr_val {
469 AttrValue::String(s) => format!("{:?}.into()", s),
470 AttrValue::Interpolation(expr) => {
471 format!("{}.into()", interpolation_expr(expr))
472 }
473 };
474 struct_fields.push_str(&format!("\n{indent} {}: {},", f.name, v));
475 } else if f.is_optional {
476 struct_fields.push_str(&format!("\n{indent} {}: None,", f.name));
477 }
478 }
479 } else {
480 for (attr_name, attr_val) in &el.attrs {
481 let v = match attr_val {
482 AttrValue::String(s) => format!("{:?}.into()", s),
483 AttrValue::Interpolation(expr) => {
484 format!("{}.into()", interpolation_expr(expr))
485 }
486 };
487 struct_fields.push_str(&format!("\n{indent} {attr_name}: {v},"));
488 }
489 }
490 if has_slot {
491 format!("{indent}{render_fn}(&{pt} {{ {struct_fields}\n{indent} }}, {children_expr})")
492 } else {
493 format!("{indent}{render_fn}(&{pt} {{ {struct_fields}\n{indent} }})")
494 }
495 } else if has_slot {
496 format!("{indent}{render_fn}({children_expr})")
497 } else {
498 format!("{indent}{render_fn}()")
499 };
500 }
501
502 gen_fallback(
504 el,
505 &indent,
506 depth,
507 stateful,
508 imports,
509 import_props,
510 prop_fields,
511 _import_has_slots,
512 )
513}
514
515fn gen_fragment(
518 el: &Element,
519 indent: &str,
520 depth: usize,
521 stateful: bool,
522 imports: &[ResolvedImport],
523 import_props: &[(&str, Option<&str>)],
524 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
525 _import_has_slots: &[(&str, bool)],
526) -> String {
527 let children_code = generate_children_code(
528 &el.children,
529 indent,
530 depth,
531 stateful,
532 imports,
533 import_props,
534 prop_fields,
535 _import_has_slots,
536 );
537 format!("{indent}div()\n{children_code}").trim_end().to_string()
538}
539
540fn generate_children_code(
542 children: &[Node],
543 indent: &str,
544 depth: usize,
545 stateful: bool,
546 imports: &[ResolvedImport],
547 import_props: &[(&str, Option<&str>)],
548 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
549 _import_has_slots: &[(&str, bool)],
550) -> String {
551 let mut out = String::new();
552 let mut i = 0;
553 while i < children.len() {
554 match &children[i] {
555 Node::Element(el) if el.name == "If" => {
556 let chain = collect_if_chain(children, i);
557 let expr = gen_if_expr(&chain, stateful, imports, import_props, prop_fields, _import_has_slots);
558 out.push_str(&format!("{indent} .child(\n"));
559 out.push_str(&format!("{indent} {expr}\n"));
560 out.push_str(&format!("{indent} )\n"));
561 i += chain.len();
562 }
563 Node::Element(el) if el.name == "For" => {
564 let for_code = gen_for_expr(el, stateful, imports, import_props, prop_fields, _import_has_slots);
565 out.push_str(&format!("{indent} .children(\n"));
566 out.push_str(&format!("{indent} {for_code}\n"));
567 out.push_str(&format!("{indent} )\n"));
568 i += 1;
569 }
570 Node::Element(el) if el.name == "Fragment" => {
571 let frag_code = generate_children_code(
572 &el.children,
573 indent,
574 depth,
575 stateful,
576 imports,
577 import_props,
578 prop_fields,
579 _import_has_slots,
580 );
581 out.push_str(&frag_code);
582 i += 1;
583 }
584 _ => {
585 out.push_str(&format!("{indent} .child(\n"));
586 match &children[i] {
587 Node::Element(child_el) => {
588 out.push_str(&generate_element(
589 child_el,
590 depth + 2,
591 stateful,
592 imports,
593 import_props,
594 prop_fields,
595 _import_has_slots,
596 ));
597 }
598 Node::Text(t) => {
599 out.push_str(&format!("{indent} {:?}", t));
600 }
601 Node::Interpolation(expr) => {
602 let e = interpolation_expr(expr);
603 out.push_str(&format!("{indent} {e}.to_string()"));
604 }
605 }
606 out.push('\n');
607 out.push_str(&format!("{indent} )\n"));
608 i += 1;
609 }
610 }
611 }
612 out
613}
614
615fn gen_stack(
616 el: &Element,
617 indent: &str,
618 depth: usize,
619 stateful: bool,
620 imports: &[ResolvedImport],
621 import_props: &[(&str, Option<&str>)],
622 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
623 _import_has_slots: &[(&str, bool)],
624) -> String {
625 let mut out = format!("{indent}div()\n");
626
627 let mut is_vertical = false;
628 for (key, val) in &el.attrs {
629 match key.as_str() {
630 "direction" if val.as_str() == "vertical" => is_vertical = true,
631 "gap" => {
632 let v = val.as_str();
633 if let Ok(n) = v.parse::<f64>() {
634 out.push_str(&format!("{indent} .gap(px({n}.))\n"));
635 }
636 }
637 _ => {}
638 }
639 }
640
641 if is_vertical {
642 out.push_str(&format!("{indent} .flex()\n{indent} .flex_col()\n"));
643 } else {
644 out.push_str(&format!("{indent} .flex()\n"));
645 }
646
647 out.push_str(&generate_children_code(
648 &el.children,
649 indent,
650 depth,
651 stateful,
652 imports,
653 import_props,
654 prop_fields,
655 _import_has_slots,
656 ));
657
658 out.trim_end().to_string()
659}
660
661fn gen_text(
662 el: &Element,
663 indent: &str,
664 depth: usize,
665 stateful: bool,
666 imports: &[ResolvedImport],
667 import_props: &[(&str, Option<&str>)],
668 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
669 _import_has_slots: &[(&str, bool)],
670) -> String {
671 let mut out = format!("{indent}div()\n");
672
673 for (key, val) in &el.attrs {
674 if key.as_str() == "size" {
675 let v = val.as_str();
676 let ts = match v {
677 "xs" => "text_xs()",
678 "sm" => "text_sm()",
679 "base" => "text_base()",
680 "lg" => "text_lg()",
681 "xl" => "text_xl()",
682 "2xl" | "xxl" => "text_2xl()",
683 "3xl" => "text_3xl()",
684 _ => "text_base()",
685 };
686 out.push_str(&format!("{indent} .{ts}\n"));
687 }
688 }
689
690 match &el.children[..] {
691 [Node::Text(t)] => {
692 out.push_str(&format!("{indent} .child({:?})", t));
693 }
694 [Node::Interpolation(expr)] => {
695 let e = interpolation_expr(expr);
696 out.push_str(&format!("{indent} .child({e}.to_string())"));
697 }
698 children => {
699 out.push_str(&generate_children_code(
700 children,
701 indent,
702 depth,
703 stateful,
704 imports,
705 import_props,
706 prop_fields,
707 _import_has_slots,
708 ));
709 }
710 }
711
712 out.trim_end().to_string()
713}
714
715fn gen_button(
716 el: &Element,
717 indent: &str,
718 depth: usize,
719 stateful: bool,
720 imports: &[ResolvedImport],
721 import_props: &[(&str, Option<&str>)],
722 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
723 _import_has_slots: &[(&str, bool)],
724) -> String {
725 let mut out = format!("{indent}div()\n");
726
727 let btn_id = match &el.children[..] {
728 [Node::Text(t)] => t.clone(),
729 _ => format!("button_{depth}"),
730 };
731 out.push_str(&format!("{indent} .id({:?})\n", btn_id));
732 out.push_str(&format!("{indent} .cursor_pointer()\n"));
733
734 for (key, val) in &el.attrs {
735 if key.as_str() == "onclick" {
736 let handler = val.as_str();
737 let trampoline = format!("__fncc_cmd_{handler}");
738 if stateful {
739 out.push_str(&format!("{indent} .on_click({{\n"));
741 out.push_str(&format!("{indent} let handle = handle.clone();\n"));
742 out.push_str(&format!("{indent} move |_, _, cx| {{\n"));
743 out.push_str(&format!("{indent} handle.update(cx, |this, cx| {{\n"));
744 out.push_str(&format!("{indent} {trampoline}(this, cx);\n"));
745 out.push_str(&format!("{indent} }}).ok();\n"));
746 out.push_str(&format!("{indent} }}\n"));
747 out.push_str(&format!("{indent} }})\n"));
748 } else {
749 out.push_str(&format!("{indent} .on_click({trampoline})\n"));
750 }
751 }
752 }
753
754 match &el.children[..] {
755 [Node::Text(t)] => out.push_str(&format!("{indent} .child({:?})", t)),
756 [Node::Interpolation(expr)] => {
757 let e = interpolation_expr(expr);
758 out.push_str(&format!("{indent} .child({e}.to_string())"));
759 }
760 children => {
761 out.push_str(&generate_children_code(
762 children,
763 indent,
764 depth,
765 stateful,
766 imports,
767 import_props,
768 prop_fields,
769 _import_has_slots,
770 ));
771 }
772 }
773
774 out.trim_end().to_string()
775}
776
777fn gen_fallback(
778 el: &Element,
779 indent: &str,
780 depth: usize,
781 stateful: bool,
782 imports: &[ResolvedImport],
783 import_props: &[(&str, Option<&str>)],
784 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
785 _import_has_slots: &[(&str, bool)],
786) -> String {
787 let mut out = format!("{indent}div()\n");
788 for (key, val) in &el.attrs {
789 let v = val.as_str();
790 out.push_str(&format!("{indent} .attr({:?}, {:?})\n", key, v));
791 }
792 out.push_str(&generate_children_code(
793 &el.children,
794 indent,
795 depth,
796 stateful,
797 imports,
798 import_props,
799 prop_fields,
800 _import_has_slots,
801 ));
802 out.trim_end().to_string()
803}
804
805fn clean_inline(s: &str) -> String {
807 let collapsed: String = s.split_whitespace().collect::<Vec<_>>().join(" ");
808 collapsed.replace(") .", ").")
809}
810
811fn interpolation_expr(expr: &str) -> String {
816 let trimmed = expr.trim();
817 if let Some(field) = trimmed.strip_prefix("state.") {
818 format!("self.{field}")
819 } else if trimmed.starts_with("props.") {
820 trimmed.to_string()
821 } else {
822 format!("self.{trimmed}")
823 }
824}
825
826fn replace_self_prefix(s: &str, var: &str) -> String {
830 let pattern = format!("self.{var}");
831 let mut out = String::new();
832 let mut last = 0;
833 for (idx, _) in s.match_indices(&pattern) {
834 out.push_str(&s[last..idx]);
835 let end = idx + pattern.len();
836 let keep = if end < s.len() {
837 let b = s.as_bytes()[end];
838 b.is_ascii_alphanumeric() || b == b'_'
839 } else {
840 false
841 };
842 if keep {
843 out.push_str(&pattern);
844 last = idx + pattern.len();
845 } else {
846 out.push_str(var);
847 last = end;
848 }
849 }
850 out.push_str(&s[last..]);
851 out
852}
853
854pub(crate) fn to_snake_case(name: &str) -> String {
855 let mut result = String::new();
856 for (i, ch) in name.chars().enumerate() {
857 if ch.is_uppercase() {
858 if i > 0 {
859 result.push('_');
860 }
861 for c in ch.to_lowercase() {
862 result.push(c);
863 }
864 } else {
865 result.push(ch);
866 }
867 }
868 result
869}
870
871#[cfg(test)]
872mod tests {
873 use super::*;
874 use crate::parser::parse;
875
876 fn generate_from(source: &str) -> String {
877 let doc = parse(source).unwrap();
878 generate(&doc)
879 }
880
881 #[test]
884 fn test_generates_stateless_render_function() {
885 let out = generate_from("<MyComp></MyComp>");
886 assert!(out.contains("pub fn render_my_comp() -> impl IntoElement {"));
887 assert!(out.contains("div()"));
888 }
889
890 #[test]
891 fn test_generates_stateful_render_impl() {
892 let src = "---\n@state CounterState\n---\n<App></App>";
893 let out = generate_from(src);
894 assert!(out.contains("impl Render for CounterState {"));
895 assert!(
896 out.contains("fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {")
897 );
898 }
899
900 #[test]
901 fn test_frontmatter_is_preserved_in_output() {
902 let src = "---\nuse crate::prelude::*;\n---\n<App></App>";
903 let out = generate_from(src);
904 assert!(out.contains("use crate::prelude::*;"));
905 }
906
907 #[test]
908 fn test_stack_with_direction_vertical_generates_flex_col() {
909 let out = generate_from("<Stack direction=\"vertical\"></Stack>");
910 assert!(out.contains(".flex()"));
911 assert!(out.contains(".flex_col()"));
912 }
913
914 #[test]
915 fn test_stack_with_direction_horizontal_generates_flex_only() {
916 let out = generate_from("<Stack direction=\"horizontal\"></Stack>");
917 assert!(out.contains(".flex()"));
918 assert!(!out.contains(".flex_col()"));
919 }
920
921 #[test]
922 fn test_stack_with_gap_generates_px_value() {
923 let out = generate_from("<Stack gap=\"12\"></Stack>");
924 assert!(out.contains(".gap(px(12.))"));
925 }
926
927 #[test]
928 fn test_text_with_size_xl() {
929 let out = generate_from("<Text size=\"xl\">hello</Text>");
930 assert!(out.contains(".text_xl()"));
931 assert!(out.contains(".child(\"hello\")"));
932 }
933
934 #[test]
935 fn test_text_with_size_unknown_falls_back_to_base() {
936 let out = generate_from("<Text size=\"huge\">text</Text>");
937 assert!(out.contains(".text_base()"));
938 }
939
940 #[test]
941 fn test_button_with_text_child() {
942 let out = generate_from("<Button onclick=\"handle_click\">Click</Button>");
943 assert!(out.contains(".id(\"Click\")"));
944 assert!(out.contains(".cursor_pointer()"));
945 assert!(out.contains(".child(\"Click\")"));
946 }
947
948 #[test]
949 fn test_stateful_button_with_onclick_generates_entity_pattern() {
950 let src = "---\n@state AppState\n---\n<Button onclick=\"inc\">+1</Button>";
951 let out = generate_from(src);
952 assert!(out.contains("let handle = handle.clone();"));
953 assert!(out.contains("handle.update(cx, |this, cx| {"));
954 assert!(out.contains("__fncc_cmd_inc(this, cx);"));
955 }
956
957 #[test]
958 fn test_stateless_button_with_onclick_generates_direct_call() {
959 let out = generate_from("<Button onclick=\"log_click\">Go</Button>");
960 assert!(out.contains(".on_click(__fncc_cmd_log_click)"));
961 }
962
963 #[test]
966 fn test_empty_element_children() {
967 let out = generate_from("<Div></Div>");
968 assert!(out.contains("div()"));
969 }
970
971 #[test]
972 fn test_unknown_element_falls_back_to_div_with_attrs() {
973 let out = generate_from("<CustomEl foo=\"bar\">content</CustomEl>");
974 assert!(out.contains(".attr(\"foo\", \"bar\")"));
975 assert!(out.contains(".child("));
976 assert!(out.contains("\"content\""));
977 }
978
979 #[test]
980 fn test_interpolation_in_text_content_generates_format() {
981 let out = generate_from("---\n@state S\n---\n<Text>{state.msg}</Text>");
982 assert!(out.contains("self.msg.to_string()"));
983 assert!(!out.contains("self.state.msg"));
984 }
985
986 #[test]
987 fn test_interpolation_strips_state_prefix() {
988 let out = generate_from("<Text>{state.count}</Text>");
989 assert!(out.contains("self.count.to_string()"));
991 }
992
993 #[test]
994 fn test_multiple_commands_collected_in_validation_fn() {
995 let src = "<Stack><Button onclick=\"a\">A</Button><Button onclick=\"b\">B</Button></Stack>";
996 let out = generate_from(src);
997 assert!(out.contains("fn _fncc_validate_"));
998 assert!(out.contains("__fncc_cmd_a"));
999 assert!(out.contains("__fncc_cmd_b"));
1000 }
1001
1002 #[test]
1005 fn test_generated_code_contains_no_markdown_or_template_leftovers() {
1006 let out = generate_from("<Text>hello</Text>");
1007 assert!(!out.contains("{{"));
1008 assert!(!out.contains("{state."));
1009 assert!(!out.contains("__fncc_cmd_") || out.contains("__fncc_cmd_"));
1010 }
1011
1012 #[test]
1013 fn test_generated_function_name_follows_snake_case() {
1014 let out = generate_from("<HTMLParser></HTMLParser>");
1015 assert!(out.contains("render_h_t_m_l_parser") || out.contains("render_html_parser"));
1016 }
1017
1018 #[test]
1021 fn test_regression_gap_with_decimal_does_not_produce_invalid_syntax() {
1022 let out = generate_from("<Stack gap=\"12.5\"></Stack>");
1023 assert!(out.contains(".gap(px(12.5))") || out.contains(".gap(px(12.5.))"));
1024 }
1025
1026 #[test]
1027 fn test_regression_duplicate_button_ids_at_same_depth() {
1028 let src = "<Stack><Button>OK</Button><Button>OK</Button></Stack>";
1029 let out = generate_from(src);
1030 let id_count = out.matches(".id(\"OK\")").count();
1031 assert!(
1032 id_count <= 2,
1033 "expected at most 2 .id(\"OK\") occurrences, got {id_count}"
1034 );
1035 }
1036
1037 #[test]
1038 fn test_regression_multiple_calls_have_unique_validation_fn_names() {
1039 let doc = parse("<Button onclick=\"x\">X</Button>").unwrap();
1040 let a = generate_with_id(&doc, 0);
1041 let b = generate_with_id(&doc, 1);
1042 assert_ne!(a, b);
1043 assert!(a.contains("__fncc_cmd_x"));
1044 assert!(b.contains("__fncc_cmd_x"));
1045 }
1046
1047 #[test]
1048 fn test_regression_empty_gap_does_not_panic() {
1049 let out = generate_from("<Stack gap=\"\"></Stack>");
1050 assert!(!out.contains(".gap(") || out.contains(".gap(px(0.))"));
1052 }
1053
1054 #[test]
1055 fn test_regression_interpolation_without_state_prefix() {
1056 let out = generate_from("---\n@state S\n---\n<Text>{custom_expr}</Text>");
1057 assert!(out.contains("custom_expr") || out.contains("self.custom_expr"));
1059 }
1060
1061 #[test]
1062 fn test_regression_many_calls_do_not_panic() {
1063 for _ in 0..100 {
1064 generate_from("<Button onclick=\"f\">F</Button>");
1065 }
1066 }
1067
1068 #[test]
1071 fn test_imported_element_generates_render_call() {
1072 let doc = parse("<Stack><Header /></Stack>").unwrap();
1073 let imports: &[(&str, &str)] = &[("Header", "render_header")];
1074 let out = generate_with_imports(&doc, 0, imports, None, None, None, &[], None, &[]);
1075 assert!(out.contains("render_header()"));
1076 }
1077
1078 #[test]
1079 fn test_imported_element_in_stateful_component() {
1080 let src = "---\n@state AppState\n---\n<Stack><Footer /></Stack>";
1081 let doc = parse(src).unwrap();
1082 let imports: &[(&str, &str)] = &[("Footer", "render_footer")];
1083 let out = generate_with_imports(&doc, 0, imports, None, None, None, &[], None, &[]);
1084 assert!(out.contains("render_footer()"));
1085 }
1086
1087 #[test]
1088 fn test_gpui_import_falls_back_to_div() {
1089 let doc = parse("<Stack><TextInput /></Stack>").unwrap();
1090 let imports: &[(&str, &str)] = &[("TextInput", "")];
1091 let out = generate_with_imports(&doc, 0, imports, None, None, None, &[], None, &[]);
1092 assert!(out.contains("div()"));
1094 }
1095
1096 #[test]
1097 fn test_builtin_takes_precedence_over_import() {
1098 let doc = parse("<Text>hello</Text>").unwrap();
1099 let imports: &[(&str, &str)] = &[("Text", "render_text")];
1100 let out = generate_with_imports(&doc, 0, imports, None, None, None, &[], None, &[]);
1101 assert!(out.contains(".child(\"hello\")"));
1103 }
1104
1105 #[test]
1106 fn test_imported_element_with_custom_component_name() {
1107 let doc = parse("<Stack><MyHeader /></Stack>").unwrap();
1108 let imports: &[(&str, &str)] = &[("MyHeader", "render_header")];
1109 let out = generate_with_imports(&doc, 0, imports, None, None, None, &[], None, &[]);
1110 assert!(out.contains("render_header()"));
1111 }
1112
1113 #[test]
1114 fn test_render_fn_name_uses_component_name_arg() {
1115 let doc = parse("<Text>hello</Text>").unwrap();
1116 let out = generate_with_imports(&doc, 0, &[], Some("CustomWidget"), None, None, &[], None, &[]);
1117 assert!(out.contains("pub fn render_custom_widget()"));
1118 assert!(!out.contains("pub fn render_text()"));
1120 }
1121
1122 #[test]
1125 fn test_props_stateless_component_with_props_signature() {
1126 let doc = parse("<Text>{props.title}</Text>").unwrap();
1127 let out = generate_with_imports(&doc, 0, &[], Some("Header"), None, Some("HeaderProps"), &[], None, &[]);
1128 assert!(out.contains("pub fn render_header(props: &HeaderProps) -> impl IntoElement {"));
1129 assert!(out.contains("props.title"));
1130 }
1131
1132 #[test]
1133 fn test_props_caller_generates_struct_construction() {
1134 let doc = parse("<Header title=\"Welcome\" />").unwrap();
1135 let imports: &[(&str, &str)] = &[("Header", "render_header")];
1136 let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
1137 let out = generate_with_imports(&doc, 0, imports, None, None, None, import_props, None, &[]);
1138 assert!(out.contains("render_header(&HeaderProps {"));
1139 assert!(out.contains("title: \"Welcome\".into(),"));
1140 assert!(out.contains("})"));
1141 }
1142
1143 #[test]
1144 fn test_props_caller_multiple_attributes() {
1145 let doc = parse("<Header title=\"Hi\" subtitle=\"World\" />").unwrap();
1146 let imports: &[(&str, &str)] = &[("Header", "render_header")];
1147 let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
1148 let out = generate_with_imports(&doc, 0, imports, None, None, None, import_props, None, &[]);
1149 assert!(out.contains("title: \"Hi\".into(),"));
1150 assert!(out.contains("subtitle: \"World\".into(),"));
1151 }
1152
1153 #[test]
1154 fn test_props_caller_option_field() {
1155 let doc = parse("<Header title=\"Hi\" />").unwrap();
1156 let imports: &[(&str, &str)] = &[("Header", "render_header")];
1157 let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
1158 let out = generate_with_imports(&doc, 0, imports, None, None, None, import_props, None, &[]);
1159 assert!(out.contains("title: \"Hi\".into(),"));
1161 }
1162
1163 #[test]
1164 fn test_props_nested_element_with_props() {
1165 let doc = parse("<Stack><Header title=\"Nested\" /><Text>ok</Text></Stack>").unwrap();
1166 let imports: &[(&str, &str)] = &[("Header", "render_header")];
1167 let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
1168 let out = generate_with_imports(&doc, 0, imports, None, None, None, import_props, None, &[]);
1169 assert!(out.contains("render_header(&HeaderProps {"));
1170 assert!(out.contains("title: \"Nested\".into(),"));
1171 }
1172
1173 #[test]
1174 fn test_props_mixed_components_with_and_without_props() {
1175 let doc = parse("<Stack><Header title=\"A\" /><Footer /></Stack>").unwrap();
1176 let imports: &[(&str, &str)] = &[("Header", "render_header"), ("Footer", "render_footer")];
1177 let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps")), ("Footer", None)];
1178 let out = generate_with_imports(&doc, 0, imports, None, None, None, import_props, None, &[]);
1179 assert!(out.contains("render_header(&HeaderProps {"));
1180 assert!(out.contains("title: \"A\".into(),"));
1181 assert!(out.contains("render_footer()"));
1182 }
1183
1184 #[test]
1185 fn test_props_element_without_props_still_calls_directly() {
1186 let doc = parse("<Footer />").unwrap();
1187 let imports: &[(&str, &str)] = &[("Footer", "render_footer")];
1188 let import_props: &[(&str, Option<&str>)] = &[("Footer", None)];
1189 let out = generate_with_imports(&doc, 0, imports, None, None, None, import_props, None, &[]);
1190 assert!(out.contains("render_footer()"));
1191 assert!(!out.contains("&"));
1192 }
1193
1194 #[test]
1195 fn test_props_self_closing_with_props() {
1196 let doc = parse("<Header title=\"SelfClose\" subtitle=\"X\" />").unwrap();
1197 let imports: &[(&str, &str)] = &[("Header", "render_header")];
1198 let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
1199 let out = generate_with_imports(&doc, 0, imports, None, None, None, import_props, None, &[]);
1200 assert!(out.contains("title: \"SelfClose\".into(),"));
1201 assert!(out.contains("subtitle: \"X\".into(),"));
1202 }
1203
1204 #[test]
1207 fn test_if_generates_if_expression() {
1208 let out = generate_from("<If condition=\"{state.show}\"><Text>Hi</Text></If>");
1209 assert!(out.contains("if self.show {"));
1210 assert!(out.contains("div().child(div().child(\"Hi\"))"));
1211 assert!(out.contains("} else {"), "else branch should be present");
1212 assert!(out.contains("div() }"));
1213 }
1214
1215 #[test]
1216 fn test_if_else_chain() {
1217 let src = "<Stack><If condition=\"{state.a}\"><Text>A</Text></If><Else><Text>B</Text></Else></Stack>";
1218 let out = generate_from(src);
1219 assert!(out.contains("if self.a {"));
1220 assert!(out.contains("} else {"));
1221 assert!(out.contains("div().child(\"B\")"));
1222 }
1223
1224 #[test]
1225 fn test_if_elseif_else_chain() {
1226 let src = "<Stack><If condition=\"{state.x}\"><Text>X</Text></If><ElseIf condition=\"{state.y}\"><Text>Y</Text></ElseIf><Else><Text>Z</Text></Else></Stack>";
1227 let out = generate_from(src);
1228 assert!(out.contains("if self.x {"));
1229 assert!(out.contains("else if self.y {"));
1230 assert!(out.contains("else {"));
1231 }
1232
1233 #[test]
1234 fn test_for_generates_iteration() {
1235 let out = generate_from("<For each=\"{state.items}\" let=\"item\"><Text>{item.name}</Text></For>");
1236 assert!(out.contains("self.items.iter().map(|item| {"));
1237 assert!(out.contains("item.name"));
1238 }
1239
1240 #[test]
1241 fn test_for_with_index() {
1242 let out = generate_from("<For each=\"{state.items}\" let=\"item\" index=\"i\"><Text>{item.name}</Text></For>");
1243 assert!(out.contains(".enumerate()"));
1244 assert!(out.contains("|(i, item)|"));
1245 }
1246
1247 #[test]
1248 fn test_fragment_generates_div() {
1249 let out = generate_from("<Fragment><Text>A</Text><Text>B</Text></Fragment>");
1250 assert!(out.contains("div()"));
1251 }
1252
1253 #[test]
1254 fn test_slot_in_stateless_component() {
1255 let doc = parse("<Stack><Slot /></Stack>").unwrap();
1256 let out = generate_with_imports(&doc, 0, &[], Some("Card"), None, None, &[], None, &[]);
1257 assert!(out.contains("children: impl IntoElement"));
1258 assert!(out.contains("children"));
1259 }
1260
1261 #[test]
1262 fn test_if_in_stack() {
1263 let src = "<Stack><If condition=\"{state.flag}\"><Text>Yes</Text></If><Text>Always</Text></Stack>";
1264 let out = generate_from(src);
1265 assert!(out.contains("if self.flag {"));
1266 assert!(out.contains("\"Always\""));
1267 }
1268
1269 #[test]
1270 fn test_for_in_stack() {
1271 let src = "<Stack><For each=\"{state.items}\" let=\"item\"><Text>{item}</Text></For></Stack>";
1272 let out = generate_from(src);
1273 assert!(out.contains("self.items.iter().map(|item| {"));
1274 assert!(out.contains(".children("));
1275 }
1276
1277 #[test]
1278 fn test_fragment_nested_in_stack() {
1279 let src = "<Stack><Fragment><Text>A</Text><Text>B</Text></Fragment></Stack>";
1280 let out = generate_from(src);
1281 assert!(out.contains("div()"));
1282 assert!(out.contains(".child(\"A\")"));
1283 assert!(out.contains(".child(\"B\")"));
1284 }
1285}