diff --git a/Cargo.toml b/Cargo.toml index 6241e2a..e44b5c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,17 @@ [workspace] members = [ - "crates/el-ui-compiler", - "crates/el-platform", - "crates/el-services", - "crates/el-aop", - "crates/el-auth", - "crates/el-publish", - "crates/el-identity", - "crates/el-style", - "crates/el-layout", - "crates/el-i18n", - "crates/el-config", - "crates/el-secrets", + "vessels/el-ui-compiler", + "vessels/el-platform", + "vessels/el-services", + "vessels/el-aop", + "vessels/el-auth", + "vessels/el-publish", + "vessels/el-identity", + "vessels/el-style", + "vessels/el-layout", + "vessels/el-i18n", + "vessels/el-config", + "vessels/el-secrets", "examples/profile-card", ] resolver = "2" diff --git a/crates/el-ui-compiler/src/codegen.rs b/crates/el-ui-compiler/src/codegen.rs deleted file mode 100644 index c53849a..0000000 --- a/crates/el-ui-compiler/src/codegen.rs +++ /dev/null @@ -1,581 +0,0 @@ -//! 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('\'', "'") -} diff --git a/examples/profile-card/Cargo.toml b/examples/profile-card/Cargo.toml index 1a0dabd..098248a 100644 --- a/examples/profile-card/Cargo.toml +++ b/examples/profile-card/Cargo.toml @@ -9,8 +9,8 @@ name = "profile-card" path = "src/main.rs" [dependencies] -el-style = { path = "../../crates/el-style" } -el-layout = { path = "../../crates/el-layout" } -el-i18n = { path = "../../crates/el-i18n" } -el-config = { path = "../../crates/el-config" } -el-secrets = { path = "../../crates/el-secrets" } +el-style = { path = "../../vessels/el-style" } +el-layout = { path = "../../vessels/el-layout" } +el-i18n = { path = "../../vessels/el-i18n" } +el-config = { path = "../../vessels/el-config" } +el-secrets = { path = "../../vessels/el-secrets" } diff --git a/spec/framework.md b/spec/framework.md index bb8f771..85db900 100644 --- a/spec/framework.md +++ b/spec/framework.md @@ -1,76 +1,98 @@ # el-ui Framework Specification -Version 0.1.0 — April 2026 +Version 1.1.0 — April 30, 2026 --- ## Overview -el-ui is a frontend framework where state is an Engram graph and reactivity is spreading activation. - -Every framework answers the same question differently: **what re-renders when state changes?** +el-ui is a frontend framework where state is an Engram graph and reactivity is spreading activation. Every framework must answer the same question: what re-renders when state changes? el-ui's answer is: whatever the spreading activation algorithm determines is relevant. | Framework | Reactivity model | |-----------|-----------------| | React | Virtual DOM diffing | -| Vue | Dependency tracking (Proxy-based) | -| Svelte | Compile-time analysis | -| **el-ui** | **Spreading activation over a state graph** | +| Vue | Dependency tracking (Proxy-based reactive primitives) | +| Svelte | Compile-time static analysis | +| Solid | Fine-grained signal-based subscriptions | +| **el-ui** | **Spreading activation over a typed state graph** | -The core insight: treating component state as a graph of related nodes, and using the same spreading activation algorithm as the Engram knowledge engine to determine what to update. Reactivity is not declared, tracked, or diffed — it is activated and propagated, exactly as associative memory works in biological neural networks. +The core insight: component state is represented as a graph of related nodes, and the same spreading activation algorithm used by the Engram knowledge engine determines which components need to update. Reactivity is not declared, tracked, or diffed — it is activated and propagated, exactly as associative memory works in biological neural networks. --- -## 1. Activation-Based Reactivity +## 1. Activation-Based Reactivity Model ### 1.1 The Model Every piece of state in an el-ui application is a **node** in an in-browser Engram graph. Nodes have: -- `id` — UUID, the node's identity -- `type` — `'state'`, `'route'`, or user-defined -- `name` — the state variable name -- `content` — the current value -- `importance` — a salience weight `[0, 1]`, boosted when the node is activated +| Field | Type | Description | +|-------|------|-------------| +| `id` | String (UUID) | Node's permanent identity | +| `type` | String | `'state'`, `'route'`, or user-defined | +| `name` | String | State variable name | +| `content` | Any | Current value | +| `importance` | Float [0, 1] | Salience weight; boosted on update | -Nodes are connected by **edges** with a `weight` `[0, 1]` and a `relation` label. +Nodes are connected by directed **edges** with: -When state changes (via `setState()`), spreading activation runs from the changed node: +| Field | Type | Description | +|-------|------|-------------| +| `weight` | Float [0, 1] | Connection strength | +| `relation` | String | Semantic label of the relationship | + +### 1.2 Activation Formula + +When state changes via `setState()`, spreading activation runs from the changed node through the graph: ``` strength = parent_strength × edge.weight × target.importance ``` -This is multiplicative, matching the Engram core engine (`engram-core/src/activation.rs`). Every factor must be non-trivial for a path to propagate — weak edges, dormant nodes, and irrelevant connections die immediately. Components subscribed to nodes in the resulting **activation surface** update. Everything else stays still. +This formula is identical to the Engram core spreading activation formula, with the semantic similarity factor (cosine_sim) omitted because the in-browser graph does not yet carry embedding vectors. In a future version connected to the Engram server, the full four-factor formula applies: -### 1.2 Why Multiplication, Not Addition +``` +strength = parent_strength × edge.weight × target.importance × cosine_sim(query, target) +``` -Addition allows many weak signals to accumulate into false relevance — an observation from the Engram architecture. The brain's associative memory is conjunctive: a path requires ALL its links to be strong. Multiplication enforces this. If any factor is near zero, the path dies. +### 1.3 Why Multiplication, Not Addition -### 1.3 Pruning +Addition allows many weak signals to accumulate into false relevance. The brain's associative memory is conjunctive: an activated path requires ALL its links to be strong. Multiplication enforces this. If any factor is near zero — weak edge, dormant node, or irrelevant connection — the path dies immediately. This is not a performance optimization; it is a semantic property of associative retrieval. -Paths with activation strength below `PRUNE_THRESHOLD` (default: `0.01`) are cut. This prevents exponential blowup in large state graphs and models the brain's attention filter. +### 1.4 Pruning Threshold -### 1.4 Importance Boost +Paths with activation strength below `PRUNE_THRESHOLD` (default: `0.01`) are cut. This prevents exponential blowup in large state graphs and models the brain's attention filter. All nodes not reached above the threshold are not considered for re-render. -When a node is updated, its importance increases by 0.1 (capped at 1.0). Recently-changed state is more salient — it activates more easily in future propagation. This mirrors long-term potentiation: frequently-used pathways become lower-resistance. +### 1.5 Importance Boost (Long-Term Potentiation Analog) + +When a state node is updated, its `importance` increases by 0.1 (capped at 1.0): + +``` +node.importance = Math.min(1.0, node.importance + 0.1) +``` + +Recently-changed state becomes more salient — it activates more easily in future propagation. This mirrors long-term potentiation: frequently-used pathways become lower-resistance. State that changes often gains importance; state that never changes fades toward background salience. + +### 1.6 Activation Surface + +The **activation surface** is the set of nodes reached during a spreading activation pass, i.e., all nodes with `strength > PRUNE_THRESHOLD`. Components subscribed to any node in the activation surface receive update notifications and re-render. --- ## 2. Component Definition Syntax -Components are defined in `.el` files (the same extension as el source). A component is a specialized el module. +Components are defined in `.el` files (shared extension with the El programming language). A component is a specialized el module with four optional sections. -### 2.1 Structure +### 2.1 Component Structure ``` component ComponentName { props { - // optional prop declarations + // prop declarations } state { - // optional state declarations + // state declarations } fn methodName(param: Type) -> ReturnType { @@ -83,26 +105,30 @@ component ComponentName { } ``` -All four sections (`props`, `state`, methods, `template`) are optional. Components with no template render an empty string. +All four sections are optional. Components with no template render an empty string. Multiple `fn` blocks are allowed; multiple `props` or `state` blocks are not (only the last one is used). ### 2.2 Props -Props are inputs from the parent component. They are read-only inside the component. +Props are read-only inputs from the parent component. ``` props { - label: String // required - variant: String = "primary" // optional, with default + label: String // required prop + variant: String = "primary" // optional with default disabled: Bool = false // boolean with default onClick: Fn() -> Void // function prop } ``` -Prop types: `String`, `Int`, `Float`, `Bool`, `Fn(...) -> T`, `[T]`, `T?`. +Supported prop types: `String`, `Int`, `Float`, `Bool`, `Fn(...) -> T`. + +**Note:** Array (`[T]`) and optional (`T?`) type syntax is not currently parsed by the compiler. The parser reads a single identifier as the type name. + +Props are accessed in generated code as `this._props_{name}` (not `this.props.name`). In method and template bodies, the compiler exposes each prop as a local constant named `{name}`. ### 2.3 State -State declarations create nodes in the component's Engram graph. +State declarations create nodes in the component's Engram graph. Every state variable requires an initial value. ``` state { @@ -113,20 +139,23 @@ state { ``` Each state variable becomes: -1. A node in `this._graph` (seeded at construction time) -2. A reactive binding: changing the value via `setState()` triggers activation +1. A node in `this._graph` (seeded at construction time with `type: 'state'`), tracked by ID in `this._stateNodes`. +2. A mirrored value in `this._state` for direct read access. +3. A reactive binding: changing the value via `setState()` triggers activation and re-render. + +In method and template bodies, the compiler exposes each state variable as a local constant named `{name}` (reading from `this._state`). ### 2.4 Methods -Methods are `fn` definitions inside the component body. They have access to the component's current state and can call `setState()`. +Methods are `fn` definitions inside the component body. State assignments in method bodies compile to `setState()` calls. ``` fn increment() -> Void { - count = count + 1 + count = count + 1 // compiles to: __self.setState('count', count + 1) } ``` -State assignments in method bodies (`count = count + 1`) are compiled to `setState('count', count + 1)` calls. +The generated JavaScript method exposes all state variables as local `const` bindings before executing the body. The variable `__self` refers to the component instance. --- @@ -134,7 +163,7 @@ State assignments in method bodies (`count = count + 1`) are compiled to `setSta ### 3.1 Interpolation -Embed any expression with `{expr}`: +Any expression embedded in `{expr}`: ```

