1use crate::ast::ProgramItem;
7use crate::layout::{LayoutKind, LayoutNode};
8use crate::value::{eval_expr, EvalContext, Value};
9use crate::Program;
10use std::fmt::Write;
11
12fn hex(r: u8, g: u8, b: u8, a: u8) -> String {
13 if a == 255 {
14 format!("#{:02x}{:02x}{:02x}", r, g, b)
15 } else {
16 format!("rgba({}, {}, {}, {:.2})", r, g, b, a as f32 / 255.0)
17 }
18}
19
20fn escape_jsx(s: &str) -> String {
21 s.replace('{', "\\{").replace('}', "\\}")
22}
23
24pub fn layout_to_react(
26 program: &Program,
27 root: &LayoutNode,
28) -> String {
29 let mut out = String::new();
30
31 let mut state_vars: Vec<(String, String)> = Vec::new();
33 let ctx = EvalContext::from_program(program);
34 for item in &program.items {
35 if let ProgramItem::StateDecl(sd) = item {
36 let js_val = match eval_expr(&ctx, &sd.initial_value) {
37 Ok(Value::Number(n)) => {
38 if n == (n as i64) as f64 {
39 format!("{}", n as i64)
40 } else {
41 format!("{}", n)
42 }
43 }
44 Ok(Value::String(s)) => format!("\"{}\"", s),
45 Ok(Value::Bool(b)) => format!("{}", b),
46 Ok(Value::Color { r, g, b, a }) => format!("\"{}\"", hex(r, g, b, a)),
47 _ => "null".to_string(),
48 };
49 state_vars.push((sd.name.clone(), js_val));
50 }
51 }
52
53 out.push_str("import React");
55 if !state_vars.is_empty() {
56 out.push_str(", { useState }");
57 }
58 out.push_str(" from 'react';\n\n");
59
60 out.push_str("export default function NewtApp() {\n");
62
63 for (name, val) in &state_vars {
65 let capitalized = format!("{}{}", &name[..1].to_uppercase(), &name[1..]);
66 writeln!(out, " const [{name}, set{capitalized}] = useState({val});").unwrap();
67 }
68 if !state_vars.is_empty() {
69 out.push('\n');
70 }
71
72 out.push_str(" return (\n");
73 emit_jsx(&mut out, root, 2, &state_vars);
74 out.push_str(" );\n");
75 out.push_str("}\n");
76
77 out
78}
79
80fn indent(out: &mut String, depth: usize) {
81 for _ in 0..depth {
82 out.push_str(" ");
83 }
84}
85
86fn emit_jsx(out: &mut String, n: &LayoutNode, depth: usize, state_vars: &[(String, String)]) {
87 let tag = jsx_tag(n);
88 let style = jsx_style(n);
89 let mut extra_props = String::new();
90
91 if let Some(ref onclick) = n.on_click {
93 let handler = convert_onclick(onclick, state_vars);
94 write!(extra_props, " onClick={{() => {{ {} }}}}", handler).unwrap();
95 }
96
97 let is_leaf = n.children.is_empty() && n.text.is_none();
99
100 indent(out, depth);
101 if is_leaf {
102 writeln!(out, "<{tag} style={{{{{style}}}}}{extra_props} />").unwrap();
103 } else if let Some(ref text) = n.text {
104 let content = convert_interpolation(text, state_vars);
106 writeln!(out, "<{tag} style={{{{{style}}}}}{extra_props}>{content}</{tag}>").unwrap();
107 } else {
108 writeln!(out, "<{tag} style={{{{{style}}}}}{extra_props}>").unwrap();
109 for child in &n.children {
110 emit_jsx(out, child, depth + 1, state_vars);
111 }
112 indent(out, depth);
113 writeln!(out, "</{tag}>").unwrap();
114 }
115}
116
117fn jsx_tag(n: &LayoutNode) -> &'static str {
118 match n.kind {
119 LayoutKind::Button => "button",
120 LayoutKind::Input => "input",
121 LayoutKind::Image => "img",
122 _ => "div",
123 }
124}
125
126fn jsx_style(n: &LayoutNode) -> String {
127 let mut parts: Vec<String> = Vec::new();
128
129 match n.kind {
131 LayoutKind::Row => {
132 parts.push("display: 'flex'".into());
133 parts.push("flexDirection: 'row'".into());
134 }
135 LayoutKind::Column => {
136 parts.push("display: 'flex'".into());
137 parts.push("flexDirection: 'column'".into());
138 }
139 LayoutKind::Center => {
140 parts.push("display: 'flex'".into());
141 parts.push("alignItems: 'center'".into());
142 parts.push("justifyContent: 'center'".into());
143 }
144 LayoutKind::Grid => {
145 parts.push("display: 'grid'".into());
146 }
147 LayoutKind::Stack => {
148 parts.push("position: 'relative'".into());
149 }
150 _ => {}
151 }
152
153 if let Some((r, g, b, a)) = n.fill {
159 parts.push(format!("background: '{}'", hex(r, g, b, a)));
160 }
161
162 if let Some((r, g, b, a)) = n.stroke {
164 let w = n.stroke_width.unwrap_or(1.0).max(0.0);
165 parts.push(format!("border: '{w}px solid {}'", hex(r, g, b, a)));
166 }
167
168 if n.radius > 0.0 {
170 parts.push(format!("borderRadius: {}", n.radius as i32));
171 }
172
173 if n.font_size > 0.0 && n.font_size != 16.0 {
175 parts.push(format!("fontSize: {}", n.font_size as i32));
176 }
177 if let Some(ref w) = n.font_weight {
178 parts.push(format!("fontWeight: '{w}'"));
179 }
180
181 if let Some(sh) = n.shadow {
183 if sh > 0.0 {
184 parts.push(format!(
185 "boxShadow: '0 {}px {}px rgba(0,0,0,0.15)'",
186 sh as i32,
187 (sh * 1.5) as i32
188 ));
189 }
190 }
191
192 parts.push("padding: 8".into());
194
195 parts.join(", ")
196}
197
198fn convert_onclick(expr: &str, state_vars: &[(String, String)]) -> String {
201 let mut result = Vec::new();
202 for stmt in expr.split(';') {
203 let stmt = stmt.trim();
204 if stmt.is_empty() {
205 continue;
206 }
207 if let Some(eq_pos) = stmt.find('=') {
208 let lhs = stmt[..eq_pos].trim();
209 let rhs = stmt[eq_pos + 1..].trim();
210 let is_state = state_vars.iter().any(|(name, _)| name == lhs);
212 if is_state {
213 let capitalized = format!("{}{}", &lhs[..1].to_uppercase(), &lhs[1..]);
214 if rhs.starts_with('!') {
216 result.push(format!("set{}(!{})", capitalized, &rhs[1..]));
217 } else {
218 result.push(format!("set{}({})", capitalized, rhs));
219 }
220 }
221 }
222 }
223 result.join("; ")
224}
225
226fn convert_interpolation(text: &str, _state_vars: &[(String, String)]) -> String {
229 if text.contains('{') {
230 let converted = text
231 .replace('{', "${")
232 .replace("\\${", "{"); format!("`{}`", converted)
234 } else {
235 text.to_string()
236 }
237}