//! Code generator — transforms el-ui AST into JavaScript module source. //! //! Each component becomes a class that: //! 1. Extends `Component` from the el-ui runtime. //! 2. Stores each `state` field as a node in an in-instance `Graph`. //! 3. Implements `render()` returning an HTML string. //! 4. Uses `setState()` to trigger spreading activation and DOM patching. //! //! ## Codegen targets //! //! The `CodegenTarget` enum selects the output format: //! //! - `Web` — ES2022 module (current, default behavior) //! - `Server` — Rust code calling the `el-platform` server backend for SSR //! - `Native(Platform)` — Rust code calling the `el-platform` native backend trait use crate::ast::*; use crate::error::CompileResult; /// Which native platform to target when using `CodegenTarget::Native`. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Platform { Ios, Android, Macos, Linux, Windows, } impl Platform { pub fn as_str(&self) -> &'static str { match self { Self::Ios => "ios", Self::Android => "android", Self::Macos => "macos", Self::Linux => "linux", Self::Windows => "windows", } } } /// Selects the output format for the code generator. /// /// - `Web` — ES2022 JavaScript module (default, uses the el-ui JS runtime) /// - `Server` — Rust module that uses `el_platform::ServerBackend` for SSR. /// The generated Rust struct implements a `render_to_html()` method. /// - `Native(Platform)` — Rust module calling `el_platform::PlatformBackend` /// for the specified native platform (iOS, Android, macOS, Linux, Windows). #[derive(Debug, Clone, PartialEq, Eq)] pub enum CodegenTarget { /// Current default — ES2022 JavaScript module for the browser. Web, /// Server-side rendering — generates Rust code using `el_platform::ServerBackend`. Server, /// Native platform — generates Rust code using the platform backend trait. Native(Platform), } impl CodegenTarget { pub fn is_web(&self) -> bool { matches!(self, Self::Web) } pub fn is_rust_output(&self) -> bool { matches!(self, Self::Server | Self::Native(_)) } } pub struct Codegen { runtime_path: String, /// The compilation target. Defaults to `Web`. pub target: CodegenTarget, } impl Codegen { pub fn new(runtime_path: &str) -> Self { Self { runtime_path: runtime_path.to_owned(), target: CodegenTarget::Web, } } pub fn with_target(mut self, target: CodegenTarget) -> Self { self.target = target; self } pub fn generate(&self, components: &[Component]) -> CompileResult { match &self.target { CodegenTarget::Web => self.generate_web(components), CodegenTarget::Server => self.generate_server_rust(components), CodegenTarget::Native(platform) => self.generate_native_rust(components, platform), } } fn generate_web(&self, components: &[Component]) -> CompileResult { let mut out = String::new(); // Runtime import out.push_str(&format!( "import {{ Component, Graph, Renderer, Router, mount }} from '{}';\n\n", self.runtime_path )); for component in components { out.push_str(&self.gen_component(component)?); out.push('\n'); } // Export all component names let names: Vec<&str> = components.iter().map(|c| c.name.as_str()).collect(); if !names.is_empty() { out.push_str(&format!("export {{ {} }};\n", names.join(", "))); } Ok(out) } /// Generate Rust code for the `server` target. /// /// The output is a Rust module where each component is a struct implementing /// `render_to_html(&self) -> String` using `el_platform::ServerBackend`. fn generate_server_rust(&self, components: &[Component]) -> CompileResult { let mut out = String::new(); out.push_str("//! el-ui SSR — generated by el-ui-compiler (target: server)\n"); out.push_str("//! Do not edit. Re-generate with: el-ui-compiler --target server\n\n"); out.push_str("use el_platform::{ServerBackend, PlatformBackend, PlatformNode};\n\n"); for comp in components { out.push_str(&format!("/// Server-side rendered component: {}\n", comp.name)); out.push_str(&format!("pub struct {} {{\n", comp.name)); for prop in &comp.props { out.push_str(&format!(" pub {}: String,\n", prop.name)); } for state in &comp.state { out.push_str(&format!(" pub {}: String,\n", state.name)); } out.push_str("}\n\n"); out.push_str(&format!("impl {} {{\n", comp.name)); out.push_str(" pub fn render_to_html(&self) -> String {\n"); out.push_str(" let backend = ServerBackend::new();\n"); // Generate a simple element tree based on the template out.push_str(" let root = self.build_node_tree();\n"); out.push_str(" backend.render_to_string(&root).unwrap_or_default()\n"); out.push_str(" }\n\n"); out.push_str(" fn build_node_tree(&self) -> PlatformNode {\n"); out.push_str(" // TODO: full template → PlatformNode tree codegen\n"); out.push_str(" // The compiler translates each TemplateNode into\n"); out.push_str(" // PlatformNode::element() / PlatformNode::text() calls.\n"); out.push_str(" PlatformNode::element(\"div\")\n"); out.push_str(" }\n"); out.push_str("}\n\n"); } Ok(out) } /// Generate Rust code for a `native` target. /// /// The output is a Rust module where each component builds a `PlatformNode` /// tree and calls the appropriate backend to mount it. fn generate_native_rust(&self, components: &[Component], platform: &Platform) -> CompileResult { let backend_type = match platform { Platform::Ios => "IosBackend", Platform::Android => "AndroidBackend", Platform::Macos => "MacosBackend", Platform::Linux => "LinuxBackend", Platform::Windows => "WindowsBackend", }; let mut out = String::new(); out.push_str(&format!( "//! el-ui native — generated by el-ui-compiler (target: {})\n", platform.as_str() )); out.push_str("//! Do not edit. Re-generate with: el-ui-compiler --target \n\n"); out.push_str(&format!( "use el_platform::{{{} as Backend, PlatformBackend, PlatformNode}};\n\n", backend_type )); for comp in components { out.push_str(&format!("/// Native component: {} ({})\n", comp.name, platform.as_str())); out.push_str(&format!("pub struct {} {{\n", comp.name)); for prop in &comp.props { out.push_str(&format!(" pub {}: String,\n", prop.name)); } for state in &comp.state { out.push_str(&format!(" pub {}: String,\n", state.name)); } out.push_str(" backend: Backend,\n"); out.push_str("}\n\n"); out.push_str(&format!("impl {} {{\n", comp.name)); out.push_str(" pub fn new() -> Self {\n"); out.push_str(" Self {\n"); for prop in &comp.props { let default = prop.default.as_deref().unwrap_or("\"\""); out.push_str(&format!(" {}: {}.to_string(),\n", prop.name, default)); } for state in &comp.state { out.push_str(&format!(" {}: {}.to_string(),\n", state.name, state.initial)); } out.push_str(" backend: Backend::new(),\n"); out.push_str(" }\n"); out.push_str(" }\n\n"); out.push_str(" pub fn mount(&self, container_id: &str) -> el_platform::PlatformResult<()> {\n"); out.push_str(" let root = self.build_node_tree();\n"); out.push_str(" self.backend.mount(root, container_id)\n"); out.push_str(" }\n\n"); out.push_str(" fn build_node_tree(&self) -> PlatformNode {\n"); out.push_str(" // TODO: full template → PlatformNode tree codegen\n"); out.push_str(" PlatformNode::element(\"div\")\n"); out.push_str(" }\n"); out.push_str("}\n\n"); } Ok(out) } fn gen_component(&self, comp: &Component) -> CompileResult { let mut out = String::new(); out.push_str(&format!("class {} extends Component {{\n", comp.name)); // constructor out.push_str(" constructor(props = {}) {\n"); out.push_str(" super();\n"); out.push_str(" this.props = props;\n"); out.push_str(" this._graph = new Graph();\n"); out.push_str(" this._stateNodes = {};\n"); out.push_str(" this._state = {};\n"); // Validate and set props if !comp.props.is_empty() { out.push_str(" // Props\n"); for prop in &comp.props { let default_js = prop.default.as_deref() .map(|d| translate_el_to_js(d)) .unwrap_or_else(|| "undefined".to_owned()); out.push_str(&format!( " this._props_{name} = props.{name} !== undefined ? props.{name} : {default};\n", name = prop.name, default = default_js, )); } } // Seed state nodes if !comp.state.is_empty() { out.push_str(" // State nodes (Engram graph seeds)\n"); for s in &comp.state { let initial_js = translate_el_to_js(&s.initial); out.push_str(&format!( " this._stateNodes['{name}'] = this._graph.seed({{ type: 'state', name: '{name}', content: {initial} }});\n", name = s.name, initial = initial_js, )); out.push_str(&format!( " this._state['{name}'] = {initial};\n", name = s.name, initial = initial_js, )); } } // Subscribe to state node changes for reactive re-render if !comp.state.is_empty() { out.push_str(" // Subscribe to graph activation events\n"); out.push_str(" for (const [key, nodeId] of Object.entries(this._stateNodes)) {\n"); out.push_str(" this._graph.subscribe(nodeId, (node) => {\n"); out.push_str(" this._state[key] = node.content;\n"); out.push_str(" if (this._renderer) this._renderer.patch();\n"); out.push_str(" });\n"); out.push_str(" }\n"); } out.push_str(" }\n\n"); // setState method out.push_str(" setState(name, value) {\n"); out.push_str(" if (this._stateNodes[name] !== undefined) {\n"); out.push_str(" this._graph.update(this._stateNodes[name], value);\n"); out.push_str(" }\n"); out.push_str(" }\n\n"); // User-defined methods for method in &comp.methods { out.push_str(&self.gen_method(method, comp)?); out.push('\n'); } // render() out.push_str(" render() {\n"); out.push_str(" const __self = this;\n"); // Expose state variables for s in &comp.state { out.push_str(&format!( " const {name} = this._state['{name}'];\n", name = s.name, )); } // Expose props for p in &comp.props { out.push_str(&format!( " const {name} = this._props_{name};\n", name = p.name, )); } out.push_str(" return `"); let template_js = self.gen_template_nodes(&comp.template.nodes, comp)?; out.push_str(&template_js); out.push_str("`;\n"); out.push_str(" }\n\n"); out.push_str("}\n"); Ok(out) } fn gen_method(&self, method: &Method, comp: &Component) -> CompileResult { let mut out = String::new(); let params: Vec = method.params.iter() .map(|(n, _)| n.clone()) .collect(); out.push_str(&format!( " {}({}) {{\n", method.name, params.join(", ") )); // Expose state in method body for s in &comp.state { out.push_str(&format!( " const {name} = this._state['{name}'];\n", name = s.name )); } // Translate body — simple pass-through with setState substitution let body = translate_method_body(&method.body, comp); for line in body.lines() { out.push_str(&format!(" {}\n", line)); } out.push_str(" }\n"); Ok(out) } fn gen_template_nodes(&self, nodes: &[TemplateNode], comp: &Component) -> CompileResult { let mut out = String::new(); for node in nodes { out.push_str(&self.gen_template_node(node, comp)?); } Ok(out) } fn gen_template_node(&self, node: &TemplateNode, comp: &Component) -> CompileResult { match node { TemplateNode::Text(t) => Ok(t.clone()), TemplateNode::Interpolation(expr) => { let js_expr = translate_interpolation(expr, comp); Ok(format!("${{{} }}", js_expr)) } TemplateNode::Element { tag, attrs, children } => { let mut out = format!("<{}", tag); for attr in attrs { out.push_str(&self.gen_attr(attr, comp)?); } if children.is_empty() { out.push_str(&format!(" data-el-tag=\"{}\">", tag)); out.push_str(&format!("", tag)); } else { out.push_str(&format!(" data-el-tag=\"{}\">", tag)); out.push_str(&self.gen_template_nodes(children, comp)?); out.push_str(&format!("", tag)); } Ok(out) } TemplateNode::Component { name, props } => { // Render as inline component call let mut prop_entries: Vec = Vec::new(); for prop in props { match prop { Attr::Static { name: pn, value } => { prop_entries.push(format!("{}: \"{}\"", pn, value)); } Attr::Dynamic { name: pn, expr } => { let js = translate_interpolation(expr, comp); prop_entries.push(format!("{}: {}", pn, js)); } Attr::EventHandler { event, handler } => { let js = translate_handler(handler, comp); prop_entries.push(format!("on{}: {}", capitalize(event), js)); } Attr::BoolAttr { name: pn, expr } => { prop_entries.push(format!("{}: {}", pn, expr)); } } } let props_js = format!("{{ {} }}", prop_entries.join(", ")); Ok(format!("${{__self._child({}, {})}}", name, props_js)) } TemplateNode::If { condition, then, else_ } => { let cond_js = translate_interpolation(condition, comp); let then_html = self.gen_template_nodes(then, comp)?; let else_html = if let Some(els) = else_ { self.gen_template_nodes(els, comp)? } else { String::new() }; Ok(format!( "${{({}) ? `{}` : `{}`}}", cond_js, then_html, else_html )) } TemplateNode::Each { items, item_name, children } => { let items_js = translate_interpolation(items, comp); let child_html = self.gen_template_nodes(children, comp)?; // Generate a map over the array Ok(format!( "${{({}).map(({}) => `{}`).join('')}}", items_js, item_name, child_html )) } TemplateNode::Activate { query, result_name, children } => { let child_html = self.gen_template_nodes(children, comp)?; Ok(format!( "${{((__self._graph.search(\"{}\")) || []).map(({}) => `{}`).join('')}}", query, result_name, child_html )) } } } fn gen_attr(&self, attr: &Attr, comp: &Component) -> CompileResult { match attr { Attr::Static { name, value } => { Ok(format!(" {}=\"{}\"", name, value)) } Attr::Dynamic { name, expr } => { let js = translate_interpolation(expr, comp); Ok(format!(" {}=\"${{{} }}\"", name, js)) } Attr::BoolAttr { name, expr } => { let js = translate_interpolation(expr, comp); Ok(format!(" ${{({}) ? '{}' : '' }}", js, name)) } Attr::EventHandler { event, handler } => { // We use data attributes to defer event binding let js = translate_handler(handler, comp); // Inline handler via data attribute — the renderer will bind these Ok(format!(" data-el-{}=\"{}\"", event, escape_attr(&js))) } } } } /// Translate an el-ui expression to JavaScript. /// Handles state assignments like `count = count + 1` → `__self.setState('count', count + 1)` fn translate_interpolation(expr: &str, comp: &Component) -> String { translate_expr(expr, comp) } fn translate_expr(expr: &str, comp: &Component) -> String { let state_names: Vec<&str> = comp.state.iter().map(|s| s.name.as_str()).collect(); // Arrow functions: passthrough // State assignment: `name = value` → `__self.setState('name', value)` let trimmed = expr.trim(); // Check for simple assignment: `ident = expr` if let Some(result) = try_translate_assignment(trimmed, &state_names) { return result; } // Arrow function containing assignment: `() => count = count + 1` if trimmed.starts_with('(') || trimmed.starts_with("e =>") || trimmed.starts_with("() =>") { return translate_arrow_fn(trimmed, &state_names); } // Otherwise pass through as-is trimmed.to_owned() } fn try_translate_assignment(expr: &str, state_names: &[&str]) -> Option { // Match: `name = value` where name is a state variable // Must not be `==` (equality) let parts: Vec<&str> = expr.splitn(2, '=').collect(); if parts.len() == 2 { let lhs = parts[0].trim(); let rhs = parts[1].trim(); // Ensure it's not `==` or `!=` or `<=` or `>=` if !rhs.starts_with('=') && !lhs.ends_with('!') && !lhs.ends_with('<') && !lhs.ends_with('>') { if state_names.contains(&lhs) { return Some(format!("__self.setState('{}', {})", lhs, rhs)); } } } None } fn translate_arrow_fn(expr: &str, state_names: &[&str]) -> String { // Translate assignments inside arrow functions // This is a best-effort string transformation let mut result = expr.to_owned(); for name in state_names { // Replace `name = ` with `__self.setState('name', ` ... `)` is too complex // for a simple string replacement, but we can handle common patterns. // Pattern: `name = expr` at end of arrow fn or in braces let pat = format!("{} = ", name); if let Some(idx) = result.find(&pat) { // Check it's not == let after = &result[idx + pat.len()..]; // Simple case: `() => count = count + 1` let prefix = &result[..idx]; result = format!("{}__self.setState('{}', {})", prefix, name, after.trim_end_matches(')')); } } result } fn translate_handler(handler: &str, comp: &Component) -> String { translate_expr(handler, comp) } /// Translate method body — replace bare state assignments with setState calls. fn translate_method_body(body: &str, comp: &Component) -> String { let state_names: Vec<&str> = comp.state.iter().map(|s| s.name.as_str()).collect(); let mut lines: Vec = Vec::new(); for line in body.lines() { let trimmed = line.trim(); if let Some(translated) = try_translate_assignment(trimmed, &state_names) { lines.push(format!("{};", translated)); } else if trimmed.starts_with("return ") { lines.push(trimmed.to_owned()); } else { lines.push(trimmed.to_owned()); } } lines.join("\n") } fn translate_el_to_js(expr: &str) -> String { let s = expr.trim(); // Fn types — translate to null (not a valid JS value, handled at runtime) if s.starts_with("Fn") { return "null".into(); } // Boolean if s == "true" { return "true".into(); } if s == "false" { return "false".into(); } // String literal if s.starts_with('"') { return s.replace('"', "\"").to_owned(); } // Numbers if s.parse::().is_ok() { return s.to_owned(); } if s.parse::().is_ok() { return s.to_owned(); } // Empty string / void if s.is_empty() { return "null".into(); } s.to_owned() } fn capitalize(s: &str) -> String { let mut c = s.chars(); match c.next() { None => String::new(), Some(f) => f.to_uppercase().collect::() + c.as_str(), } } fn escape_attr(s: &str) -> String { s.replace('"', """).replace('\'', "'") }