feat: el-ui — activation-based frontend framework, spreading activation reactivity, graph state
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
//! 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.
|
||||
|
||||
use crate::ast::*;
|
||||
use crate::error::CompileResult;
|
||||
|
||||
pub struct Codegen {
|
||||
runtime_path: String,
|
||||
}
|
||||
|
||||
impl Codegen {
|
||||
pub fn new(runtime_path: &str) -> Self {
|
||||
Self { runtime_path: runtime_path.to_owned() }
|
||||
}
|
||||
|
||||
pub fn generate(&self, components: &[Component]) -> CompileResult<String> {
|
||||
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)
|
||||
}
|
||||
|
||||
fn gen_component(&self, comp: &Component) -> CompileResult<String> {
|
||||
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<String> {
|
||||
let mut out = String::new();
|
||||
let params: Vec<String> = 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<String> {
|
||||
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<String> {
|
||||
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<String> = 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<String> {
|
||||
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<String> {
|
||||
// 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<String> = 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::<i64>().is_ok() { return s.to_owned(); }
|
||||
if s.parse::<f64>().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::<String>() + c.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_attr(s: &str) -> String {
|
||||
s.replace('"', """).replace('\'', "'")
|
||||
}
|
||||
Reference in New Issue
Block a user