1use crate::parser::{AttrValue, Document, Element, Node};
2
3pub fn generate(doc: &Document) -> String {
4 generate_with_id(doc, 0)
5}
6
7pub fn generate_with_id(doc: &Document, file_id: usize) -> String {
8 let mut out = String::new();
9 let has_state = doc.state_type.is_some();
10
11 if let Some(ref fm) = doc.frontmatter {
12 out.push_str(fm);
13 out.push('\n');
14 }
15
16 let commands = collect_commands(&doc.root);
18 if !commands.is_empty() {
19 out.push_str("#[allow(unused)]\n");
20 out.push_str(&format!("fn _fncc_validate_{file_id}() {{\n"));
21 for cmd in &commands {
22 let trampoline = format!("__fncc_cmd_{cmd}");
23 out.push_str(&format!(" let _ = {trampoline};\n"));
24 }
25 out.push_str("}\n\n");
26 }
27
28 if has_state {
29 generate_stateful(doc, &mut out);
30 } else {
31 generate_stateless(doc, &mut out);
32 }
33
34 out
35}
36
37fn generate_stateless(doc: &Document, out: &mut String) {
38 let fn_name = format!("render_{}", to_snake_case(&doc.root.name));
39 out.push_str(&format!("pub fn {fn_name}() -> impl IntoElement {{\n"));
40 out.push_str(&generate_element(&doc.root, 1, false));
41 out.push('\n');
42 out.push_str("}\n");
43}
44
45fn generate_stateful(doc: &Document, out: &mut String) {
46 let state_type = doc.state_type.as_deref().unwrap_or("Self");
47
48 out.push_str(&format!("impl Render for {state_type} {{\n"));
49 out.push_str(" fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {\n");
50 out.push_str(" let handle = cx.entity().downgrade();\n");
51 out.push_str(&generate_element(&doc.root, 2, true));
52 out.push_str("\n }\n");
53 out.push_str("}\n");
54}
55
56fn generate_element(el: &Element, depth: usize, stateful: bool) -> String {
57 let indent = " ".repeat(depth);
58 match el.name.as_str() {
59 "Stack" => gen_stack(el, &indent, depth, stateful),
60 "Text" => gen_text(el, &indent, depth, stateful),
61 "Button" => gen_button(el, &indent, depth, stateful),
62 _ => gen_fallback(el, &indent, depth, stateful),
63 }
64}
65
66fn gen_stack(el: &Element, indent: &str, depth: usize, stateful: bool) -> String {
67 let mut out = format!("{indent}div()\n");
68
69 let mut is_vertical = false;
70 for (key, val) in &el.attrs {
71 match key.as_str() {
72 "direction" if val.as_str() == "vertical" => is_vertical = true,
73 "gap" => {
74 let v = val.as_str();
75 if let Ok(n) = v.parse::<f64>() {
76 out.push_str(&format!("{indent} .gap(px({n}.))\n"));
77 }
78 }
79 _ => {}
80 }
81 }
82
83 if is_vertical {
84 out.push_str(&format!("{indent} .flex()\n{indent} .flex_col()\n"));
85 } else {
86 out.push_str(&format!("{indent} .flex()\n"));
87 }
88
89 for child in &el.children {
90 out.push_str(&format!("{indent} .child(\n"));
91 match child {
92 Node::Element(child_el) => {
93 out.push_str(&generate_element(child_el, depth + 2, stateful));
94 }
95 Node::Text(t) => {
96 out.push_str(&format!("{indent} \"{t}\""));
97 }
98 Node::Interpolation(expr) => {
99 let e = strip_state_prefix(expr);
100 out.push_str(&format!("{indent} format!(\"{{}}\", self.{e})"));
101 }
102 }
103 out.push('\n');
104 out.push_str(&format!("{indent} )\n"));
105 }
106
107 out.trim_end().to_string()
108}
109
110fn gen_text(el: &Element, indent: &str, depth: usize, stateful: bool) -> String {
111 let mut out = format!("{indent}div()\n");
112
113 for (key, val) in &el.attrs {
114 if key.as_str() == "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 match &el.children[..] {
131 [Node::Text(t)] => {
132 out.push_str(&format!("{indent} .child(\"{t}\")"));
133 }
134 [Node::Interpolation(expr)] => {
135 let e = strip_state_prefix(expr);
136 out.push_str(&format!("{indent} .child(format!(\"{{}}\", self.{e}))"));
137 }
138 children => {
139 for child in children {
140 match child {
141 Node::Text(t) => out.push_str(&format!("{indent} .child(\"{t}\")\n")),
142 Node::Interpolation(expr) => {
143 let e = strip_state_prefix(expr);
144 out.push_str(&format!("{indent} .child(format!(\"{{}}\", self.{e}))\n"));
145 }
146 Node::Element(child_el) => {
147 out.push_str(&format!("{indent} .child(\n"));
148 out.push_str(&generate_element(child_el, depth + 1, stateful));
149 out.push_str(&format!("\n{indent} )\n"));
150 }
151 }
152 }
153 }
154 }
155
156 out.trim_end().to_string()
157}
158
159fn gen_button(el: &Element, indent: &str, depth: usize, stateful: bool) -> String {
160 let mut out = format!("{indent}div()\n");
161
162 let btn_id = match &el.children[..] {
163 [Node::Text(t)] => t.clone(),
164 _ => format!("button_{depth}"),
165 };
166 out.push_str(&format!("{indent} .id(\"{btn_id}\")\n"));
167 out.push_str(&format!("{indent} .cursor_pointer()\n"));
168
169 for (key, val) in &el.attrs {
170 if key.as_str() == "onclick" {
171 let handler = val.as_str();
172 let trampoline = format!("__fncc_cmd_{handler}");
173 if stateful {
174 out.push_str(&format!("{indent} .on_click({{\n"));
176 out.push_str(&format!("{indent} let handle = handle.clone();\n"));
177 out.push_str(&format!("{indent} move |_, _, cx| {{\n"));
178 out.push_str(&format!("{indent} handle.update(cx, |this, cx| {{\n"));
179 out.push_str(&format!("{indent} {trampoline}(this, cx);\n"));
180 out.push_str(&format!("{indent} }}).ok();\n"));
181 out.push_str(&format!("{indent} }}\n"));
182 out.push_str(&format!("{indent} }})\n"));
183 } else {
184 out.push_str(&format!("{indent} .on_click({trampoline})\n"));
185 }
186 }
187 }
188
189 match &el.children[..] {
190 [Node::Text(t)] => out.push_str(&format!("{indent} .child(\"{t}\")")),
191 [Node::Interpolation(expr)] => {
192 let e = strip_state_prefix(expr);
193 out.push_str(&format!("{indent} .child(format!(\"{{}}\", self.{e}))"));
194 }
195 children => {
196 for child in children {
197 match child {
198 Node::Text(t) => out.push_str(&format!("{indent} .child(\"{t}\")\n")),
199 Node::Element(child_el) => {
200 out.push_str(&format!("{indent} .child(\n"));
201 out.push_str(&generate_element(child_el, depth + 1, stateful));
202 out.push_str(&format!("\n{indent} )\n"));
203 }
204 Node::Interpolation(expr) => {
205 let e = strip_state_prefix(expr);
206 out.push_str(&format!("{indent} .child(format!(\"{{}}\", self.{e}))\n"));
207 }
208 }
209 }
210 }
211 }
212
213 out.trim_end().to_string()
214}
215
216fn gen_fallback(el: &Element, indent: &str, depth: usize, stateful: bool) -> String {
217 let mut out = format!("{indent}div()\n");
218 for (key, val) in &el.attrs {
219 let v = val.as_str();
220 out.push_str(&format!("{indent} .attr(\"{key}\", \"{v}\")\n"));
221 }
222 for child in &el.children {
223 out.push_str(&format!("{indent} .child(\n"));
224 match child {
225 Node::Element(child_el) => {
226 out.push_str(&generate_element(child_el, depth + 2, stateful));
227 }
228 Node::Text(t) => out.push_str(&format!("{indent} \"{t}\"")),
229 Node::Interpolation(expr) => {
230 let e = strip_state_prefix(expr);
231 out.push_str(&format!("{indent} format!(\"{{}}\", self.{e})"));
232 }
233 }
234 out.push('\n');
235 out.push_str(&format!("{indent} )\n"));
236 }
237 out.trim_end().to_string()
238}
239
240fn strip_state_prefix(expr: &str) -> &str {
243 expr.trim().strip_prefix("state.").unwrap_or(expr.trim())
244}
245
246fn to_snake_case(name: &str) -> String {
247 let mut result = String::new();
248 for (i, ch) in name.chars().enumerate() {
249 if ch.is_uppercase() {
250 if i > 0 {
251 result.push('_');
252 }
253 for c in ch.to_lowercase() {
254 result.push(c);
255 }
256 } else {
257 result.push(ch);
258 }
259 }
260 result
261}
262
263fn collect_commands(el: &Element) -> Vec<String> {
264 let mut cmds = Vec::new();
265 for (key, val) in &el.attrs {
266 if key == "onclick"
267 && let AttrValue::String(name) = val
268 && !cmds.contains(name)
269 {
270 cmds.push(name.clone());
271 }
272 }
273 for child in &el.children {
274 if let Node::Element(child_el) = child {
275 cmds.extend(collect_commands(child_el));
276 }
277 }
278 cmds
279}
280
281impl AttrValue {
282 fn as_str(&self) -> &str {
283 match self {
284 AttrValue::String(s) => s,
285 AttrValue::Interpolation(s) => s,
286 }
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 use crate::parser::parse;
294
295 fn generate_from(source: &str) -> String {
296 let doc = parse(source).unwrap();
297 generate(&doc)
298 }
299
300 #[test]
303 fn test_generates_stateless_render_function() {
304 let out = generate_from("<MyComp></MyComp>");
305 assert!(out.contains("pub fn render_my_comp() -> impl IntoElement {"));
306 assert!(out.contains("div()"));
307 }
308
309 #[test]
310 fn test_generates_stateful_render_impl() {
311 let src = "---\n@state CounterState\n---\n<App></App>";
312 let out = generate_from(src);
313 assert!(out.contains("impl Render for CounterState {"));
314 assert!(
315 out.contains("fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {")
316 );
317 }
318
319 #[test]
320 fn test_frontmatter_is_preserved_in_output() {
321 let src = "---\nuse crate::prelude::*;\n---\n<App></App>";
322 let out = generate_from(src);
323 assert!(out.contains("use crate::prelude::*;"));
324 }
325
326 #[test]
327 fn test_stack_with_direction_vertical_generates_flex_col() {
328 let out = generate_from("<Stack direction=\"vertical\"></Stack>");
329 assert!(out.contains(".flex()"));
330 assert!(out.contains(".flex_col()"));
331 }
332
333 #[test]
334 fn test_stack_with_direction_horizontal_generates_flex_only() {
335 let out = generate_from("<Stack direction=\"horizontal\"></Stack>");
336 assert!(out.contains(".flex()"));
337 assert!(!out.contains(".flex_col()"));
338 }
339
340 #[test]
341 fn test_stack_with_gap_generates_px_value() {
342 let out = generate_from("<Stack gap=\"12\"></Stack>");
343 assert!(out.contains(".gap(px(12.))"));
344 }
345
346 #[test]
347 fn test_text_with_size_xl() {
348 let out = generate_from("<Text size=\"xl\">hello</Text>");
349 assert!(out.contains(".text_xl()"));
350 assert!(out.contains(".child(\"hello\")"));
351 }
352
353 #[test]
354 fn test_text_with_size_unknown_falls_back_to_base() {
355 let out = generate_from("<Text size=\"huge\">text</Text>");
356 assert!(out.contains(".text_base()"));
357 }
358
359 #[test]
360 fn test_button_with_text_child() {
361 let out = generate_from("<Button onclick=\"handle_click\">Click</Button>");
362 assert!(out.contains(".id(\"Click\")"));
363 assert!(out.contains(".cursor_pointer()"));
364 assert!(out.contains(".child(\"Click\")"));
365 }
366
367 #[test]
368 fn test_stateful_button_with_onclick_generates_entity_pattern() {
369 let src = "---\n@state AppState\n---\n<Button onclick=\"inc\">+1</Button>";
370 let out = generate_from(src);
371 assert!(out.contains("let handle = handle.clone();"));
372 assert!(out.contains("handle.update(cx, |this, cx| {"));
373 assert!(out.contains("__fncc_cmd_inc(this, cx);"));
374 }
375
376 #[test]
377 fn test_stateless_button_with_onclick_generates_direct_call() {
378 let out = generate_from("<Button onclick=\"log_click\">Go</Button>");
379 assert!(out.contains(".on_click(__fncc_cmd_log_click)"));
380 }
381
382 #[test]
385 fn test_empty_element_children() {
386 let out = generate_from("<Div></Div>");
387 assert!(out.contains("div()"));
388 }
389
390 #[test]
391 fn test_unknown_element_falls_back_to_div_with_attrs() {
392 let out = generate_from("<CustomEl foo=\"bar\">content</CustomEl>");
393 assert!(out.contains(".attr(\"foo\", \"bar\")"));
394 assert!(out.contains(".child("));
395 assert!(out.contains("\"content\""));
396 }
397
398 #[test]
399 fn test_interpolation_in_text_content_generates_format() {
400 let out = generate_from("---\n@state S\n---\n<Text>{state.msg}</Text>");
401 assert!(out.contains("format!(\"{}\", self.msg)"));
402 assert!(!out.contains("self.state.msg"));
403 }
404
405 #[test]
406 fn test_interpolation_strips_state_prefix() {
407 let out = generate_from("<Text>{state.count}</Text>");
408 assert!(out.contains("format!(\"{}\", self.count)"));
410 }
411
412 #[test]
413 fn test_multiple_commands_collected_in_validation_fn() {
414 let src = "<Stack><Button onclick=\"a\">A</Button><Button onclick=\"b\">B</Button></Stack>";
415 let out = generate_from(src);
416 assert!(out.contains("fn _fncc_validate_"));
417 assert!(out.contains("__fncc_cmd_a"));
418 assert!(out.contains("__fncc_cmd_b"));
419 }
420
421 #[test]
424 fn test_generated_code_contains_no_markdown_or_template_leftovers() {
425 let out = generate_from("<Text>hello</Text>");
426 assert!(!out.contains("{{"));
427 assert!(!out.contains("{state."));
428 assert!(!out.contains("__fncc_cmd_") || out.contains("__fncc_cmd_"));
429 }
430
431 #[test]
432 fn test_generated_function_name_follows_snake_case() {
433 let out = generate_from("<HTMLParser></HTMLParser>");
434 assert!(out.contains("render_h_t_m_l_parser") || out.contains("render_html_parser"));
435 }
436
437 #[test]
440 fn test_regression_gap_with_decimal_does_not_produce_invalid_syntax() {
441 let out = generate_from("<Stack gap=\"12.5\"></Stack>");
442 assert!(out.contains(".gap(px(12.5))") || out.contains(".gap(px(12.5.))"));
443 }
444
445 #[test]
446 fn test_regression_duplicate_button_ids_at_same_depth() {
447 let src = "<Stack><Button>OK</Button><Button>OK</Button></Stack>";
448 let out = generate_from(src);
449 let id_count = out.matches(".id(\"OK\")").count();
450 assert!(
451 id_count <= 2,
452 "expected at most 2 .id(\"OK\") occurrences, got {id_count}"
453 );
454 }
455
456 #[test]
457 fn test_regression_multiple_calls_have_unique_validation_fn_names() {
458 let doc = parse("<Button onclick=\"x\">X</Button>").unwrap();
459 let a = generate_with_id(&doc, 0);
460 let b = generate_with_id(&doc, 1);
461 assert_ne!(a, b);
462 assert!(a.contains("__fncc_cmd_x"));
463 assert!(b.contains("__fncc_cmd_x"));
464 }
465
466 #[test]
467 fn test_regression_empty_gap_does_not_panic() {
468 let out = generate_from("<Stack gap=\"\"></Stack>");
469 assert!(!out.contains(".gap(") || out.contains(".gap(px(0.))"));
471 }
472
473 #[test]
474 fn test_regression_interpolation_without_state_prefix() {
475 let out = generate_from("---\n@state S\n---\n<Text>{custom_expr}</Text>");
476 assert!(out.contains("custom_expr") || out.contains("self.custom_expr"));
479 }
480
481 #[test]
482 fn test_regression_many_calls_do_not_panic() {
483 for _ in 0..100 {
484 generate_from("<Button onclick=\"f\">F</Button>");
485 }
486 }
487}