Skip to main content

fncc_core/
codegen.rs

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
16/// Resolved import entry: (tag_name, render_fn_name)
17/// - For .fui component imports: ("Header", "render_header")
18/// - For gpui imports: ("TextInput", "") — registered but no special codegen
19pub type ResolvedImport<'a> = (&'a str, &'a str);
20
21/// Generate code for a document with a specific component name.
22/// `component_name` is used for the render function name (derived from the file stem).
23/// If `None`, falls back to the root element name (backward compat).
24/// `resolved_state_type` overrides the document's `@state` directive (used by semantic analysis).
25pub 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    // collect referenced command names for validation
47    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    // Build function signature based on props and slot
100    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
153/// Group consecutive If/ElseIf/Else elements into branches.
154/// Stops at a second `If` (which begins an independent conditional block)
155/// or any non-If/ElseIf/Else element.
156fn 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; // new `If` starts a separate chain
165                }
166            }
167            Node::Element(el) if el.name == "ElseIf" || el.name == "Else" => {
168                if branches.is_empty() {
169                    break; // orphaned Else/ElseIf without preceding If
170                }
171                branches.push(el);
172            }
173            _ => break,
174        }
175    }
176    branches
177}
178
179/// Generate an if/else-if/else expression for a chain of If/ElseIf/Else elements.
180/// Returns a single-line Rust expression like `if cond { div()... } else { div()... }`.
181fn 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    // Add else { div() } if the last branch isn't an Else
259    if branches.last().map(|b| b.name.as_str()) != Some("Else") {
260        expr.push_str(" else { div() }");
261    }
262    expr
263}
264
265/// Generate a For loop expression for use inside `.children(...)`.
266fn 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    // Strip self. prefix for loop variables so they reference the closure args, not state fields.
333    // Uses identifier-aware replacement to avoid corrupting fields that share a prefix
334    // e.g. self.item_count stays unchanged when var = "item"
335    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    // Built-in elements
359    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            // Standalone — generate if-expression with no else
416            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    // .fui component imports (have a non-empty render function name)
431    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    // Fallback for gpui imports and unknown elements
503    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
515/// Generate code for a `<Fragment>` — wraps children in `div()` using
516/// `generate_children_code` for consistent control-flow handling.
517fn 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
540/// Generate child code with control-flow awareness (If/ElseIf/Else chains, For, Fragment inlining).
541fn 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                // Level 3: use entity handle pattern
740                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
805// Collapse whitespace and remove space between `)` and `.` for inline expressions.
806fn clean_inline(s: &str) -> String {
807    let collapsed: String = s.split_whitespace().collect::<Vec<_>>().join(" ");
808    collapsed.replace(") .", ").")
809}
810
811/// Resolve an interpolation expression to the correct Rust variable reference.
812/// - `state.field` → `self.field` (for stateful components)
813/// - `props.field` → `props.field` (for stateless components with props)
814/// - `bare_expr` → `self.bare_expr` (fallback for stateful components)
815fn 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
826/// Replace `self.{var}` with `{var}` only when followed by a non-identifier character
827/// or end of string. This prevents corrupting state fields that share a prefix with
828/// a loop variable (e.g. `self.item_count` is not changed when var = "item").
829fn 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    // --- Happy path ---
882
883    #[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    // --- Edge cases ---
964
965    #[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        // stateless, so state. prefix is stripped but no self. prefix
990        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    // --- Contract tests ---
1003
1004    #[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    // --- Regression tests ---
1019
1020    #[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        // empty string fails to parse as f64, should skip gap
1051        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        // custom_expr doesn't start with "state.", so interpolation_expr returns "self.custom_expr"
1058        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    // --- Component imports ---
1069
1070    #[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        // GPUI imports have empty render fn — fall through to div
1093        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        // Built-in "Text" handling takes precedence, not render_text()
1102        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        // Should NOT use root element name
1119        assert!(!out.contains("pub fn render_text()"));
1120    }
1121
1122    // --- Props tests ---
1123
1124    #[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        // Option<T> fields are transparent at codegen — .into() handles conversion
1160        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    // --- Control Flow & Composition tests ---
1205
1206    #[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}