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 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 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
245fn 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}