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, &[], 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 style_cascade: Option<&fncc_styles::Stylesheet>,
36 style_theme: Option<&str>,
37) -> String {
38 let mut out = String::new();
39 let state_type = resolved_state_type.or(doc.state_type.as_deref());
40 let has_state = state_type.is_some();
41 let has_slot = parser::has_slot(&doc.root);
42
43 if let Some(ref fm) = doc.frontmatter {
44 out.push_str(fm);
45 out.push('\n');
46 }
47
48 let commands = parser::collect_commands(&doc.root);
49 if !commands.is_empty() {
50 out.push_str("#[allow(unused)]\n");
51 out.push_str(&format!("fn _fncc_validate_{file_id}() {{\n"));
52 for cmd in &commands {
53 let trampoline = format!("__fncc_cmd_{cmd}");
54 out.push_str(&format!(" let _ = {trampoline};\n"));
55 }
56 out.push_str("}\n\n");
57 }
58
59 if has_state {
60 generate_stateful(
61 doc,
62 &mut out,
63 imports,
64 state_type,
65 import_props,
66 prop_fields,
67 import_has_slots,
68 style_cascade,
69 style_theme,
70 );
71 } else {
72 generate_stateless(
73 doc,
74 &mut out,
75 imports,
76 component_name,
77 props_type,
78 import_props,
79 prop_fields,
80 import_has_slots,
81 has_slot,
82 style_cascade,
83 style_theme,
84 );
85 }
86
87 out
88}
89
90fn generate_stateless(
91 doc: &Document,
92 out: &mut String,
93 imports: &[ResolvedImport],
94 component_name: Option<&str>,
95 props_type: Option<&str>,
96 import_props: &[(&str, Option<&str>)],
97 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
98 import_has_slots: &[(&str, bool)],
99 has_slot: bool,
100 style_cascade: Option<&fncc_styles::Stylesheet>,
101 style_theme: Option<&str>,
102) {
103 let name = component_name.unwrap_or(&doc.root.name);
104 let fn_name = format!("render_{}", to_snake_case(name));
105
106 let params = match (props_type, has_slot) {
107 (Some(pt), true) => format!("props: &{pt}, children: impl IntoElement"),
108 (Some(pt), false) => format!("props: &{pt}"),
109 (None, true) => "children: impl IntoElement".to_string(),
110 (None, false) => String::new(),
111 };
112
113 let sig = if params.is_empty() {
114 format!("pub fn {fn_name}() -> impl IntoElement {{\n")
115 } else {
116 format!("pub fn {fn_name}({params}) -> impl IntoElement {{\n")
117 };
118 out.push_str(&sig);
119 out.push_str(&generate_element(
120 &doc.root,
121 1,
122 false,
123 imports,
124 import_props,
125 prop_fields,
126 import_has_slots,
127 style_cascade,
128 style_theme,
129 ));
130 out.push('\n');
131 out.push_str("}\n");
132}
133
134fn generate_stateful(
135 doc: &Document,
136 out: &mut String,
137 imports: &[ResolvedImport],
138 state_type: Option<&str>,
139 import_props: &[(&str, Option<&str>)],
140 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
141 _import_has_slots: &[(&str, bool)],
142 style_cascade: Option<&fncc_styles::Stylesheet>,
143 style_theme: Option<&str>,
144) {
145 let state_type = state_type.unwrap_or("Self");
146
147 out.push_str(&format!("impl Render for {state_type} {{\n"));
148 out.push_str(" fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {\n");
149 out.push_str(" let handle = cx.entity().downgrade();\n");
150 out.push_str(&generate_element(
151 &doc.root,
152 2,
153 true,
154 imports,
155 import_props,
156 prop_fields,
157 _import_has_slots,
158 style_cascade,
159 style_theme,
160 ));
161 out.push_str("\n }\n");
162 out.push_str("}\n");
163}
164
165fn collect_if_chain(nodes: &[Node], start: usize) -> Vec<&Element> {
169 let mut branches = Vec::new();
170 for node in &nodes[start..] {
171 match node {
172 Node::Element(el) if el.name == "If" => {
173 if branches.is_empty() {
174 branches.push(el);
175 } else {
176 break; }
178 }
179 Node::Element(el) if el.name == "ElseIf" || el.name == "Else" => {
180 if branches.is_empty() {
181 break; }
183 branches.push(el);
184 }
185 _ => break,
186 }
187 }
188 branches
189}
190
191fn gen_if_expr(
194 branches: &[&Element],
195 stateful: bool,
196 imports: &[ResolvedImport],
197 import_props: &[(&str, Option<&str>)],
198 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
199 _import_has_slots: &[(&str, bool)],
200 style_cascade: Option<&fncc_styles::Stylesheet>,
201 style_theme: Option<&str>,
202) -> String {
203 let mut expr = String::new();
204 for (i, el) in branches.iter().enumerate() {
205 if i == 0 {
206 expr.push_str("if ");
207 } else if el.name == "Else" {
208 expr.push_str(" else ");
209 } else {
210 expr.push_str(" else if ");
211 }
212 if el.name != "Else" {
213 if let Some(cond) = parser::get_condition_attr(el) {
214 let e = interpolation_expr(cond);
215 expr.push_str(&e);
216 expr.push(' ');
217 } else {
218 expr.push_str("compile_error!(\"<");
219 expr.push_str(&el.name);
220 expr.push_str("> element is missing `condition` attribute\") ");
221 }
222 }
223 expr.push_str("{ ");
224 match &el.children[..] {
225 [] => expr.push_str("div()"),
226 [single] => match single {
227 Node::Text(t) => expr.push_str(&format!("div().child({:?})", t)),
228 Node::Interpolation(interp) => {
229 let e = interpolation_expr(interp);
230 expr.push_str(&format!("div().child({e}.to_string())"));
231 }
232 Node::Element(child_el) => {
233 let child_code = generate_element(
234 child_el,
235 0,
236 stateful,
237 imports,
238 import_props,
239 prop_fields,
240 _import_has_slots,
241 style_cascade,
242 style_theme,
243 );
244 expr.push_str(&format!("div().child({})", clean_inline(&child_code)));
245 }
246 },
247 children => {
248 expr.push_str("div()");
249 for child in children {
250 match child {
251 Node::Text(t) => expr.push_str(&format!(".child({:?})", t)),
252 Node::Interpolation(interp) => {
253 let e = interpolation_expr(interp);
254 expr.push_str(&format!(".child({e}.to_string())"));
255 }
256 Node::Element(child_el) => {
257 let child_code = generate_element(
258 child_el,
259 0,
260 stateful,
261 imports,
262 import_props,
263 prop_fields,
264 _import_has_slots,
265 style_cascade,
266 style_theme,
267 );
268 expr.push_str(&format!(".child({})", clean_inline(&child_code)));
269 }
270 }
271 }
272 }
273 }
274 expr.push_str(" }");
275 }
276 if branches.last().map(|b| b.name.as_str()) != Some("Else") {
278 expr.push_str(" else { div() }");
279 }
280 expr
281}
282
283fn gen_for_expr(
285 el: &Element,
286 stateful: bool,
287 imports: &[ResolvedImport],
288 import_props: &[(&str, Option<&str>)],
289 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
290 _import_has_slots: &[(&str, bool)],
291 style_cascade: Option<&fncc_styles::Stylesheet>,
292 style_theme: Option<&str>,
293) -> String {
294 let each = match parser::get_each_attr(el) {
295 Some(e) => e,
296 None => {
297 return "compile_error!(\"<For> element is missing required `each` attribute\")".to_string();
298 }
299 };
300 let let_var = parser::get_let_attr(el).unwrap_or("item");
301 let index_var = parser::get_index_attr(el);
302 let iter_expr = interpolation_expr(each);
303
304 let mut body = String::new();
305 match &el.children[..] {
306 [] => body.push_str("div()"),
307 [single] => match single {
308 Node::Text(t) => body.push_str(&format!("div().child({:?})", t)),
309 Node::Interpolation(interp) => {
310 let e = interpolation_expr(interp);
311 body.push_str(&format!("div().child({e}.to_string())"));
312 }
313 Node::Element(child_el) => {
314 let child_code = generate_element(
315 child_el,
316 1,
317 stateful,
318 imports,
319 import_props,
320 prop_fields,
321 _import_has_slots,
322 style_cascade,
323 style_theme,
324 );
325 body.push_str(&child_code);
326 }
327 },
328 children => {
329 body.push_str("div()");
330 for child in children {
331 match child {
332 Node::Text(t) => body.push_str(&format!(".child({:?})", t)),
333 Node::Interpolation(interp) => {
334 let e = interpolation_expr(interp);
335 body.push_str(&format!(".child({e}.to_string())"));
336 }
337 Node::Element(child_el) => {
338 let child_code = generate_element(
339 child_el,
340 1,
341 stateful,
342 imports,
343 import_props,
344 prop_fields,
345 _import_has_slots,
346 style_cascade,
347 style_theme,
348 );
349 body.push_str(&format!(".child({})", clean_inline(&child_code)));
350 }
351 }
352 }
353 }
354 }
355
356 let var_names: Vec<&str> = index_var.iter().chain(std::iter::once(&let_var)).copied().collect();
360 for var in &var_names {
361 body = replace_self_prefix(&body, var);
362 }
363
364 if let Some(index) = index_var {
365 format!("{iter_expr}.iter().enumerate().map(|({index}, {let_var})| {{\n {body}\n}})")
366 } else {
367 format!("{iter_expr}.iter().map(|{let_var}| {{\n {body}\n}})")
368 }
369}
370
371fn generate_element(
372 el: &Element,
373 depth: usize,
374 stateful: bool,
375 imports: &[ResolvedImport],
376 import_props: &[(&str, Option<&str>)],
377 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
378 _import_has_slots: &[(&str, bool)],
379 style_cascade: Option<&fncc_styles::Stylesheet>,
380 style_theme: Option<&str>,
381) -> String {
382 let indent = " ".repeat(depth);
383
384 let code = match el.name.as_str() {
385 "Stack" => gen_stack(
386 el,
387 &indent,
388 depth,
389 stateful,
390 imports,
391 import_props,
392 prop_fields,
393 _import_has_slots,
394 style_cascade,
395 style_theme,
396 ),
397 "Text" => gen_text(
398 el,
399 &indent,
400 depth,
401 stateful,
402 imports,
403 import_props,
404 prop_fields,
405 _import_has_slots,
406 style_cascade,
407 style_theme,
408 ),
409 "Button" => gen_button(
410 el,
411 &indent,
412 depth,
413 stateful,
414 imports,
415 import_props,
416 prop_fields,
417 _import_has_slots,
418 style_cascade,
419 style_theme,
420 ),
421 "Fragment" => gen_fragment(
422 el,
423 &indent,
424 depth,
425 stateful,
426 imports,
427 import_props,
428 prop_fields,
429 _import_has_slots,
430 style_cascade,
431 style_theme,
432 ),
433 "For" => format!(
434 "div().children({})",
435 gen_for_expr(
436 el,
437 stateful,
438 imports,
439 import_props,
440 prop_fields,
441 _import_has_slots,
442 style_cascade,
443 style_theme
444 )
445 ),
446 "If" | "ElseIf" | "Else" => {
447 let branches = [el];
448 gen_if_expr(
449 &branches,
450 stateful,
451 imports,
452 import_props,
453 prop_fields,
454 _import_has_slots,
455 style_cascade,
456 style_theme,
457 )
458 }
459 "Slot" => "children".to_string(),
460 _ => {
461 if let Some(render_fn) = imports
462 .iter()
463 .find(|(name, fn_name)| name == &el.name && !fn_name.is_empty())
464 .map(|(_, fn_name)| *fn_name)
465 {
466 gen_import_call(
467 el,
468 &indent,
469 render_fn,
470 import_props,
471 prop_fields,
472 _import_has_slots,
473 stateful,
474 imports,
475 depth,
476 style_cascade,
477 style_theme,
478 )
479 } else {
480 gen_fallback(
481 el,
482 &indent,
483 depth,
484 stateful,
485 imports,
486 import_props,
487 prop_fields,
488 _import_has_slots,
489 style_cascade,
490 style_theme,
491 )
492 }
493 }
494 };
495
496 match inject_element_styles(&code, el, style_cascade, style_theme) {
497 Ok(code) => code,
498 Err(e) => format!("compile_error!({e:?})"),
499 }
500}
501
502fn gen_import_call(
503 el: &Element,
504 indent: &str,
505 render_fn: &str,
506 import_props: &[(&str, Option<&str>)],
507 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
508 _import_has_slots: &[(&str, bool)],
509 stateful: bool,
510 imports: &[ResolvedImport],
511 depth: usize,
512 style_cascade: Option<&fncc_styles::Stylesheet>,
513 style_theme: Option<&str>,
514) -> String {
515 let props_type = import_props.iter().find(|(n, _)| n == &el.name).and_then(|(_, p)| *p);
516 let has_slot = _import_has_slots
517 .iter()
518 .find(|(n, _)| n == &el.name)
519 .map(|(_, h)| *h)
520 .unwrap_or(false);
521
522 let children_expr = if has_slot {
523 if el.children.is_empty() {
524 "div()".to_string()
525 } else {
526 let child_code = generate_children_code(
527 &el.children,
528 indent,
529 depth + 1,
530 stateful,
531 imports,
532 import_props,
533 prop_fields,
534 _import_has_slots,
535 style_cascade,
536 style_theme,
537 );
538 clean_inline(&format!("div(){child_code}"))
539 }
540 } else {
541 String::new()
542 };
543
544 if let Some(pt) = props_type {
545 let mut struct_fields = String::new();
546 if let Some(fields) = prop_fields.and_then(|m| m.get(pt)) {
547 for f in fields {
548 if let Some((_, attr_val)) = el.attrs.iter().find(|(n, _)| n == &f.name) {
549 let v = match attr_val {
550 AttrValue::String(s) => format!("{:?}.into()", s),
551 AttrValue::Interpolation(expr) => format!("{}.into()", interpolation_expr(expr)),
552 };
553 struct_fields.push_str(&format!("\n{indent} {}: {},", f.name, v));
554 } else if f.is_optional {
555 struct_fields.push_str(&format!("\n{indent} {}: None,", f.name));
556 }
557 }
558 } else {
559 for (attr_name, attr_val) in &el.attrs {
560 if attr_name == "class" || attr_name == "style" {
561 continue;
562 }
563 let v = match attr_val {
564 AttrValue::String(s) => format!("{:?}.into()", s),
565 AttrValue::Interpolation(expr) => format!("{}.into()", interpolation_expr(expr)),
566 };
567 struct_fields.push_str(&format!("\n{indent} {attr_name}: {v},"));
568 }
569 }
570 if has_slot {
571 format!("{indent}{render_fn}(&{pt} {{ {struct_fields}\n{indent} }}, {children_expr})")
572 } else {
573 format!("{indent}{render_fn}(&{pt} {{ {struct_fields}\n{indent} }})")
574 }
575 } else if has_slot {
576 format!("{indent}{render_fn}({children_expr})")
577 } else {
578 format!("{indent}{render_fn}()")
579 }
580}
581
582fn inject_element_styles(
583 code: &str,
584 el: &Element,
585 cascade: Option<&fncc_styles::Stylesheet>,
586 theme: Option<&str>,
587) -> Result<String, String> {
588 let cascade = match cascade {
589 Some(c) => c,
590 None => return Ok(code.to_string()),
591 };
592
593 let classes: Vec<String> = el
594 .attrs
595 .iter()
596 .filter(|(name, _)| name == "class")
597 .filter_map(|(_, val)| match val {
598 AttrValue::String(s) => Some(s.split_whitespace().map(|s| s.to_string()).collect::<Vec<_>>()),
599 AttrValue::Interpolation(_) => None,
600 })
601 .flatten()
602 .collect();
603
604 let inline_style = el
605 .attrs
606 .iter()
607 .find(|(name, _)| name == "style")
608 .and_then(|(_, val)| match val {
609 AttrValue::String(s) => Some(s.clone()),
610 AttrValue::Interpolation(_) => None,
611 });
612
613 if classes.is_empty() && inline_style.is_none() {
614 return Ok(code.to_string());
615 }
616
617 let calls = fncc_styles::resolve(&classes, inline_style.as_deref(), cascade, theme)?;
618
619 if calls.is_empty() {
620 return Ok(code.to_string());
621 }
622
623 let indent = code
625 .lines()
626 .rev()
627 .find(|l| !l.trim().is_empty())
628 .map(|l| l.len() - l.trim_start().len())
629 .unwrap_or(4);
630 let indent_str = " ".repeat(indent);
631
632 let style_code: String = calls.iter().map(|c| format!("\n{indent_str}.{}", c.code)).collect();
633
634 let mut insert_pos = None;
636 for (i, line) in code.lines().enumerate() {
637 let trimmed = line.trim_start();
638 if trimmed.starts_with(".child(") || trimmed.starts_with(".children(") {
639 insert_pos = Some(
640 code.lines()
641 .take(i)
642 .map(|l| l.len() + 1)
643 .sum::<usize>()
644 .saturating_sub(1),
645 );
646 break;
647 }
648 }
649
650 if let Some(pos) = insert_pos {
651 Ok(format!("{}{}{}", &code[..pos], style_code, &code[pos..]))
652 } else {
653 Ok(format!("{}{}", code.trim_end(), style_code))
654 }
655}
656
657fn gen_fragment(
660 el: &Element,
661 indent: &str,
662 depth: usize,
663 stateful: bool,
664 imports: &[ResolvedImport],
665 import_props: &[(&str, Option<&str>)],
666 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
667 _import_has_slots: &[(&str, bool)],
668 style_cascade: Option<&fncc_styles::Stylesheet>,
669 style_theme: Option<&str>,
670) -> String {
671 let children_code = generate_children_code(
672 &el.children,
673 indent,
674 depth,
675 stateful,
676 imports,
677 import_props,
678 prop_fields,
679 _import_has_slots,
680 style_cascade,
681 style_theme,
682 );
683 format!("{indent}div()\n{children_code}").trim_end().to_string()
684}
685
686fn generate_children_code(
688 children: &[Node],
689 indent: &str,
690 depth: usize,
691 stateful: bool,
692 imports: &[ResolvedImport],
693 import_props: &[(&str, Option<&str>)],
694 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
695 _import_has_slots: &[(&str, bool)],
696 style_cascade: Option<&fncc_styles::Stylesheet>,
697 style_theme: Option<&str>,
698) -> String {
699 let mut out = String::new();
700 let mut i = 0;
701 while i < children.len() {
702 match &children[i] {
703 Node::Element(el) if el.name == "If" => {
704 let chain = collect_if_chain(children, i);
705 let expr = gen_if_expr(
706 &chain,
707 stateful,
708 imports,
709 import_props,
710 prop_fields,
711 _import_has_slots,
712 style_cascade,
713 style_theme,
714 );
715 out.push_str(&format!("{indent} .child(\n"));
716 out.push_str(&format!("{indent} {expr}\n"));
717 out.push_str(&format!("{indent} )\n"));
718 i += chain.len();
719 }
720 Node::Element(el) if el.name == "For" => {
721 let for_code = gen_for_expr(
722 el,
723 stateful,
724 imports,
725 import_props,
726 prop_fields,
727 _import_has_slots,
728 style_cascade,
729 style_theme,
730 );
731 out.push_str(&format!("{indent} .children(\n"));
732 out.push_str(&format!("{indent} {for_code}\n"));
733 out.push_str(&format!("{indent} )\n"));
734 i += 1;
735 }
736 Node::Element(el) if el.name == "Fragment" => {
737 let frag_code = generate_children_code(
738 &el.children,
739 indent,
740 depth,
741 stateful,
742 imports,
743 import_props,
744 prop_fields,
745 _import_has_slots,
746 style_cascade,
747 style_theme,
748 );
749 out.push_str(&frag_code);
750 i += 1;
751 }
752 _ => {
753 out.push_str(&format!("{indent} .child(\n"));
754 match &children[i] {
755 Node::Element(child_el) => {
756 out.push_str(&generate_element(
757 child_el,
758 depth + 2,
759 stateful,
760 imports,
761 import_props,
762 prop_fields,
763 _import_has_slots,
764 style_cascade,
765 style_theme,
766 ));
767 }
768 Node::Text(t) => {
769 out.push_str(&format!("{indent} {:?}", t));
770 }
771 Node::Interpolation(expr) => {
772 let e = interpolation_expr(expr);
773 out.push_str(&format!("{indent} {e}.to_string()"));
774 }
775 }
776 out.push('\n');
777 out.push_str(&format!("{indent} )\n"));
778 i += 1;
779 }
780 }
781 }
782 out
783}
784
785fn gen_stack(
786 el: &Element,
787 indent: &str,
788 depth: usize,
789 stateful: bool,
790 imports: &[ResolvedImport],
791 import_props: &[(&str, Option<&str>)],
792 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
793 _import_has_slots: &[(&str, bool)],
794 style_cascade: Option<&fncc_styles::Stylesheet>,
795 style_theme: Option<&str>,
796) -> String {
797 let mut out = format!("{indent}div()\n");
798
799 let mut is_vertical = false;
800 for (key, val) in &el.attrs {
801 match key.as_str() {
802 "direction" if val.as_str() == "vertical" => is_vertical = true,
803 "gap" => {
804 let v = val.as_str();
805 if let Ok(n) = v.parse::<f64>() {
806 out.push_str(&format!("{indent} .gap(px({}))\n", float_lit(n)));
807 }
808 }
809 _ => {}
810 }
811 }
812
813 if is_vertical {
814 out.push_str(&format!("{indent} .flex()\n{indent} .flex_col()\n"));
815 } else {
816 out.push_str(&format!("{indent} .flex()\n"));
817 }
818
819 out.push_str(&generate_children_code(
820 &el.children,
821 indent,
822 depth,
823 stateful,
824 imports,
825 import_props,
826 prop_fields,
827 _import_has_slots,
828 style_cascade,
829 style_theme,
830 ));
831
832 out.trim_end().to_string()
833}
834
835fn gen_text(
836 el: &Element,
837 indent: &str,
838 depth: usize,
839 stateful: bool,
840 imports: &[ResolvedImport],
841 import_props: &[(&str, Option<&str>)],
842 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
843 _import_has_slots: &[(&str, bool)],
844 style_cascade: Option<&fncc_styles::Stylesheet>,
845 style_theme: Option<&str>,
846) -> String {
847 let mut out = format!("{indent}div()\n");
848
849 for (key, val) in &el.attrs {
850 if key.as_str() == "size" {
851 let v = val.as_str();
852 let ts = match v {
853 "xs" => "text_xs()",
854 "sm" => "text_sm()",
855 "base" => "text_base()",
856 "lg" => "text_lg()",
857 "xl" => "text_xl()",
858 "2xl" | "xxl" => "text_2xl()",
859 "3xl" => "text_3xl()",
860 _ => "text_base()",
861 };
862 out.push_str(&format!("{indent} .{ts}\n"));
863 }
864 }
865
866 match &el.children[..] {
867 [Node::Text(t)] => {
868 out.push_str(&format!("{indent} .child({:?})", t));
869 }
870 [Node::Interpolation(expr)] => {
871 let e = interpolation_expr(expr);
872 out.push_str(&format!("{indent} .child({e}.to_string())"));
873 }
874 children => {
875 out.push_str(&generate_children_code(
876 children,
877 indent,
878 depth,
879 stateful,
880 imports,
881 import_props,
882 prop_fields,
883 _import_has_slots,
884 style_cascade,
885 style_theme,
886 ));
887 }
888 }
889
890 out.trim_end().to_string()
891}
892
893fn gen_button(
894 el: &Element,
895 indent: &str,
896 depth: usize,
897 stateful: bool,
898 imports: &[ResolvedImport],
899 import_props: &[(&str, Option<&str>)],
900 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
901 _import_has_slots: &[(&str, bool)],
902 style_cascade: Option<&fncc_styles::Stylesheet>,
903 style_theme: Option<&str>,
904) -> String {
905 let mut out = format!("{indent}div()\n");
906
907 let btn_id = match &el.children[..] {
908 [Node::Text(t)] => t.clone(),
909 _ => format!("button_{depth}"),
910 };
911 out.push_str(&format!("{indent} .id({:?})\n", btn_id));
912 out.push_str(&format!("{indent} .cursor_pointer()\n"));
913
914 for (key, val) in &el.attrs {
915 if key.as_str() == "onclick" {
916 let handler = val.as_str();
917 let trampoline = format!("__fncc_cmd_{handler}");
918 if stateful {
919 out.push_str(&format!("{indent} .on_click({{\n"));
921 out.push_str(&format!("{indent} let handle = handle.clone();\n"));
922 out.push_str(&format!("{indent} move |_, _, cx| {{\n"));
923 out.push_str(&format!("{indent} handle.update(cx, |this, cx| {{\n"));
924 out.push_str(&format!("{indent} {trampoline}(this, cx);\n"));
925 out.push_str(&format!("{indent} }}).ok();\n"));
926 out.push_str(&format!("{indent} }}\n"));
927 out.push_str(&format!("{indent} }})\n"));
928 } else {
929 out.push_str(&format!("{indent} .on_click({trampoline})\n"));
930 }
931 }
932 }
933
934 match &el.children[..] {
935 [Node::Text(t)] => out.push_str(&format!("{indent} .child({:?})", t)),
936 [Node::Interpolation(expr)] => {
937 let e = interpolation_expr(expr);
938 out.push_str(&format!("{indent} .child({e}.to_string())"));
939 }
940 children => {
941 out.push_str(&generate_children_code(
942 children,
943 indent,
944 depth,
945 stateful,
946 imports,
947 import_props,
948 prop_fields,
949 _import_has_slots,
950 style_cascade,
951 style_theme,
952 ));
953 }
954 }
955
956 out.trim_end().to_string()
957}
958
959fn gen_fallback(
960 el: &Element,
961 indent: &str,
962 depth: usize,
963 stateful: bool,
964 imports: &[ResolvedImport],
965 import_props: &[(&str, Option<&str>)],
966 prop_fields: Option<&HashMap<String, Vec<PropField>>>,
967 _import_has_slots: &[(&str, bool)],
968 style_cascade: Option<&fncc_styles::Stylesheet>,
969 style_theme: Option<&str>,
970) -> String {
971 let mut out = format!("{indent}div()\n");
972 for (key, val) in &el.attrs {
973 if key == "class" || key == "style" {
974 continue;
975 }
976 let v = val.as_str();
977 out.push_str(&format!("{indent} .attr({:?}, {:?})\n", key, v));
978 }
979 out.push_str(&generate_children_code(
980 &el.children,
981 indent,
982 depth,
983 stateful,
984 imports,
985 import_props,
986 prop_fields,
987 _import_has_slots,
988 style_cascade,
989 style_theme,
990 ));
991 out.trim_end().to_string()
992}
993
994fn clean_inline(s: &str) -> String {
996 let collapsed: String = s.split_whitespace().collect::<Vec<_>>().join(" ");
997 collapsed.replace(") .", ").")
998}
999
1000fn float_lit(n: f64) -> String {
1003 if n.fract() == 0.0 {
1004 format!("{n}.")
1005 } else {
1006 format!("{n}")
1007 }
1008}
1009
1010fn interpolation_expr(expr: &str) -> String {
1015 let trimmed = expr.trim();
1016 if let Some(field) = trimmed.strip_prefix("state.") {
1017 format!("self.{field}")
1018 } else if trimmed.starts_with("props.") {
1019 trimmed.to_string()
1020 } else {
1021 format!("self.{trimmed}")
1022 }
1023}
1024
1025fn replace_self_prefix(s: &str, var: &str) -> String {
1029 let pattern = format!("self.{var}");
1030 let mut out = String::new();
1031 let mut last = 0;
1032 for (idx, _) in s.match_indices(&pattern) {
1033 out.push_str(&s[last..idx]);
1034 let end = idx + pattern.len();
1035 let keep = if end < s.len() {
1036 let b = s.as_bytes()[end];
1037 b.is_ascii_alphanumeric() || b == b'_'
1038 } else {
1039 false
1040 };
1041 if keep {
1042 out.push_str(&pattern);
1043 last = idx + pattern.len();
1044 } else {
1045 out.push_str(var);
1046 last = end;
1047 }
1048 }
1049 out.push_str(&s[last..]);
1050 out
1051}
1052
1053pub(crate) fn to_snake_case(name: &str) -> String {
1054 let mut result = String::new();
1055 for (i, ch) in name.chars().enumerate() {
1056 if ch.is_uppercase() {
1057 if i > 0 {
1058 result.push('_');
1059 }
1060 for c in ch.to_lowercase() {
1061 result.push(c);
1062 }
1063 } else {
1064 result.push(ch);
1065 }
1066 }
1067 result
1068}
1069
1070#[cfg(test)]
1071mod tests {
1072 use super::*;
1073 use crate::parser::parse;
1074
1075 fn cascade_none() -> Option<fncc_styles::Stylesheet> {
1076 None
1077 }
1078
1079 fn generate_from(source: &str) -> String {
1080 let doc = parse(source).unwrap();
1081 generate(&doc)
1082 }
1083
1084 #[test]
1087 fn test_generates_stateless_render_function() {
1088 let out = generate_from("<MyComp></MyComp>");
1089 assert!(out.contains("pub fn render_my_comp() -> impl IntoElement {"));
1090 assert!(out.contains("div()"));
1091 }
1092
1093 #[test]
1094 fn test_generates_stateful_render_impl() {
1095 let src = "---\n@state CounterState\n---\n<App></App>";
1096 let out = generate_from(src);
1097 assert!(out.contains("impl Render for CounterState {"));
1098 assert!(
1099 out.contains("fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {")
1100 );
1101 }
1102
1103 #[test]
1104 fn test_frontmatter_is_preserved_in_output() {
1105 let src = "---\nuse crate::prelude::*;\n---\n<App></App>";
1106 let out = generate_from(src);
1107 assert!(out.contains("use crate::prelude::*;"));
1108 }
1109
1110 #[test]
1111 fn test_stack_with_direction_vertical_generates_flex_col() {
1112 let out = generate_from("<Stack direction=\"vertical\"></Stack>");
1113 assert!(out.contains(".flex()"));
1114 assert!(out.contains(".flex_col()"));
1115 }
1116
1117 #[test]
1118 fn test_stack_with_direction_horizontal_generates_flex_only() {
1119 let out = generate_from("<Stack direction=\"horizontal\"></Stack>");
1120 assert!(out.contains(".flex()"));
1121 assert!(!out.contains(".flex_col()"));
1122 }
1123
1124 #[test]
1125 fn test_stack_with_gap_generates_px_value() {
1126 let out = generate_from("<Stack gap=\"12\"></Stack>");
1127 assert!(out.contains(".gap(px(12.))"));
1128 }
1129
1130 #[test]
1131 fn test_text_with_size_xl() {
1132 let out = generate_from("<Text size=\"xl\">hello</Text>");
1133 assert!(out.contains(".text_xl()"));
1134 assert!(out.contains(".child(\"hello\")"));
1135 }
1136
1137 #[test]
1138 fn test_text_with_size_unknown_falls_back_to_base() {
1139 let out = generate_from("<Text size=\"huge\">text</Text>");
1140 assert!(out.contains(".text_base()"));
1141 }
1142
1143 #[test]
1144 fn test_button_with_text_child() {
1145 let out = generate_from("<Button onclick=\"handle_click\">Click</Button>");
1146 assert!(out.contains(".id(\"Click\")"));
1147 assert!(out.contains(".cursor_pointer()"));
1148 assert!(out.contains(".child(\"Click\")"));
1149 }
1150
1151 #[test]
1152 fn test_stateful_button_with_onclick_generates_entity_pattern() {
1153 let src = "---\n@state AppState\n---\n<Button onclick=\"inc\">+1</Button>";
1154 let out = generate_from(src);
1155 assert!(out.contains("let handle = handle.clone();"));
1156 assert!(out.contains("handle.update(cx, |this, cx| {"));
1157 assert!(out.contains("__fncc_cmd_inc(this, cx);"));
1158 }
1159
1160 #[test]
1161 fn test_stateless_button_with_onclick_generates_direct_call() {
1162 let out = generate_from("<Button onclick=\"log_click\">Go</Button>");
1163 assert!(out.contains(".on_click(__fncc_cmd_log_click)"));
1164 }
1165
1166 #[test]
1169 fn test_empty_element_children() {
1170 let out = generate_from("<Div></Div>");
1171 assert!(out.contains("div()"));
1172 }
1173
1174 #[test]
1175 fn test_unknown_element_falls_back_to_div_with_attrs() {
1176 let out = generate_from("<CustomEl foo=\"bar\">content</CustomEl>");
1177 assert!(out.contains(".attr(\"foo\", \"bar\")"));
1178 assert!(out.contains(".child("));
1179 assert!(out.contains("\"content\""));
1180 }
1181
1182 #[test]
1183 fn test_interpolation_in_text_content_generates_format() {
1184 let out = generate_from("---\n@state S\n---\n<Text>{state.msg}</Text>");
1185 assert!(out.contains("self.msg.to_string()"));
1186 assert!(!out.contains("self.state.msg"));
1187 }
1188
1189 #[test]
1190 fn test_interpolation_strips_state_prefix() {
1191 let out = generate_from("<Text>{state.count}</Text>");
1192 assert!(out.contains("self.count.to_string()"));
1194 }
1195
1196 #[test]
1197 fn test_multiple_commands_collected_in_validation_fn() {
1198 let src = "<Stack><Button onclick=\"a\">A</Button><Button onclick=\"b\">B</Button></Stack>";
1199 let out = generate_from(src);
1200 assert!(out.contains("fn _fncc_validate_"));
1201 assert!(out.contains("__fncc_cmd_a"));
1202 assert!(out.contains("__fncc_cmd_b"));
1203 }
1204
1205 #[test]
1208 fn test_generated_code_contains_no_markdown_or_template_leftovers() {
1209 let out = generate_from("<Text>hello</Text>");
1210 assert!(!out.contains("{{"));
1211 assert!(!out.contains("{state."));
1212 assert!(!out.contains("__fncc_cmd_") || out.contains("__fncc_cmd_"));
1213 }
1214
1215 #[test]
1216 fn test_generated_function_name_follows_snake_case() {
1217 let out = generate_from("<HTMLParser></HTMLParser>");
1218 assert!(out.contains("render_h_t_m_l_parser") || out.contains("render_html_parser"));
1219 }
1220
1221 #[test]
1224 fn test_regression_gap_with_decimal_does_not_produce_invalid_syntax() {
1225 let out = generate_from("<Stack gap=\"12.5\"></Stack>");
1226 assert!(out.contains(".gap(px(12.5))"), "got: {out}");
1227 }
1228
1229 #[test]
1230 fn test_regression_duplicate_button_ids_at_same_depth() {
1231 let src = "<Stack><Button>OK</Button><Button>OK</Button></Stack>";
1232 let out = generate_from(src);
1233 let id_count = out.matches(".id(\"OK\")").count();
1234 assert!(
1235 id_count <= 2,
1236 "expected at most 2 .id(\"OK\") occurrences, got {id_count}"
1237 );
1238 }
1239
1240 #[test]
1241 fn test_regression_multiple_calls_have_unique_validation_fn_names() {
1242 let doc = parse("<Button onclick=\"x\">X</Button>").unwrap();
1243 let a = generate_with_id(&doc, 0);
1244 let b = generate_with_id(&doc, 1);
1245 assert_ne!(a, b);
1246 assert!(a.contains("__fncc_cmd_x"));
1247 assert!(b.contains("__fncc_cmd_x"));
1248 }
1249
1250 #[test]
1251 fn test_regression_empty_gap_does_not_panic() {
1252 let out = generate_from("<Stack gap=\"\"></Stack>");
1253 assert!(!out.contains(".gap(") || out.contains(".gap(px(0.))"));
1255 }
1256
1257 #[test]
1258 fn test_regression_interpolation_without_state_prefix() {
1259 let out = generate_from("---\n@state S\n---\n<Text>{custom_expr}</Text>");
1260 assert!(out.contains("custom_expr") || out.contains("self.custom_expr"));
1262 }
1263
1264 #[test]
1265 fn test_regression_many_calls_do_not_panic() {
1266 for _ in 0..100 {
1267 generate_from("<Button onclick=\"f\">F</Button>");
1268 }
1269 }
1270
1271 #[test]
1274 fn test_imported_element_generates_render_call() {
1275 let doc = parse("<Stack><Header /></Stack>").unwrap();
1276 let imports: &[(&str, &str)] = &[("Header", "render_header")];
1277 let out = generate_with_imports(
1278 &doc,
1279 0,
1280 imports,
1281 None,
1282 None,
1283 None,
1284 &[],
1285 None,
1286 &[],
1287 cascade_none().as_ref(),
1288 None,
1289 );
1290 assert!(out.contains("render_header()"));
1291 }
1292
1293 #[test]
1294 fn test_imported_element_in_stateful_component() {
1295 let src = "---\n@state AppState\n---\n<Stack><Footer /></Stack>";
1296 let doc = parse(src).unwrap();
1297 let imports: &[(&str, &str)] = &[("Footer", "render_footer")];
1298 let out = generate_with_imports(
1299 &doc,
1300 0,
1301 imports,
1302 None,
1303 None,
1304 None,
1305 &[],
1306 None,
1307 &[],
1308 cascade_none().as_ref(),
1309 None,
1310 );
1311 assert!(out.contains("render_footer()"));
1312 }
1313
1314 #[test]
1315 fn test_gpui_import_falls_back_to_div() {
1316 let doc = parse("<Stack><TextInput /></Stack>").unwrap();
1317 let imports: &[(&str, &str)] = &[("TextInput", "")];
1318 let out = generate_with_imports(
1319 &doc,
1320 0,
1321 imports,
1322 None,
1323 None,
1324 None,
1325 &[],
1326 None,
1327 &[],
1328 cascade_none().as_ref(),
1329 None,
1330 );
1331 assert!(out.contains("div()"));
1333 }
1334
1335 #[test]
1336 fn test_builtin_takes_precedence_over_import() {
1337 let doc = parse("<Text>hello</Text>").unwrap();
1338 let imports: &[(&str, &str)] = &[("Text", "render_text")];
1339 let out = generate_with_imports(
1340 &doc,
1341 0,
1342 imports,
1343 None,
1344 None,
1345 None,
1346 &[],
1347 None,
1348 &[],
1349 cascade_none().as_ref(),
1350 None,
1351 );
1352 assert!(out.contains(".child(\"hello\")"));
1354 }
1355
1356 #[test]
1357 fn test_imported_element_with_custom_component_name() {
1358 let doc = parse("<Stack><MyHeader /></Stack>").unwrap();
1359 let imports: &[(&str, &str)] = &[("MyHeader", "render_header")];
1360 let out = generate_with_imports(
1361 &doc,
1362 0,
1363 imports,
1364 None,
1365 None,
1366 None,
1367 &[],
1368 None,
1369 &[],
1370 cascade_none().as_ref(),
1371 None,
1372 );
1373 assert!(out.contains("render_header()"));
1374 }
1375
1376 #[test]
1377 fn test_render_fn_name_uses_component_name_arg() {
1378 let doc = parse("<Text>hello</Text>").unwrap();
1379 let out = generate_with_imports(
1380 &doc,
1381 0,
1382 &[],
1383 Some("CustomWidget"),
1384 None,
1385 None,
1386 &[],
1387 None,
1388 &[],
1389 cascade_none().as_ref(),
1390 None,
1391 );
1392 assert!(out.contains("pub fn render_custom_widget()"));
1393 assert!(!out.contains("pub fn render_text()"));
1395 }
1396
1397 #[test]
1400 fn test_props_stateless_component_with_props_signature() {
1401 let doc = parse("<Text>{props.title}</Text>").unwrap();
1402 let out = generate_with_imports(
1403 &doc,
1404 0,
1405 &[],
1406 Some("Header"),
1407 None,
1408 Some("HeaderProps"),
1409 &[],
1410 None,
1411 &[],
1412 cascade_none().as_ref(),
1413 None,
1414 );
1415 assert!(out.contains("pub fn render_header(props: &HeaderProps) -> impl IntoElement {"));
1416 assert!(out.contains("props.title"));
1417 }
1418
1419 #[test]
1420 fn test_props_caller_generates_struct_construction() {
1421 let doc = parse("<Header title=\"Welcome\" />").unwrap();
1422 let imports: &[(&str, &str)] = &[("Header", "render_header")];
1423 let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
1424 let out = generate_with_imports(
1425 &doc,
1426 0,
1427 imports,
1428 None,
1429 None,
1430 None,
1431 import_props,
1432 None,
1433 &[],
1434 cascade_none().as_ref(),
1435 None,
1436 );
1437 assert!(out.contains("render_header(&HeaderProps {"));
1438 assert!(out.contains("title: \"Welcome\".into(),"));
1439 assert!(out.contains("})"));
1440 }
1441
1442 #[test]
1443 fn test_props_caller_multiple_attributes() {
1444 let doc = parse("<Header title=\"Hi\" subtitle=\"World\" />").unwrap();
1445 let imports: &[(&str, &str)] = &[("Header", "render_header")];
1446 let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
1447 let out = generate_with_imports(
1448 &doc,
1449 0,
1450 imports,
1451 None,
1452 None,
1453 None,
1454 import_props,
1455 None,
1456 &[],
1457 cascade_none().as_ref(),
1458 None,
1459 );
1460 assert!(out.contains("title: \"Hi\".into(),"));
1461 assert!(out.contains("subtitle: \"World\".into(),"));
1462 }
1463
1464 #[test]
1465 fn test_props_caller_option_field() {
1466 let doc = parse("<Header title=\"Hi\" />").unwrap();
1467 let imports: &[(&str, &str)] = &[("Header", "render_header")];
1468 let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
1469 let out = generate_with_imports(
1470 &doc,
1471 0,
1472 imports,
1473 None,
1474 None,
1475 None,
1476 import_props,
1477 None,
1478 &[],
1479 cascade_none().as_ref(),
1480 None,
1481 );
1482 assert!(out.contains("title: \"Hi\".into(),"));
1484 }
1485
1486 #[test]
1487 fn test_props_nested_element_with_props() {
1488 let doc = parse("<Stack><Header title=\"Nested\" /><Text>ok</Text></Stack>").unwrap();
1489 let imports: &[(&str, &str)] = &[("Header", "render_header")];
1490 let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
1491 let out = generate_with_imports(
1492 &doc,
1493 0,
1494 imports,
1495 None,
1496 None,
1497 None,
1498 import_props,
1499 None,
1500 &[],
1501 cascade_none().as_ref(),
1502 None,
1503 );
1504 assert!(out.contains("render_header(&HeaderProps {"));
1505 assert!(out.contains("title: \"Nested\".into(),"));
1506 }
1507
1508 #[test]
1509 fn test_props_mixed_components_with_and_without_props() {
1510 let doc = parse("<Stack><Header title=\"A\" /><Footer /></Stack>").unwrap();
1511 let imports: &[(&str, &str)] = &[("Header", "render_header"), ("Footer", "render_footer")];
1512 let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps")), ("Footer", None)];
1513 let out = generate_with_imports(
1514 &doc,
1515 0,
1516 imports,
1517 None,
1518 None,
1519 None,
1520 import_props,
1521 None,
1522 &[],
1523 cascade_none().as_ref(),
1524 None,
1525 );
1526 assert!(out.contains("render_header(&HeaderProps {"));
1527 assert!(out.contains("title: \"A\".into(),"));
1528 assert!(out.contains("render_footer()"));
1529 }
1530
1531 #[test]
1532 fn test_props_element_without_props_still_calls_directly() {
1533 let doc = parse("<Footer />").unwrap();
1534 let imports: &[(&str, &str)] = &[("Footer", "render_footer")];
1535 let import_props: &[(&str, Option<&str>)] = &[("Footer", None)];
1536 let out = generate_with_imports(
1537 &doc,
1538 0,
1539 imports,
1540 None,
1541 None,
1542 None,
1543 import_props,
1544 None,
1545 &[],
1546 cascade_none().as_ref(),
1547 None,
1548 );
1549 assert!(out.contains("render_footer()"));
1550 assert!(!out.contains("&"));
1551 }
1552
1553 #[test]
1554 fn test_props_self_closing_with_props() {
1555 let doc = parse("<Header title=\"SelfClose\" subtitle=\"X\" />").unwrap();
1556 let imports: &[(&str, &str)] = &[("Header", "render_header")];
1557 let import_props: &[(&str, Option<&str>)] = &[("Header", Some("HeaderProps"))];
1558 let out = generate_with_imports(
1559 &doc,
1560 0,
1561 imports,
1562 None,
1563 None,
1564 None,
1565 import_props,
1566 None,
1567 &[],
1568 cascade_none().as_ref(),
1569 None,
1570 );
1571 assert!(out.contains("title: \"SelfClose\".into(),"));
1572 assert!(out.contains("subtitle: \"X\".into(),"));
1573 }
1574
1575 #[test]
1578 fn test_if_generates_if_expression() {
1579 let out = generate_from("<If condition=\"{state.show}\"><Text>Hi</Text></If>");
1580 assert!(out.contains("if self.show {"));
1581 assert!(out.contains("div().child(div().child(\"Hi\"))"));
1582 assert!(out.contains("} else {"), "else branch should be present");
1583 assert!(out.contains("div() }"));
1584 }
1585
1586 #[test]
1587 fn test_if_else_chain() {
1588 let src = "<Stack><If condition=\"{state.a}\"><Text>A</Text></If><Else><Text>B</Text></Else></Stack>";
1589 let out = generate_from(src);
1590 assert!(out.contains("if self.a {"));
1591 assert!(out.contains("} else {"));
1592 assert!(out.contains("div().child(\"B\")"));
1593 }
1594
1595 #[test]
1596 fn test_if_elseif_else_chain() {
1597 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>";
1598 let out = generate_from(src);
1599 assert!(out.contains("if self.x {"));
1600 assert!(out.contains("else if self.y {"));
1601 assert!(out.contains("else {"));
1602 }
1603
1604 #[test]
1605 fn test_for_generates_iteration() {
1606 let out = generate_from("<For each=\"{state.items}\" let=\"item\"><Text>{item.name}</Text></For>");
1607 assert!(out.contains("self.items.iter().map(|item| {"));
1608 assert!(out.contains("item.name"));
1609 }
1610
1611 #[test]
1612 fn test_for_with_index() {
1613 let out = generate_from("<For each=\"{state.items}\" let=\"item\" index=\"i\"><Text>{item.name}</Text></For>");
1614 assert!(out.contains(".enumerate()"));
1615 assert!(out.contains("|(i, item)|"));
1616 }
1617
1618 #[test]
1619 fn test_fragment_generates_div() {
1620 let out = generate_from("<Fragment><Text>A</Text><Text>B</Text></Fragment>");
1621 assert!(out.contains("div()"));
1622 }
1623
1624 #[test]
1625 fn test_slot_in_stateless_component() {
1626 let doc = parse("<Stack><Slot /></Stack>").unwrap();
1627 let out = generate_with_imports(
1628 &doc,
1629 0,
1630 &[],
1631 Some("Card"),
1632 None,
1633 None,
1634 &[],
1635 None,
1636 &[],
1637 cascade_none().as_ref(),
1638 None,
1639 );
1640 assert!(out.contains("children: impl IntoElement"));
1641 assert!(out.contains("children"));
1642 }
1643
1644 #[test]
1645 fn test_if_in_stack() {
1646 let src = "<Stack><If condition=\"{state.flag}\"><Text>Yes</Text></If><Text>Always</Text></Stack>";
1647 let out = generate_from(src);
1648 assert!(out.contains("if self.flag {"));
1649 assert!(out.contains("\"Always\""));
1650 }
1651
1652 #[test]
1653 fn test_for_in_stack() {
1654 let src = "<Stack><For each=\"{state.items}\" let=\"item\"><Text>{item}</Text></For></Stack>";
1655 let out = generate_from(src);
1656 assert!(out.contains("self.items.iter().map(|item| {"));
1657 assert!(out.contains(".children("));
1658 }
1659
1660 #[test]
1661 fn test_fragment_nested_in_stack() {
1662 let src = "<Stack><Fragment><Text>A</Text><Text>B</Text></Fragment></Stack>";
1663 let out = generate_from(src);
1664 assert!(out.contains("div()"));
1665 assert!(out.contains(".child(\"A\")"));
1666 assert!(out.contains(".child(\"B\")"));
1667 }
1668
1669 #[test]
1672 fn test_style_class_injects_method_calls() {
1673 let doc = parse("<Text class=\"heading\">Title</Text>").unwrap();
1674 let mut ss = fncc_styles::Stylesheet::default();
1675 ss.rules.insert(
1676 "heading".into(),
1677 vec![("color".into(), "red".into()), ("font-size".into(), "24px".into())],
1678 );
1679 let out = generate_with_imports(&doc, 0, &[], None, None, None, &[], None, &[], Some(&ss), None);
1680 assert!(
1681 out.contains(".text_color(rgba(0xff0000ff))"),
1682 "missing text_color: {out}"
1683 );
1684 assert!(out.contains(".text_2xl()"), "missing text_2xl: {out}");
1685 }
1686
1687 #[test]
1688 fn test_style_inline_injects_method_calls() {
1689 let doc = parse("<Text style=\"color: blue; padding: 8px\">Hi</Text>").unwrap();
1690 let ss = fncc_styles::Stylesheet::default();
1691 let out = generate_with_imports(&doc, 0, &[], None, None, None, &[], None, &[], Some(&ss), None);
1692 assert!(out.contains(".text_color(rgba(0x0000ffff))"));
1693 assert!(out.contains(".p(px(8.))"));
1694 }
1695
1696 #[test]
1697 fn test_style_class_unknown_skipped() {
1698 let doc = parse("<Text class=\"nonexistent\">Hi</Text>").unwrap();
1699 let ss = fncc_styles::Stylesheet::default();
1700 let out = generate_with_imports(&doc, 0, &[], None, None, None, &[], None, &[], Some(&ss), None);
1701 assert!(out.contains(".child(\"Hi\")"));
1703 }
1704
1705 #[test]
1706 fn test_style_class_and_inline_merged() {
1707 let doc = parse("<Button class=\"big\" style=\"color: white\">Go</Button>").unwrap();
1708 let mut ss = fncc_styles::Stylesheet::default();
1709 ss.rules.insert(
1710 "big".into(),
1711 vec![("padding".into(), "16px".into()), ("font-size".into(), "20px".into())],
1712 );
1713 let out = generate_with_imports(&doc, 0, &[], None, None, None, &[], None, &[], Some(&ss), None);
1714 assert!(out.contains(".p(px(16.))"));
1715 assert!(out.contains(".text_xl()"));
1716 assert!(out.contains(".text_color(rgba(0xffffffff))"));
1717 }
1718}