1- use crate :: parser:: { AppTree , Node , NodeKind , StyleDecl } ;
1+ use crate :: parser:: { AppTree , Node , NodeKind , SignalDecl , StyleDecl } ;
22#[ allow( unused_imports) ]
33use anyhow:: Context ;
44use anyhow:: Result ;
55
66/// Generate a complete Rust main.rs that builds and runs a W3C OS application.
77pub fn generate ( tree : & AppTree ) -> Result < String > {
8- let component_code = gen_node ( & tree. root , 0 ) ;
8+ let is_reactive = !tree. signals . is_empty ( ) ;
9+ let signal_names: Vec < & str > = tree. signals . iter ( ) . map ( |s| s. name . as_str ( ) ) . collect ( ) ;
10+ let component_code = gen_node ( & tree. root , 0 , & signal_names) ;
911
10- Ok ( format ! (
11- r#"//! Auto-generated by w3cos compiler — do not edit.
12+ if is_reactive {
13+ let signal_inits = gen_signal_inits ( & tree. signals ) ;
14+ Ok ( format ! (
15+ r#"//! Auto-generated by w3cos compiler — do not edit.
16+ use w3cos_std::{{Component, EventAction, Style}};
17+ use w3cos_std::style::*;
18+ use w3cos_std::color::Color;
19+
20+ fn main() {{
21+ w3cos_runtime::run_app(build_ui).expect("W3C OS app crashed");
22+ }}
23+
24+ fn build_ui() -> Component {{
25+ {signal_inits}
26+ {component_code}
27+ }}
28+ "#
29+ ) )
30+ } else {
31+ Ok ( format ! (
32+ r#"//! Auto-generated by w3cos compiler — do not edit.
1233use w3cos_std::{{Component, Style}};
1334use w3cos_std::style::*;
1435use w3cos_std::color::Color;
1536
1637fn main() {{
17- let root = build_ui();
18- w3cos_runtime::run_app(root).expect("W3C OS app crashed");
38+ w3cos_runtime::run_app(build_ui).expect("W3C OS app crashed");
1939}}
2040
2141fn build_ui() -> Component {{
2242{component_code}
2343}}
2444"#
25- ) )
45+ ) )
46+ }
47+ }
48+
49+ fn gen_signal_inits ( signals : & [ SignalDecl ] ) -> String {
50+ signals
51+ . iter ( )
52+ . enumerate ( )
53+ . map ( |( i, sig) | {
54+ format ! (
55+ " let _ = w3cos_runtime::state::create_signal({initial});\n let {name} = w3cos_runtime::state::get_signal({i});" ,
56+ initial = sig. initial,
57+ name = sig. name,
58+ )
59+ } )
60+ . collect :: < Vec < _ > > ( )
61+ . join ( "\n " )
2662}
2763
2864/// Generate a Cargo.toml for the compiled application.
@@ -82,18 +118,32 @@ fn find_workspace_root() -> Result<std::path::PathBuf> {
82118 )
83119}
84120
85- fn gen_node ( node : & Node , depth : usize ) -> String {
121+ fn gen_node ( node : & Node , depth : usize , signal_names : & [ & str ] ) -> String {
86122 let indent = " " . repeat ( depth + 1 ) ;
87123 let style_code = gen_style ( & node. style , depth + 1 ) ;
88124
89125 match & node. kind {
90126 NodeKind :: Text ( content) => {
91127 let text = node. text . as_deref ( ) . unwrap_or ( content. as_str ( ) ) ;
92- format ! ( "{indent}Component::text({text:?}, {style_code})" )
128+ let text_expr = gen_text_expr ( text, signal_names) ;
129+ format ! ( "{indent}Component::text({text_expr}, {style_code})" )
93130 }
94131 NodeKind :: Button ( label) => {
95132 let lbl = node. label . as_deref ( ) . unwrap_or ( label. as_str ( ) ) ;
96- format ! ( "{indent}Component::button({lbl:?}, {style_code})" )
133+ if let Some ( ref action_str) = node. on_click {
134+ let action = gen_event_action ( action_str, signal_names) ;
135+ format ! ( "{indent}Component::button_with_click({lbl:?}, {style_code}, {action})" )
136+ } else {
137+ format ! ( "{indent}Component::button({lbl:?}, {style_code})" )
138+ }
139+ }
140+ NodeKind :: Image ( src) => {
141+ let src_val = node. src . as_deref ( ) . unwrap_or ( src. as_str ( ) ) ;
142+ format ! ( "{indent}Component::image({src_val:?}, {style_code})" )
143+ }
144+ NodeKind :: TextInput => {
145+ let placeholder = node. placeholder . as_deref ( ) . unwrap_or ( "Enter text" ) ;
146+ format ! ( "{indent}Component::text_input(\" \" , {placeholder:?}, {style_code})" )
97147 }
98148 NodeKind :: Column | NodeKind :: Row | NodeKind :: Box => {
99149 let constructor = match & node. kind {
@@ -108,7 +158,7 @@ fn gen_node(node: &Node, depth: usize) -> String {
108158 let items: Vec < String > = node
109159 . children
110160 . iter ( )
111- . map ( |c| gen_node ( c, depth + 2 ) )
161+ . map ( |c| gen_node ( c, depth + 2 , signal_names ) )
112162 . collect ( ) ;
113163 format ! ( "vec![\n {},\n {indent}]" , items. join( ",\n " ) )
114164 } ;
@@ -117,6 +167,53 @@ fn gen_node(node: &Node, depth: usize) -> String {
117167 }
118168}
119169
170+ fn gen_text_expr ( text : & str , signal_names : & [ & str ] ) -> String {
171+ if !text. contains ( '{' ) {
172+ return format ! ( "{text:?}" ) ;
173+ }
174+ // Check for {signal_name} interpolation
175+ for ( i, name) in signal_names. iter ( ) . enumerate ( ) {
176+ let placeholder = format ! ( "{{{name}}}" ) ;
177+ if text == placeholder {
178+ return format ! ( "w3cos_runtime::state::get_signal({i}).to_string()" ) ;
179+ }
180+ if text. contains ( & placeholder) {
181+ return format ! (
182+ "{text:?}.replace(\" {placeholder}\" , &w3cos_runtime::state::get_signal({i}).to_string())"
183+ ) ;
184+ }
185+ }
186+ format ! ( "{text:?}" )
187+ }
188+
189+ fn gen_event_action ( action_str : & str , signal_names : & [ & str ] ) -> String {
190+ let parts: Vec < & str > = action_str. split ( ':' ) . collect ( ) ;
191+ if parts. len ( ) < 2 {
192+ return "EventAction::None" . to_string ( ) ;
193+ }
194+
195+ let action_type = parts[ 0 ] . trim ( ) ;
196+ let signal_name = parts[ 1 ] . trim ( ) ;
197+ let signal_id = signal_names
198+ . iter ( )
199+ . position ( |& n| n == signal_name)
200+ . unwrap_or ( 0 ) ;
201+
202+ match action_type {
203+ "increment" => format ! ( "EventAction::Increment({signal_id})" ) ,
204+ "decrement" => format ! ( "EventAction::Decrement({signal_id})" ) ,
205+ "toggle" => format ! ( "EventAction::Toggle({signal_id})" ) ,
206+ "set" => {
207+ let value = parts
208+ . get ( 2 )
209+ . and_then ( |v| v. trim ( ) . parse :: < i64 > ( ) . ok ( ) )
210+ . unwrap_or ( 0 ) ;
211+ format ! ( "EventAction::Set({signal_id}, {value})" )
212+ }
213+ _ => "EventAction::None" . to_string ( ) ,
214+ }
215+ }
216+
120217fn gen_style ( s : & StyleDecl , depth : usize ) -> String {
121218 let indent = " " . repeat ( depth) ;
122219 let mut fields = Vec :: new ( ) ;
@@ -272,7 +369,11 @@ mod tests {
272369 children : vec ! [ ] ,
273370 text : Some ( "hello" . to_string ( ) ) ,
274371 label : None ,
372+ on_click : None ,
373+ src : None ,
374+ placeholder : None ,
275375 } ,
376+ signals : vec ! [ ] ,
276377 } ;
277378 let rust = generate ( & tree) . unwrap ( ) ;
278379 assert ! ( rust. contains( "Component::text(\" hello\" " ) ) ;
@@ -289,7 +390,11 @@ mod tests {
289390 children : vec ! [ ] ,
290391 text : None ,
291392 label : Some ( "click me" . to_string ( ) ) ,
393+ on_click : None ,
394+ src : None ,
395+ placeholder : None ,
292396 } ,
397+ signals : vec ! [ ] ,
293398 } ;
294399 let rust = generate ( & tree) . unwrap ( ) ;
295400 assert ! ( rust. contains( "Component::button(\" click me\" " ) ) ;
@@ -308,18 +413,28 @@ mod tests {
308413 children: vec![ ] ,
309414 text: Some ( "a" . to_string( ) ) ,
310415 label: None ,
416+ on_click: None ,
417+ src: None ,
418+ placeholder: None ,
311419 } ,
312420 Node {
313421 kind: NodeKind :: Text ( "b" . to_string( ) ) ,
314422 style: StyleDecl :: default ( ) ,
315423 children: vec![ ] ,
316424 text: Some ( "b" . to_string( ) ) ,
317425 label: None ,
426+ on_click: None ,
427+ src: None ,
428+ placeholder: None ,
318429 } ,
319430 ] ,
320431 text : None ,
321432 label : None ,
433+ on_click : None ,
434+ src : None ,
435+ placeholder : None ,
322436 } ,
437+ signals : vec ! [ ] ,
323438 } ;
324439 let rust = generate ( & tree) . unwrap ( ) ;
325440 assert ! ( rust. contains( "Component::column" ) ) ;
@@ -328,6 +443,26 @@ mod tests {
328443 assert ! ( rust. contains( "vec![" ) ) ;
329444 }
330445
446+ #[ test]
447+ fn codegen_text_input_produces_component_text_input ( ) {
448+ let tree = AppTree {
449+ root : Node {
450+ kind : NodeKind :: TextInput ,
451+ style : StyleDecl :: default ( ) ,
452+ children : vec ! [ ] ,
453+ text : None ,
454+ label : None ,
455+ on_click : None ,
456+ src : None ,
457+ placeholder : Some ( "Enter name" . to_string ( ) ) ,
458+ } ,
459+ signals : vec ! [ ] ,
460+ } ;
461+ let rust = generate ( & tree) . unwrap ( ) ;
462+ assert ! ( rust. contains( "Component::text_input" ) ) ;
463+ assert ! ( rust. contains( "\" Enter name\" " ) ) ;
464+ }
465+
331466 #[ test]
332467 fn codegen_row_with_style ( ) {
333468 let mut style = StyleDecl :: default ( ) ;
@@ -339,7 +474,11 @@ mod tests {
339474 children : vec ! [ ] ,
340475 text : None ,
341476 label : None ,
477+ on_click : None ,
478+ src : None ,
479+ placeholder : None ,
342480 } ,
481+ signals : vec ! [ ] ,
343482 } ;
344483 let rust = generate ( & tree) . unwrap ( ) ;
345484 assert ! ( rust. contains( "Component::row" ) ) ;
@@ -358,10 +497,33 @@ mod tests {
358497 children : vec ! [ ] ,
359498 text : Some ( "styled" . to_string ( ) ) ,
360499 label : None ,
500+ on_click : None ,
501+ src : None ,
502+ placeholder : None ,
361503 } ,
504+ signals : vec ! [ ] ,
362505 } ;
363506 let rust = generate ( & tree) . unwrap ( ) ;
364507 assert ! ( rust. contains( "Color::from_hex(\" #fff\" )" ) ) ;
365508 assert ! ( rust. contains( "Color::from_hex(\" #e94560\" )" ) ) ;
366509 }
510+
511+ #[ test]
512+ fn codegen_image_produces_component_image ( ) {
513+ let tree = AppTree {
514+ root : Node {
515+ kind : NodeKind :: Image ( "path.png" . to_string ( ) ) ,
516+ style : StyleDecl :: default ( ) ,
517+ children : vec ! [ ] ,
518+ text : None ,
519+ label : None ,
520+ on_click : None ,
521+ src : Some ( "path.png" . to_string ( ) ) ,
522+ placeholder : None ,
523+ } ,
524+ signals : vec ! [ ] ,
525+ } ;
526+ let rust = generate ( & tree) . unwrap ( ) ;
527+ assert ! ( rust. contains( "Component::image(\" path.png\" " ) ) ;
528+ }
367529}
0 commit comments