{count}

@@ -144,7 +173,7 @@ Embed any expression with `{expr}`: ### 3.2 Event Binding -Bind DOM events with `on:event={handler}`: +DOM events bound with `on:event={handler}`: ``` @@ -154,7 +183,7 @@ Bind DOM events with `on:event={handler}`: Supported events: `click`, `input`, `change`, `mouseenter`, `mouseleave`, `keydown`, `keyup`, `keypress`, `focus`, `blur`, `submit`, `mousedown`, `mouseup`, `dblclick`, `contextmenu`. -The compiler emits `data-el-{event}` attributes. The renderer binds handlers at mount time and rebinds after each patch. +The compiler emits `data-el-{event}` attributes on elements. The renderer binds handlers at mount time using `new Function(...)` evaluated in the component's scope (`__self`), and rebinds after each patch. ### 3.3 Dynamic Attributes @@ -172,32 +201,30 @@ The compiler emits `data-el-{event}` attributes. The renderer binds handlers at ### 3.5 Boolean Attributes +The parser recognises a fixed set of boolean attribute names: `disabled`, `checked`, `readonly`, `required`, `multiple`, `selected`. These emit the attribute name when the expression is truthy, nothing when falsy. + ``` ``` -Boolean attributes emit the attribute name if the expression is truthy, nothing if falsy. +A standalone attribute with no value (e.g., `"#, + app = app, + label = html_escape_codegen(label), + ) + } + + SemanticPrimitive::Text { content, concept } => { + let tag = concept.html_tag(); + let class = match concept { + TextConcept::Heading { .. } => format!(" class=\"heading\""), + TextConcept::Body => format!(" class=\"body-text\""), + TextConcept::Caption => format!(" class=\"caption\""), + TextConcept::Label => format!(" class=\"label\""), + TextConcept::Code => format!(" class=\"code\""), + }; + format!( + "<{tag}{class}>{content}", + tag = tag, + class = class, + content = html_escape_codegen(content), + ) + } + + SemanticPrimitive::Container { children, layout } => { + let flex_dir = layout.css_flex_direction(); + let child_html: Vec = + children.iter().map(generate_server_html).collect(); + format!( + r#"
{children}
"#, + flex_dir = flex_dir, + children = child_html.join(""), + ) + } + + SemanticPrimitive::Input { binding, hint, appearance } => { + let placeholder = hint.as_deref().unwrap_or(""); + let app = appearance.css_class(); + format!( + r#""#, + app = app, + binding = binding, + placeholder = html_escape_codegen(placeholder), + ) + } + + SemanticPrimitive::Image { src, alt } => { + format!( + r#"{alt}"#, + src = html_escape_codegen(src), + alt = html_escape_codegen(alt), + ) + } + + SemanticPrimitive::Toggle { binding, label } => { + format!( + r#""#, + binding = binding, + label = html_escape_codegen(label), + ) + } + + SemanticPrimitive::List { items_binding, item_name, item_template } => { + // At SSR time the list is empty — the client hydrates with real data. + let placeholder_item = generate_server_html(item_template); + format!( + r#"
"#, + items_binding = items_binding, + item_name = item_name, + placeholder_item = placeholder_item, + ) + } + } +} + +// --------------------------------------------------------------------------- +// Helpers shared across codegen functions +// --------------------------------------------------------------------------- + +/// Minimal HTML escape for content emitted into generated source strings. +fn html_escape_codegen(s: &str) -> String { + s.replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) +} + +/// Sanitize a display string into a valid Rust identifier fragment. +fn sanitize_id(s: &str) -> String { + s.chars() + .map(|c| if c.is_alphanumeric() { c } else { '_' }) + .collect::() + .to_lowercase() +} diff --git a/crates/el-ui-compiler/src/error.rs b/vessels/el-ui-compiler/src/error.rs similarity index 100% rename from crates/el-ui-compiler/src/error.rs rename to vessels/el-ui-compiler/src/error.rs diff --git a/crates/el-ui-compiler/src/lexer.rs b/vessels/el-ui-compiler/src/lexer.rs similarity index 100% rename from crates/el-ui-compiler/src/lexer.rs rename to vessels/el-ui-compiler/src/lexer.rs diff --git a/crates/el-ui-compiler/src/lib.rs b/vessels/el-ui-compiler/src/lib.rs similarity index 99% rename from crates/el-ui-compiler/src/lib.rs rename to vessels/el-ui-compiler/src/lib.rs index 6a05628..a932372 100644 --- a/crates/el-ui-compiler/src/lib.rs +++ b/vessels/el-ui-compiler/src/lib.rs @@ -11,6 +11,7 @@ pub mod codegen; pub mod error; pub mod lexer; pub mod parser; +pub mod semantic; pub use ast::{Attr, Component, Method, PropDef, StateDef, Template, TemplateNode}; diff --git a/vessels/el-ui-compiler/src/main.el b/vessels/el-ui-compiler/src/main.el new file mode 100644 index 0000000..f4d4fef --- /dev/null +++ b/vessels/el-ui-compiler/src/main.el @@ -0,0 +1,142 @@ +// el-ui-compiler — El→browser-target component compiler (STUB). +// +// The Rust crate transforms `.el` component files into JavaScript using a +// real lexer/parser/codegen pipeline backed by the el-ui runtime +// (`el-ui.js`). This El surface is a placeholder. +// +// RUNTIME GAP — load-bearing: +// `elc` (the bootstrap El compiler) currently emits C only. There is no +// JavaScript or WebAssembly backend yet, so the El surface cannot +// actually compile a component to JS today. +// +// What this vessel provides: +// - The shape of the public API (Compiler with runtime_path, +// compile_component, compile_app). +// - A `CompileResult` record consumers can branch on. +// - Stub bodies that return a deterministic "not yet implemented" +// module string so downstream tooling can integration-test against +// a stable contract. +// +// When elc gains a JS/Wasm backend, swap the bodies of `lex`, `parse`, +// `semantic_check`, and `codegen` for real implementations — the public +// surface should not need to change. + +// ── Errors ────────────────────────────────────────────────────────────────── + +fn err_lex() -> String { "elc.lex" } +fn err_parse() -> String { "elc.parse" } +fn err_semantic() -> String { "elc.semantic" } +fn err_codegen() -> String { "elc.codegen" } +fn err_backend_missing() -> String { "elc.backend_missing" } + +// ── Compile result ────────────────────────────────────────────────────────── +// +// Modeled as JSON since the runtime's product types and field accessors +// don't yet support the Bool/String mix we want here cleanly. + +fn result_ok(code: String) -> String { + "{\"ok\":true,\"code\":" + json_quote(code) + ",\"error\":\"\"}" +} + +fn result_err(error: String) -> String { + "{\"ok\":false,\"code\":\"\",\"error\":\"" + error + "\"}" +} + +// Minimal JSON string quoter (escape backslash + double-quote only). +// elc still doesn't expose a json_string_quote builtin. +fn json_quote(s: String) -> String { + let escaped: String = str_replace(s, "\\", "\\\\") + let escaped = str_replace(escaped, "\"", "\\\"") + "\"" + escaped + "\"" +} + +fn result_ok_p(result_json: String) -> Bool { + json_get_bool(result_json, "ok") +} + +// ── Compiler config ───────────────────────────────────────────────────────── + +fn compiler_default_runtime_path() -> String { "./el-ui.js" } +fn compiler_default_target() -> String { "js" } + +fn compiler_new() -> String { + "{\"runtime_path\":\"./el-ui.js\",\"target\":\"js\"}" +} + +fn compiler_with_runtime(c_json: String, path: String) -> String { + json_set(c_json, "runtime_path", json_quote(path)) +} + +fn compiler_with_target(c_json: String, target: String) -> String { + json_set(c_json, "target", json_quote(target)) +} + +fn compiler_runtime_path(c_json: String) -> String { + json_get_string(c_json, "runtime_path") +} + +fn compiler_target(c_json: String) -> String { + json_get_string(c_json, "target") +} + +// ── Pipeline stages (stubs) ───────────────────────────────────────────────── +// +// Real implementations live in the Rust crate (lexer.rs, parser.rs, +// semantic.rs, codegen.rs). These El stubs only validate inputs and +// produce a marker payload so callers can wire the contract. + +fn lex(source: String) -> String { + if str_eq(source, "") { return "" } + "[]" // would be tokens JSON +} + +fn parse(tokens_json: String) -> String { + if str_eq(tokens_json, "") { return "" } + "[]" // would be Component AST JSON +} + +fn semantic_check(ast_json: String) -> String { + ast_json // pass-through stub +} + +fn codegen(c_json: String, ast_json: String) -> String { + let runtime: String = compiler_runtime_path(c_json) + let preamble: String = "// AUTOGENERATED by el-ui-compiler (STUB)\n" + let import_line: String = "import { register, h } from \"" + runtime + "\";\n" + let body: String = "throw new Error(\"" + err_backend_missing() + + ": el-ui-compiler has no JS backend yet; emit C with elc instead\");\n" + preamble + import_line + body +} + +// ── Public entry points ───────────────────────────────────────────────────── + +fn compile_component(c_json: String, source: String) -> String { + let tokens: String = lex(source) + if str_eq(tokens, "") { return result_err(err_lex()) } + let ast: String = parse(tokens) + if str_eq(ast, "") { return result_err(err_parse()) } + let checked: String = semantic_check(ast) + if str_eq(checked, "") { return result_err(err_semantic()) } + let js: String = codegen(c_json, checked) + result_ok(js) +} + +// `entry_source` is the app entry; `components_json` is a JSON object map of +// name -> source text. We compile each component first, then the entry. +fn compile_app(c_json: String, entry_source: String, components_json: String) -> String { + // RUNTIME GAP: no json_object_keys() yet, so we cannot iterate the map + // generically here. Real implementation will iterate and short-circuit + // on first compile error. For the stub we just compile the entry. + compile_component(c_json, entry_source) +} + +// ── Entry — smoke test ────────────────────────────────────────────────────── + +let c: String = compiler_new() +let c = compiler_with_runtime(c, "./el-ui.js") +let r: String = compile_component(c, "component Hello { template {

