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