Skip to main content

fncc_core/
codegen.rs

1use crate::parser::{AttrValue, Document, Element, Node};
2
3static FILE_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
4
5pub fn generate(doc: &Document) -> String {
6    let mut out = String::new();
7    let file_id = FILE_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8    let has_state = doc.state_type.is_some();
9
10    if let Some(ref fm) = doc.frontmatter {
11        out.push_str(fm);
12        out.push('\n');
13    }
14
15    // collect referenced command names for validation
16    let commands = collect_commands(&doc.root);
17    if !commands.is_empty() {
18        out.push_str("#[allow(unused)]\n");
19        out.push_str(&format!("fn _fncc_validate_{file_id}() {{\n"));
20        for cmd in &commands {
21            let trampoline = format!("__fncc_cmd_{cmd}");
22            out.push_str(&format!("    let _ = {trampoline};\n"));
23        }
24        out.push_str("}\n\n");
25    }
26
27    if has_state {
28        generate_stateful(&doc, &mut out);
29    } else {
30        generate_stateless(&doc, &mut out);
31    }
32
33    out
34}
35
36fn generate_stateless(doc: &Document, out: &mut String) {
37    let fn_name = format!("render_{}", to_snake_case(&doc.root.name));
38    out.push_str(&format!("pub fn {fn_name}() -> impl IntoElement {{\n"));
39    out.push_str(&generate_element(&doc.root, 1, false));
40    out.push('\n');
41    out.push_str("}\n");
42}
43
44fn generate_stateful(doc: &Document, out: &mut String) {
45    let state_type = doc.state_type.as_deref().unwrap_or("Self");
46
47    out.push_str(&format!("impl Render for {state_type} {{\n"));
48    out.push_str("    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {\n");
49    out.push_str("        let handle = cx.entity().downgrade();\n");
50    out.push_str(&generate_element(&doc.root, 2, true));
51    out.push_str("\n    }\n");
52    out.push_str("}\n");
53}
54
55fn generate_element(el: &Element, depth: usize, stateful: bool) -> String {
56    let indent = "    ".repeat(depth);
57    match el.name.as_str() {
58        "Stack" => gen_stack(el, &indent, depth, stateful),
59        "Text" => gen_text(el, &indent, depth, stateful),
60        "Button" => gen_button(el, &indent, depth, stateful),
61        _ => gen_fallback(el, &indent, depth, stateful),
62    }
63}
64
65fn gen_stack(el: &Element, indent: &str, depth: usize, stateful: bool) -> String {
66    let mut out = format!("{indent}div()\n");
67
68    let mut is_vertical = false;
69    for (key, val) in &el.attrs {
70        match key.as_str() {
71            "direction" if val.as_str() == "vertical" => is_vertical = true,
72            "gap" => {
73                let v = val.as_str();
74                if let Ok(n) = v.parse::<f64>() {
75                    out.push_str(&format!("{indent}    .gap(px({n}.))\n"));
76                }
77            }
78            _ => {}
79        }
80    }
81
82    if is_vertical {
83        out.push_str(&format!("{indent}    .flex()\n{indent}    .flex_col()\n"));
84    } else {
85        out.push_str(&format!("{indent}    .flex()\n"));
86    }
87
88    for child in &el.children {
89        out.push_str(&format!("{indent}    .child(\n"));
90        match child {
91            Node::Element(child_el) => {
92                out.push_str(&generate_element(child_el, depth + 2, stateful));
93            }
94            Node::Text(t) => {
95                out.push_str(&format!("{indent}        \"{t}\""));
96            }
97            Node::Interpolation(expr) => {
98                let e = strip_state_prefix(expr);
99                out.push_str(&format!("{indent}        format!(\"{{}}\", self.{e})"));
100            }
101        }
102        out.push('\n');
103        out.push_str(&format!("{indent}    )\n"));
104    }
105
106    out.trim_end().to_string()
107}
108
109fn gen_text(el: &Element, indent: &str, depth: usize, stateful: bool) -> String {
110    let mut out = format!("{indent}div()\n");
111
112    for (key, val) in &el.attrs {
113        match key.as_str() {
114            "size" => {
115                let v = val.as_str();
116                let ts = match v {
117                    "xs" => "text_xs()",
118                    "sm" => "text_sm()",
119                    "base" => "text_base()",
120                    "lg" => "text_lg()",
121                    "xl" => "text_xl()",
122                    "2xl" | "xxl" => "text_2xl()",
123                    "3xl" => "text_3xl()",
124                    _ => "text_base()",
125                };
126                out.push_str(&format!("{indent}    .{ts}\n"));
127            }
128            _ => {}
129        }
130    }
131
132    match &el.children[..] {
133        [Node::Text(t)] => {
134            out.push_str(&format!("{indent}    .child(\"{t}\")"));
135        }
136        [Node::Interpolation(expr)] => {
137            let e = strip_state_prefix(expr);
138            out.push_str(&format!("{indent}    .child(format!(\"{{}}\", self.{e}))"));
139        }
140        children => {
141            for child in children {
142                match child {
143                    Node::Text(t) => out.push_str(&format!("{indent}    .child(\"{t}\")\n")),
144                    Node::Interpolation(expr) => {
145                        let e = strip_state_prefix(expr);
146                        out.push_str(&format!("{indent}    .child(format!(\"{{}}\", self.{e}))\n"));
147                    }
148                    Node::Element(child_el) => {
149                        out.push_str(&format!("{indent}    .child(\n"));
150                        out.push_str(&generate_element(child_el, depth + 1, stateful));
151                        out.push_str(&format!("\n{indent}    )\n"));
152                    }
153                }
154            }
155        }
156    }
157
158    out.trim_end().to_string()
159}
160
161fn gen_button(el: &Element, indent: &str, depth: usize, stateful: bool) -> String {
162    let mut out = format!("{indent}div()\n");
163
164    let btn_id = match &el.children[..] {
165        [Node::Text(t)] => t.clone(),
166        _ => format!("button_{depth}"),
167    };
168    out.push_str(&format!("{indent}    .id(\"{btn_id}\")\n"));
169    out.push_str(&format!("{indent}    .cursor_pointer()\n"));
170
171    for (key, val) in &el.attrs {
172        match key.as_str() {
173            "onclick" => {
174                let handler = val.as_str();
175                let trampoline = format!("__fncc_cmd_{handler}");
176                if stateful {
177                    // Level 3: use entity handle pattern
178                    out.push_str(&format!("{indent}    .on_click({{\n"));
179                    out.push_str(&format!("{indent}        let handle = handle.clone();\n"));
180                    out.push_str(&format!("{indent}        move |_, _, cx| {{\n"));
181                    out.push_str(&format!("{indent}            handle.update(cx, |this, cx| {{\n"));
182                    out.push_str(&format!("{indent}                {trampoline}(this, cx);\n"));
183                    out.push_str(&format!("{indent}            }}).ok();\n"));
184                    out.push_str(&format!("{indent}        }}\n"));
185                    out.push_str(&format!("{indent}    }})\n"));
186                } else {
187                    out.push_str(&format!("{indent}    .on_click({trampoline})\n"));
188                }
189            }
190            _ => {}
191        }
192    }
193
194    match &el.children[..] {
195        [Node::Text(t)] => out.push_str(&format!("{indent}    .child(\"{t}\")")),
196        [Node::Interpolation(expr)] => {
197            let e = strip_state_prefix(expr);
198            out.push_str(&format!("{indent}    .child(format!(\"{{}}\", self.{e}))"));
199        }
200        children => {
201            for child in children {
202                match child {
203                    Node::Text(t) => out.push_str(&format!("{indent}    .child(\"{t}\")\n")),
204                    Node::Element(child_el) => {
205                        out.push_str(&format!("{indent}    .child(\n"));
206                        out.push_str(&generate_element(child_el, depth + 1, stateful));
207                        out.push_str(&format!("\n{indent}    )\n"));
208                    }
209                    Node::Interpolation(expr) => {
210                        let e = strip_state_prefix(expr);
211                        out.push_str(&format!("{indent}    .child(format!(\"{{}}\", self.{e}))\n"));
212                    }
213                }
214            }
215        }
216    }
217
218    out.trim_end().to_string()
219}
220
221fn gen_fallback(el: &Element, indent: &str, depth: usize, stateful: bool) -> String {
222    let mut out = format!("{indent}div()\n");
223    for (key, val) in &el.attrs {
224        let v = val.as_str();
225        out.push_str(&format!("{indent}    .attr(\"{key}\", \"{v}\")\n"));
226    }
227    for child in &el.children {
228        out.push_str(&format!("{indent}    .child(\n"));
229        match child {
230            Node::Element(child_el) => {
231                out.push_str(&generate_element(child_el, depth + 2, stateful));
232            }
233            Node::Text(t) => out.push_str(&format!("{indent}        \"{t}\"")),
234            Node::Interpolation(expr) => {
235                let e = strip_state_prefix(expr);
236                out.push_str(&format!("{indent}        format!(\"{{}}\", self.{e})"));
237            }
238        }
239        out.push('\n');
240        out.push_str(&format!("{indent}    )\n"));
241    }
242    out.trim_end().to_string()
243}
244
245/// Removes `state.` prefix from interpolation expressions
246/// e.g. "state.count" → "count"
247fn strip_state_prefix(expr: &str) -> &str {
248    expr.trim().strip_prefix("state.").unwrap_or(expr.trim())
249}
250
251fn to_snake_case(name: &str) -> String {
252    let mut result = String::new();
253    for (i, ch) in name.chars().enumerate() {
254        if ch.is_uppercase() {
255            if i > 0 {
256                result.push('_');
257            }
258            for c in ch.to_lowercase() {
259                result.push(c);
260            }
261        } else {
262            result.push(ch);
263        }
264    }
265    result
266}
267
268fn collect_commands(el: &Element) -> Vec<String> {
269    let mut cmds = Vec::new();
270    for (key, val) in &el.attrs {
271        if key == "onclick" {
272            if let AttrValue::String(name) = val {
273                if !cmds.contains(name) {
274                    cmds.push(name.clone());
275                }
276            }
277        }
278    }
279    for child in &el.children {
280        if let Node::Element(child_el) = child {
281            cmds.extend(collect_commands(child_el));
282        }
283    }
284    cmds
285}
286
287impl AttrValue {
288    fn as_str(&self) -> &str {
289        match self {
290            AttrValue::String(s) => s,
291            AttrValue::Interpolation(s) => s,
292        }
293    }
294}