hi

} }") +if result_ok_p(r) { + println("[el-ui-compiler] STUB compiled (target=" + compiler_target(c) + ")") +} else { + println("[el-ui-compiler] error: " + json_get_string(r, "error")) +} diff --git a/crates/el-ui-compiler/src/main.rs b/vessels/el-ui-compiler/src/main.rs similarity index 100% rename from crates/el-ui-compiler/src/main.rs rename to vessels/el-ui-compiler/src/main.rs diff --git a/crates/el-ui-compiler/src/parser.rs b/vessels/el-ui-compiler/src/parser.rs similarity index 100% rename from crates/el-ui-compiler/src/parser.rs rename to vessels/el-ui-compiler/src/parser.rs diff --git a/vessels/el-ui-compiler/src/semantic.rs b/vessels/el-ui-compiler/src/semantic.rs new file mode 100644 index 0000000..093d60e --- /dev/null +++ b/vessels/el-ui-compiler/src/semantic.rs @@ -0,0 +1,523 @@ +//! Semantic primitive layer — EBD-driven appearance and layout concepts. +//! +//! # Design rationale +//! +//! Traditional UI frameworks describe *appearance*: a red button with rounded corners. +//! El UI describes *concepts*: a destructive action trigger. The platform then maps +//! that concept to its native visual convention — red on iOS, a warning style on +//! macOS, an outlined filled button on Android, etc. +//! +//! This is the EBD principle applied to UI: the component expresses *what something is*, +//! the platform backend expresses *how it looks*. +//! +//! # The semantic → PlatformNode pipeline +//! +//! ```text +//! .el template +//! ↓ +//! SemanticPrimitive (this module) +//! ↓ +//! platform codegen (codegen.rs — per-platform dispatch) +//! ↓ +//! PlatformNode (el-platform IR) +//! ↓ +//! platform backend (el-platform — native API calls) +//! ``` + +// Semantic types drive codegen decisions. They do not directly construct +// PlatformNodes at compile time — the generated *source code* strings reference +// PlatformNode from the el-platform crate, which is a separate runtime crate. +// The compiler emits Rust source that will reference el_platform::PlatformNode. + +// --------------------------------------------------------------------------- +// Appearance concepts +// --------------------------------------------------------------------------- + +/// EBD appearance concept — WHAT a control is, not HOW it looks. +/// +/// Each platform maps these to its visual conventions: +/// - `Action` → primary button style (blue on Apple, filled on Material) +/// - `Destructive` → warning/danger style (red tint on Apple, error colour on Material) +/// - `Secondary` → subdued style (grey on Apple, outlined on Material) +/// - `Navigation` → used for link-like / back-navigation elements +/// - `Informational` → read-only display elements +/// - `Structural` → layout containers with no interactive meaning +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AppearanceConcept { + /// Primary action trigger — the most prominent interactive element. + Action, + /// Dangerous or irreversible action (delete, reset, etc.). + Destructive, + /// Less prominent action — secondary to the main action on screen. + Secondary, + /// Navigational element — links, back buttons, tab items. + Navigation, + /// Displays information — labels, status indicators, badges. + Informational, + /// Layout or structural container — no interactive semantics. + Structural, +} + +impl AppearanceConcept { + /// Canonical CSS class suffix for the server/web targets. + pub fn css_class(&self) -> &'static str { + match self { + Self::Action => "action", + Self::Destructive => "destructive", + Self::Secondary => "secondary", + Self::Navigation => "navigation", + Self::Informational => "informational", + Self::Structural => "structural", + } + } + + /// AppKit bezel style constant for the macOS target. + pub fn appkit_bezel_style(&self) -> &'static str { + match self { + Self::Action => "NSBezelStyle::rounded()", + Self::Destructive => "NSBezelStyle::rounded()", // red tint applied via theme token + Self::Secondary => "NSBezelStyle::smallSquare()", + Self::Navigation => "NSBezelStyle::recessed()", + Self::Informational => "NSBezelStyle::inline()", + Self::Structural => "NSBezelStyle::rounded()", + } + } + + /// UIKit button configuration for the iOS target. + pub fn uikit_button_config(&self) -> &'static str { + match self { + Self::Action => "UIButton.Configuration.filled()", + Self::Destructive => "UIButton.Configuration.filled()", // tintColor = .systemRed + Self::Secondary => "UIButton.Configuration.gray()", + Self::Navigation => "UIButton.Configuration.plain()", + Self::Informational => "UIButton.Configuration.plain()", + Self::Structural => "UIButton.Configuration.plain()", + } + } + + /// Jetpack Compose button composable for the Android target. + pub fn compose_button_type(&self) -> &'static str { + match self { + Self::Action => "Button", + Self::Destructive => "Button", // containerColor = MaterialTheme.colorScheme.error + Self::Secondary => "OutlinedButton", + Self::Navigation => "TextButton", + Self::Informational => "TextButton", + Self::Structural => "TextButton", + } + } + + /// WinUI control style for the Windows target. + pub fn winui_button_style(&self) -> &'static str { + match self { + Self::Action => "AccentButtonStyle", + Self::Destructive => "AccentButtonStyle", // Background = DangerBrush via theme + Self::Secondary => "DefaultButtonStyle", + Self::Navigation => "NavigationBackButtonNormalStyle", + Self::Informational => "DefaultButtonStyle", + Self::Structural => "DefaultButtonStyle", + } + } + + /// GTK CSS class for the Linux target. + pub fn gtk_css_class(&self) -> &'static str { + match self { + Self::Action => "suggested-action", + Self::Destructive => "destructive-action", + Self::Secondary => "flat", + Self::Navigation => "flat", + Self::Informational => "flat", + Self::Structural => "flat", + } + } +} + +// --------------------------------------------------------------------------- +// Layout concepts +// --------------------------------------------------------------------------- + +/// Layout concept — how children are arranged. +/// +/// Platform mapping: +/// - `Stack` → VStack / UIStackView(vertical) / Column / StackPanel / GtkBox(vertical) +/// - `Row` → HStack / UIStackView(horizontal) / Row / StackPanel(horizontal) / GtkBox(horizontal) +/// - `Grid` → LazyVGrid / UICollectionView / LazyVerticalGrid / GridView +/// - `Overlay` → ZStack / UIView layering / Box / Canvas / GtkOverlay +/// - `Scroll` → ScrollView / UIScrollView / LazyColumn / ScrollViewer / GtkScrolledWindow +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LayoutConcept { + /// Vertical stack — children arranged top-to-bottom. + Stack, + /// Horizontal row — children arranged left-to-right. + Row, + /// Grid — children in a two-dimensional grid. + Grid, + /// Overlay — children layered on the z-axis. + Overlay, + /// Scrollable container — overflowing children are scrollable. + Scroll, +} + +impl LayoutConcept { + /// HTML/CSS flex direction for the web/server targets. + pub fn css_flex_direction(&self) -> &'static str { + match self { + Self::Stack => "column", + Self::Row => "row", + Self::Grid => "row wrap", // use CSS grid in practice + Self::Overlay => "row", // position: relative + absolute children + Self::Scroll => "column", + } + } + + /// AppKit stack view orientation. + pub fn appkit_orientation(&self) -> &'static str { + match self { + Self::Stack | Self::Grid | Self::Overlay | Self::Scroll => "NSUserInterfaceLayoutOrientation::vertical", + Self::Row => "NSUserInterfaceLayoutOrientation::horizontal", + } + } + + /// UIKit stack view axis. + pub fn uikit_axis(&self) -> &'static str { + match self { + Self::Stack | Self::Grid | Self::Overlay | Self::Scroll => "NSLayoutConstraint.Axis.vertical", + Self::Row => "NSLayoutConstraint.Axis.horizontal", + } + } + + /// Jetpack Compose layout composable. + pub fn compose_layout(&self) -> &'static str { + match self { + Self::Stack => "Column", + Self::Row => "Row", + Self::Grid => "LazyVerticalGrid", + Self::Overlay => "Box", + Self::Scroll => "LazyColumn", + } + } + + /// WinUI panel type. + pub fn winui_panel(&self) -> &'static str { + match self { + Self::Stack => "Microsoft.UI.Xaml.Controls.StackPanel", + Self::Row => "Microsoft.UI.Xaml.Controls.StackPanel", // Orientation=Horizontal + Self::Grid => "Microsoft.UI.Xaml.Controls.Grid", + Self::Overlay => "Microsoft.UI.Xaml.Controls.Canvas", + Self::Scroll => "Microsoft.UI.Xaml.Controls.ScrollViewer", + } + } + + /// GTK widget type. + pub fn gtk_widget(&self) -> &'static str { + match self { + Self::Stack => "GtkBox", // orientation=vertical + Self::Row => "GtkBox", // orientation=horizontal + Self::Grid => "GtkGrid", + Self::Overlay => "GtkOverlay", + Self::Scroll => "GtkScrolledWindow", + } + } +} + +// --------------------------------------------------------------------------- +// Text role concepts +// --------------------------------------------------------------------------- + +/// Text role concept — the semantic purpose of a piece of text. +/// +/// Platform mapping: +/// - `Heading` → large/bold system font at the appropriate level +/// - `Body` → default body text style +/// - `Caption` → small secondary text (subtitles, footnotes) +/// - `Label` → paired with a control — describes it +/// - `Code` → monospaced font, code block style +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TextConcept { + /// Document heading at the given level (1 = most prominent, 6 = least). + Heading { level: u8 }, + /// Default body text. + Body, + /// Small secondary text — captions, footnotes, helper text. + Caption, + /// A control label — paired with an input or button. + Label, + /// Monospaced code text. + Code, +} + +impl TextConcept { + /// HTML tag for the server/web targets. + pub fn html_tag(&self) -> String { + match self { + Self::Heading { level } => format!("h{}", level.clamp(&1, &6)), + Self::Body => "p".to_string(), + Self::Caption => "small".to_string(), + Self::Label => "label".to_string(), + Self::Code => "code".to_string(), + } + } + + /// AppKit font preset. + pub fn appkit_font(&self) -> &'static str { + match self { + Self::Heading { level: 1 } => "NSFont.systemFont(ofSize: 28, weight: .bold)", + Self::Heading { level: 2 } => "NSFont.systemFont(ofSize: 22, weight: .semibold)", + Self::Heading { .. } => "NSFont.systemFont(ofSize: 17, weight: .medium)", + Self::Body => "NSFont.systemFont(ofSize: 13)", + Self::Caption => "NSFont.systemFont(ofSize: 11, weight: .light)", + Self::Label => "NSFont.systemFont(ofSize: 13)", + Self::Code => "NSFont.monospacedSystemFont(ofSize: 13, weight: .regular)", + } + } + + /// UIKit font preset. + pub fn uikit_font(&self) -> &'static str { + match self { + Self::Heading { level: 1 } => "UIFont.preferredFont(forTextStyle: .largeTitle)", + Self::Heading { level: 2 } => "UIFont.preferredFont(forTextStyle: .title1)", + Self::Heading { level: 3 } => "UIFont.preferredFont(forTextStyle: .title2)", + Self::Heading { .. } => "UIFont.preferredFont(forTextStyle: .headline)", + Self::Body => "UIFont.preferredFont(forTextStyle: .body)", + Self::Caption => "UIFont.preferredFont(forTextStyle: .caption1)", + Self::Label => "UIFont.preferredFont(forTextStyle: .callout)", + Self::Code => "UIFont.monospacedSystemFont(ofSize: 14, weight: .regular)", + } + } + + /// Material 3 text style for Jetpack Compose. + pub fn compose_text_style(&self) -> &'static str { + match self { + Self::Heading { level: 1 } => "MaterialTheme.typography.headlineLarge", + Self::Heading { level: 2 } => "MaterialTheme.typography.headlineMedium", + Self::Heading { level: 3 } => "MaterialTheme.typography.headlineSmall", + Self::Heading { .. } => "MaterialTheme.typography.titleLarge", + Self::Body => "MaterialTheme.typography.bodyLarge", + Self::Caption => "MaterialTheme.typography.labelSmall", + Self::Label => "MaterialTheme.typography.labelMedium", + Self::Code => "MaterialTheme.typography.bodyMedium", // monospace font family + } + } +} + +// --------------------------------------------------------------------------- +// Semantic primitives +// --------------------------------------------------------------------------- + +/// A semantic UI primitive — CONCEPT, not visual element. +/// +/// Each variant describes *what* the element is, not *how* it looks. +/// The platform codegen translates these into native API calls. +/// +/// These are the building blocks; the platform backends decide the visual +/// expression. A `Button { appearance: Destructive }` looks different on +/// macOS (red-tinted rounded button) vs Android (error-coloured filled button) +/// but expresses the same concept. +#[derive(Debug, Clone, PartialEq)] +pub enum SemanticPrimitive { + /// An interactive button — triggers an action on press. + Button { + /// Visible label text. + label: String, + /// What kind of action this button represents. + appearance: AppearanceConcept, + /// Handler name from the component's method table, if any. + on_press: Option, + }, + + /// A text display element. + Text { + /// The content to display. + content: String, + /// The semantic role of this text. + concept: TextConcept, + }, + + /// A layout container with child primitives. + Container { + /// Child primitives, in render order. + children: Vec, + /// How children are arranged. + layout: LayoutConcept, + }, + + /// A text input field bound to a state variable. + Input { + /// State variable name this field is bound to (two-way). + binding: String, + /// Placeholder / hint text shown when empty. + hint: Option, + /// Visual role of this input. + appearance: AppearanceConcept, + }, + + /// An image element. + Image { + /// Source URI or asset name. + src: String, + /// Accessibility description. + alt: String, + }, + + /// A toggle/checkbox control bound to a boolean state variable. + Toggle { + /// State variable name (must be Bool). + binding: String, + /// Label displayed alongside the toggle. + label: String, + }, + + /// A list of homogeneous items, each rendered by the same child primitive. + List { + /// Items to iterate over — a state variable or expression name. + items_binding: String, + /// The name given to each item within the child template. + item_name: String, + /// The primitive template applied to each item. + item_template: Box, + }, +} + +impl SemanticPrimitive { + /// Convenience constructor for a primary action button. + pub fn action_button(label: impl Into, on_press: impl Into) -> Self { + Self::Button { + label: label.into(), + appearance: AppearanceConcept::Action, + on_press: Some(on_press.into()), + } + } + + /// Convenience constructor for a destructive action button. + pub fn destructive_button(label: impl Into, on_press: impl Into) -> Self { + Self::Button { + label: label.into(), + appearance: AppearanceConcept::Destructive, + on_press: Some(on_press.into()), + } + } + + /// Convenience constructor for a heading. + pub fn heading(content: impl Into, level: u8) -> Self { + Self::Text { + content: content.into(), + concept: TextConcept::Heading { level }, + } + } + + /// Convenience constructor for body text. + pub fn body_text(content: impl Into) -> Self { + Self::Text { + content: content.into(), + concept: TextConcept::Body, + } + } + + /// Convenience constructor for a vertical stack container. + pub fn stack(children: Vec) -> Self { + Self::Container { + children, + layout: LayoutConcept::Stack, + } + } + + /// Convenience constructor for a horizontal row container. + pub fn row(children: Vec) -> Self { + Self::Container { + children, + layout: LayoutConcept::Row, + } + } +} + +// --------------------------------------------------------------------------- +// Semantic extraction from AST +// --------------------------------------------------------------------------- + +/// Map an HTML tag + appearance attribute from the AST into a `SemanticPrimitive`. +/// +/// This is the bridge from the parser's `TemplateNode` representation to the +/// semantic layer. The compiler calls this during its semantic analysis pass. +/// +/// Appearance is inferred from: +/// 1. An explicit `appearance="..."` attribute on the element. +/// 2. The element's tag + class names (heuristic fallback). +pub fn infer_appearance(tag: &str, class: Option<&str>, explicit: Option<&str>) -> AppearanceConcept { + // Explicit attribute wins. + if let Some(a) = explicit { + return match a { + "action" | "primary" => AppearanceConcept::Action, + "destructive" | "danger" | "delete" => AppearanceConcept::Destructive, + "secondary" | "subdued" => AppearanceConcept::Secondary, + "navigation" | "nav" | "link" => AppearanceConcept::Navigation, + "informational" | "info" | "display" => AppearanceConcept::Informational, + "structural" | "layout" => AppearanceConcept::Structural, + _ => AppearanceConcept::Action, + }; + } + + // Infer from class names. + if let Some(cls) = class { + if cls.contains("danger") || cls.contains("destructive") || cls.contains("delete") { + return AppearanceConcept::Destructive; + } + if cls.contains("secondary") || cls.contains("outline") || cls.contains("ghost") { + return AppearanceConcept::Secondary; + } + if cls.contains("nav") || cls.contains("link") { + return AppearanceConcept::Navigation; + } + } + + // Tag-level heuristic fallback. + match tag { + "button" => AppearanceConcept::Action, + "a" => AppearanceConcept::Navigation, + "input" | "textarea" | "select" => AppearanceConcept::Action, + "div" | "section" | "main" | "article" | "aside" | "header" | "footer" => { + AppearanceConcept::Structural + } + "span" | "p" | "label" | "small" => AppearanceConcept::Informational, + _ => AppearanceConcept::Structural, + } +} + +/// Infer a `TextConcept` from an HTML tag name. +pub fn infer_text_concept(tag: &str) -> TextConcept { + match tag { + "h1" => TextConcept::Heading { level: 1 }, + "h2" => TextConcept::Heading { level: 2 }, + "h3" => TextConcept::Heading { level: 3 }, + "h4" => TextConcept::Heading { level: 4 }, + "h5" => TextConcept::Heading { level: 5 }, + "h6" => TextConcept::Heading { level: 6 }, + "label" => TextConcept::Label, + "small" | "caption" => TextConcept::Caption, + "code" | "pre" | "kbd" | "samp" => TextConcept::Code, + _ => TextConcept::Body, + } +} + +/// Infer a `LayoutConcept` from an HTML tag name and optional class names. +pub fn infer_layout_concept(tag: &str, class: Option<&str>) -> LayoutConcept { + if let Some(cls) = class { + if cls.contains("row") || cls.contains("horizontal") || cls.contains("flex-row") { + return LayoutConcept::Row; + } + if cls.contains("grid") { + return LayoutConcept::Grid; + } + if cls.contains("overlay") || cls.contains("stack-z") { + return LayoutConcept::Overlay; + } + if cls.contains("scroll") { + return LayoutConcept::Scroll; + } + } + + match tag { + "ul" | "ol" => LayoutConcept::Scroll, // scrollable list + "nav" => LayoutConcept::Row, + _ => LayoutConcept::Stack, // default: vertical stack + } +} diff --git a/crates/el-ui-compiler/src/tests.rs b/vessels/el-ui-compiler/src/tests.rs similarity index 100% rename from crates/el-ui-compiler/src/tests.rs rename to vessels/el-ui-compiler/src/tests.rs