Archived
f4abfe6fdc
Belated rename commit for foundation/el-ui — was missed in the workspace-wide crates→vessels pass earlier today. Same structural intent as the rename in the other repos: 'crates' is the Rust word, 'vessel' is El's, and the directory rename is the marker that this slot holds an El buildable unit even if its current contents are still Rust pending port. Plus the El ports themselves — manifest.el + src/main.el per sub- vessel (el-aop, el-auth, el-config, el-i18n, el-identity, el-layout, el-platform, el-publish, el-secrets, el-services, el-style, el-ui- compiler). The ui-compiler is a stub: elc only emits C right now; generating browser-target JS/Wasm is the biggest open language gap and gets its own project. Until then, el-ui-compiler emits a JS module that throws elc.backend_missing so callers fail loudly. Cross-repo path dependencies in Cargo.toml updated to vessels/.
641 lines
22 KiB
Rust
641 lines
22 KiB
Rust
//! Parser for el-ui component files.
|
|
//!
|
|
//! Hand-written recursive descent. Produces a `Vec<Component>` from a token stream.
|
|
|
|
use crate::ast::*;
|
|
use crate::error::{CompileError, CompileResult};
|
|
use crate::lexer::Token;
|
|
|
|
pub fn parse(tokens: &[Token]) -> CompileResult<Vec<Component>> {
|
|
let mut p = Parser::new(tokens);
|
|
p.parse_program()
|
|
}
|
|
|
|
struct Parser<'a> {
|
|
tokens: &'a [Token],
|
|
pos: usize,
|
|
}
|
|
|
|
impl<'a> Parser<'a> {
|
|
fn new(tokens: &'a [Token]) -> Self {
|
|
Self { tokens, pos: 0 }
|
|
}
|
|
|
|
fn peek(&self) -> &Token {
|
|
self.tokens.get(self.pos).unwrap_or(&Token::Eof)
|
|
}
|
|
|
|
fn advance(&mut self) -> &Token {
|
|
let tok = self.tokens.get(self.pos).unwrap_or(&Token::Eof);
|
|
if self.pos < self.tokens.len() {
|
|
self.pos += 1;
|
|
}
|
|
tok
|
|
}
|
|
|
|
fn expect(&mut self, expected: &Token) -> CompileResult<()> {
|
|
let tok = self.advance();
|
|
if tok == expected {
|
|
Ok(())
|
|
} else {
|
|
Err(CompileError::Parse {
|
|
msg: format!("expected {:?}, got {:?}", expected, tok),
|
|
})
|
|
}
|
|
}
|
|
|
|
fn expect_ident(&mut self) -> CompileResult<String> {
|
|
match self.advance().clone() {
|
|
Token::Ident(s) => Ok(s),
|
|
other => Err(CompileError::Parse {
|
|
msg: format!("expected identifier, got {:?}", other),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn parse_program(&mut self) -> CompileResult<Vec<Component>> {
|
|
let mut components = Vec::new();
|
|
while *self.peek() != Token::Eof {
|
|
match self.peek() {
|
|
Token::Component => {
|
|
components.push(self.parse_component()?);
|
|
}
|
|
_ => {
|
|
// Skip unknown top-level tokens
|
|
self.advance();
|
|
}
|
|
}
|
|
}
|
|
Ok(components)
|
|
}
|
|
|
|
fn parse_component(&mut self) -> CompileResult<Component> {
|
|
self.expect(&Token::Component)?;
|
|
let name = self.expect_ident()?;
|
|
self.expect(&Token::LBrace)?;
|
|
|
|
let mut props = Vec::new();
|
|
let mut state = Vec::new();
|
|
let mut methods = Vec::new();
|
|
let mut template = Template { nodes: Vec::new() };
|
|
|
|
loop {
|
|
match self.peek().clone() {
|
|
Token::RBrace => {
|
|
self.advance();
|
|
break;
|
|
}
|
|
Token::Eof => break,
|
|
Token::Props => {
|
|
self.advance();
|
|
props = self.parse_prop_block()?;
|
|
}
|
|
Token::State => {
|
|
self.advance();
|
|
state = self.parse_state_block()?;
|
|
}
|
|
Token::Fn => {
|
|
methods.push(self.parse_method()?);
|
|
}
|
|
Token::Template => {
|
|
self.advance(); // consume `template`
|
|
template = self.parse_template_block()?;
|
|
}
|
|
_ => {
|
|
self.advance(); // skip unknown
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(Component { name, props, state, methods, template })
|
|
}
|
|
|
|
fn parse_prop_block(&mut self) -> CompileResult<Vec<PropDef>> {
|
|
self.expect(&Token::LBrace)?;
|
|
let mut props = Vec::new();
|
|
loop {
|
|
match self.peek().clone() {
|
|
Token::RBrace | Token::Eof => {
|
|
self.advance();
|
|
break;
|
|
}
|
|
Token::Ident(name) => {
|
|
self.advance();
|
|
self.expect(&Token::Colon)?;
|
|
let type_name = self.parse_type_name()?;
|
|
let default = if *self.peek() == Token::Eq {
|
|
self.advance();
|
|
Some(self.parse_default_value()?)
|
|
} else {
|
|
None
|
|
};
|
|
// Optional trailing comma/semicolon
|
|
if matches!(self.peek(), Token::Comma | Token::Semicolon) {
|
|
self.advance();
|
|
}
|
|
props.push(PropDef { name, type_name, default });
|
|
}
|
|
_ => {
|
|
self.advance();
|
|
}
|
|
}
|
|
}
|
|
Ok(props)
|
|
}
|
|
|
|
fn parse_state_block(&mut self) -> CompileResult<Vec<StateDef>> {
|
|
self.expect(&Token::LBrace)?;
|
|
let mut defs = Vec::new();
|
|
loop {
|
|
match self.peek().clone() {
|
|
Token::RBrace | Token::Eof => {
|
|
self.advance();
|
|
break;
|
|
}
|
|
Token::Ident(name) => {
|
|
self.advance();
|
|
self.expect(&Token::Colon)?;
|
|
let type_name = self.parse_type_name()?;
|
|
self.expect(&Token::Eq)?;
|
|
let initial = self.parse_default_value()?;
|
|
if matches!(self.peek(), Token::Comma | Token::Semicolon) {
|
|
self.advance();
|
|
}
|
|
defs.push(StateDef { name, type_name, initial });
|
|
}
|
|
_ => {
|
|
self.advance();
|
|
}
|
|
}
|
|
}
|
|
Ok(defs)
|
|
}
|
|
|
|
fn parse_type_name(&mut self) -> CompileResult<String> {
|
|
let name = match self.peek().clone() {
|
|
Token::Ident(s) => { self.advance(); s }
|
|
other => return Err(CompileError::Parse {
|
|
msg: format!("expected type name, got {:?}", other),
|
|
}),
|
|
};
|
|
// Check for array type [T] → already consumed base name, but array types
|
|
// start with [ so this handles bare type names only
|
|
Ok(name)
|
|
}
|
|
|
|
fn parse_default_value(&mut self) -> CompileResult<String> {
|
|
// Collect tokens until comma, semicolon, or next top-level item
|
|
// We need to handle nested structures like Fn types, etc.
|
|
let mut result = String::new();
|
|
let mut depth = 0i32;
|
|
loop {
|
|
match self.peek() {
|
|
Token::Eof => break,
|
|
Token::LParen | Token::LBrace | Token::LBracket => {
|
|
depth += 1;
|
|
let tok = self.advance();
|
|
result.push_str(&token_to_str(tok));
|
|
}
|
|
Token::RParen | Token::RBrace | Token::RBracket => {
|
|
if depth == 0 { break; }
|
|
depth -= 1;
|
|
let tok = self.advance();
|
|
result.push_str(&token_to_str(tok));
|
|
}
|
|
Token::Comma | Token::Semicolon if depth == 0 => break,
|
|
// These signal end of the value if at depth 0
|
|
Token::Ident(_) | Token::Props | Token::State | Token::Fn
|
|
| Token::Template | Token::Component if depth == 0 => break,
|
|
_ => {
|
|
let tok = self.advance();
|
|
result.push_str(&token_to_str(tok));
|
|
result.push(' ');
|
|
}
|
|
}
|
|
}
|
|
Ok(result.trim().to_owned())
|
|
}
|
|
|
|
fn parse_method(&mut self) -> CompileResult<Method> {
|
|
self.expect(&Token::Fn)?;
|
|
let name = self.expect_ident()?;
|
|
self.expect(&Token::LParen)?;
|
|
|
|
let mut params: Vec<(String, String)> = Vec::new();
|
|
loop {
|
|
match self.peek().clone() {
|
|
Token::RParen | Token::Eof => { self.advance(); break; }
|
|
Token::Ident(pname) => {
|
|
self.advance();
|
|
self.expect(&Token::Colon)?;
|
|
let ptype = self.parse_type_name()?;
|
|
params.push((pname, ptype));
|
|
if *self.peek() == Token::Comma { self.advance(); }
|
|
}
|
|
_ => { self.advance(); }
|
|
}
|
|
}
|
|
|
|
self.expect(&Token::Arrow)?;
|
|
let return_type = self.parse_type_name()?;
|
|
|
|
// Read the method body between { }
|
|
let body = self.read_block_raw()?;
|
|
|
|
Ok(Method { name, params, return_type, body })
|
|
}
|
|
|
|
/// Read everything between { and matching } as raw text.
|
|
fn read_block_raw(&mut self) -> CompileResult<String> {
|
|
self.expect(&Token::LBrace)?;
|
|
let mut result = String::new();
|
|
let mut depth = 1i32;
|
|
loop {
|
|
match self.peek() {
|
|
Token::Eof => break,
|
|
Token::LBrace => { depth += 1; self.advance(); result.push_str("{ "); }
|
|
Token::RBrace => {
|
|
depth -= 1;
|
|
self.advance();
|
|
if depth == 0 { break; }
|
|
result.push_str("} ");
|
|
}
|
|
tok => {
|
|
result.push_str(&token_to_str(tok));
|
|
result.push(' ');
|
|
self.advance();
|
|
}
|
|
}
|
|
}
|
|
Ok(result.trim().to_owned())
|
|
}
|
|
|
|
/// Parse the template block. At this point the lexer has already expanded
|
|
/// the template into special tokens (LBrace/RBrace wrapping interpolations,
|
|
/// LAngle/Ident for elements, etc.).
|
|
fn parse_template_block(&mut self) -> CompileResult<Template> {
|
|
self.expect(&Token::LBrace)?;
|
|
let nodes = self.parse_template_nodes()?;
|
|
// The matching RBrace is consumed inside parse_template_nodes
|
|
Ok(Template { nodes })
|
|
}
|
|
|
|
fn parse_template_nodes(&mut self) -> CompileResult<Vec<TemplateNode>> {
|
|
let mut nodes: Vec<TemplateNode> = Vec::new();
|
|
loop {
|
|
match self.peek().clone() {
|
|
Token::Eof | Token::RBrace => {
|
|
self.advance();
|
|
break;
|
|
}
|
|
Token::CloseTag(_) => {
|
|
// Consumed by parent element parser
|
|
break;
|
|
}
|
|
Token::LAngle => {
|
|
nodes.push(self.parse_element()?);
|
|
}
|
|
Token::LBrace => {
|
|
// Interpolation: { RawText }
|
|
self.advance(); // consume {
|
|
if let Token::RawText(expr) = self.peek().clone() {
|
|
self.advance();
|
|
nodes.push(TemplateNode::Interpolation(expr));
|
|
if *self.peek() == Token::RBrace { self.advance(); }
|
|
} else {
|
|
nodes.push(TemplateNode::Text("{".into()));
|
|
}
|
|
}
|
|
Token::HashIdent(kw) => {
|
|
let kw = kw.clone();
|
|
self.advance();
|
|
nodes.push(self.parse_block_tag(&kw)?);
|
|
}
|
|
Token::SlashIdent(_) => {
|
|
// End of a block — caller handles
|
|
break;
|
|
}
|
|
Token::ColonIdent(_) => {
|
|
// {:else} — caller handles
|
|
break;
|
|
}
|
|
Token::RawText(t) => {
|
|
let t = t.clone();
|
|
self.advance();
|
|
if !t.is_empty() {
|
|
nodes.push(TemplateNode::Text(t));
|
|
}
|
|
}
|
|
_ => {
|
|
self.advance(); // skip
|
|
}
|
|
}
|
|
}
|
|
Ok(nodes)
|
|
}
|
|
|
|
fn parse_element(&mut self) -> CompileResult<TemplateNode> {
|
|
self.expect(&Token::LAngle)?;
|
|
let tag = self.expect_ident()?;
|
|
|
|
let is_component = tag.chars().next().map(|c| c.is_uppercase()).unwrap_or(false);
|
|
|
|
let mut attrs: Vec<Attr> = Vec::new();
|
|
|
|
// Parse attributes until > or />
|
|
let mut self_closing = false;
|
|
loop {
|
|
match self.peek().clone() {
|
|
Token::SelfClose => {
|
|
self.advance();
|
|
self_closing = true;
|
|
break;
|
|
}
|
|
Token::RAngle => {
|
|
self.advance();
|
|
break;
|
|
}
|
|
Token::Eof => break,
|
|
Token::OnColon(event) => {
|
|
let event = event.clone();
|
|
self.advance();
|
|
let handler = if let Token::RawText(h) = self.peek().clone() {
|
|
self.advance();
|
|
h
|
|
} else {
|
|
String::new()
|
|
};
|
|
attrs.push(Attr::EventHandler { event, handler });
|
|
}
|
|
Token::Ident(name) => {
|
|
let name = name.clone();
|
|
self.advance();
|
|
if *self.peek() == Token::Eq {
|
|
self.advance(); // consume =
|
|
match self.peek().clone() {
|
|
Token::StringLit(val) => {
|
|
self.advance();
|
|
attrs.push(Attr::Static { name, value: val });
|
|
}
|
|
Token::RawText(expr) => {
|
|
self.advance();
|
|
// Determine if it's a bool attr
|
|
// Simple heuristic: if name is "disabled", "checked", "readonly"
|
|
let bool_attrs = ["disabled", "checked", "readonly", "required", "multiple", "selected"];
|
|
if bool_attrs.contains(&name.as_str()) {
|
|
attrs.push(Attr::BoolAttr { name, expr });
|
|
} else {
|
|
attrs.push(Attr::Dynamic { name, expr });
|
|
}
|
|
}
|
|
_ => {
|
|
attrs.push(Attr::Static { name, value: String::new() });
|
|
}
|
|
}
|
|
} else {
|
|
// Standalone attribute (boolean)
|
|
attrs.push(Attr::BoolAttr { name, expr: "true".into() });
|
|
}
|
|
}
|
|
_ => {
|
|
self.advance();
|
|
}
|
|
}
|
|
}
|
|
|
|
if self_closing || is_component {
|
|
if is_component {
|
|
return Ok(TemplateNode::Component { name: tag, props: attrs });
|
|
}
|
|
return Ok(TemplateNode::Element { tag, attrs, children: Vec::new() });
|
|
}
|
|
|
|
// Read children until </tag>
|
|
let children = self.parse_template_children(&tag)?;
|
|
|
|
Ok(TemplateNode::Element { tag, attrs, children })
|
|
}
|
|
|
|
fn parse_template_children(&mut self, close_tag: &str) -> CompileResult<Vec<TemplateNode>> {
|
|
let mut children: Vec<TemplateNode> = Vec::new();
|
|
loop {
|
|
match self.peek().clone() {
|
|
Token::Eof => break,
|
|
Token::RBrace => break,
|
|
Token::CloseTag(tag) => {
|
|
self.advance();
|
|
if tag == close_tag || tag.is_empty() {
|
|
break;
|
|
}
|
|
// Mismatched close tag — ignore
|
|
}
|
|
Token::LAngle => {
|
|
children.push(self.parse_element()?);
|
|
}
|
|
Token::LBrace => {
|
|
self.advance();
|
|
if let Token::RawText(expr) = self.peek().clone() {
|
|
self.advance();
|
|
children.push(TemplateNode::Interpolation(expr));
|
|
if *self.peek() == Token::RBrace { self.advance(); }
|
|
}
|
|
}
|
|
Token::HashIdent(kw) => {
|
|
let kw = kw.clone();
|
|
self.advance();
|
|
children.push(self.parse_block_tag(&kw)?);
|
|
}
|
|
Token::SlashIdent(_) | Token::ColonIdent(_) => break,
|
|
Token::RawText(t) => {
|
|
let t = t.clone();
|
|
self.advance();
|
|
if !t.trim().is_empty() {
|
|
children.push(TemplateNode::Text(t));
|
|
}
|
|
}
|
|
_ => { self.advance(); }
|
|
}
|
|
}
|
|
Ok(children)
|
|
}
|
|
|
|
fn parse_block_tag(&mut self, kw: &str) -> CompileResult<TemplateNode> {
|
|
match kw {
|
|
"if" => self.parse_if_block(),
|
|
"each" => self.parse_each_block(),
|
|
"activate" => self.parse_activate_block(),
|
|
_ => Err(CompileError::Parse { msg: format!("unknown block tag: #{}", kw) }),
|
|
}
|
|
}
|
|
|
|
fn parse_if_block(&mut self) -> CompileResult<TemplateNode> {
|
|
// Next token should be RawText with the condition
|
|
let condition = if let Token::RawText(cond) = self.peek().clone() {
|
|
self.advance();
|
|
cond
|
|
} else {
|
|
return Err(CompileError::Parse { msg: "expected condition after {#if}".into() });
|
|
};
|
|
|
|
let then = self.parse_template_nodes_until_close_or_else()?;
|
|
|
|
let else_ = if let Token::ColonIdent(kw) = self.peek().clone() {
|
|
if kw == "else" {
|
|
self.advance();
|
|
Some(self.parse_template_nodes_until_close_or_else()?)
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// Consume {/if}
|
|
if let Token::SlashIdent(kw) = self.peek().clone() {
|
|
if kw == "if" { self.advance(); }
|
|
}
|
|
|
|
Ok(TemplateNode::If { condition, then, else_ })
|
|
}
|
|
|
|
fn parse_each_block(&mut self) -> CompileResult<TemplateNode> {
|
|
// RawText: "items as item"
|
|
let raw = if let Token::RawText(r) = self.peek().clone() {
|
|
self.advance();
|
|
r
|
|
} else {
|
|
return Err(CompileError::Parse { msg: "expected 'items as item' after {#each}".into() });
|
|
};
|
|
|
|
let (items, item_name) = parse_each_header(&raw)?;
|
|
let children = self.parse_template_nodes_until_close_or_else()?;
|
|
|
|
if let Token::SlashIdent(kw) = self.peek().clone() {
|
|
if kw == "each" { self.advance(); }
|
|
}
|
|
|
|
Ok(TemplateNode::Each { items, item_name, children })
|
|
}
|
|
|
|
fn parse_activate_block(&mut self) -> CompileResult<TemplateNode> {
|
|
// RawText: `"query" as results`
|
|
let raw = if let Token::RawText(r) = self.peek().clone() {
|
|
self.advance();
|
|
r
|
|
} else {
|
|
return Err(CompileError::Parse { msg: "expected query after {#activate}".into() });
|
|
};
|
|
|
|
let (query, result_name) = parse_activate_header(&raw)?;
|
|
let children = self.parse_template_nodes_until_close_or_else()?;
|
|
|
|
if let Token::SlashIdent(kw) = self.peek().clone() {
|
|
if kw == "activate" { self.advance(); }
|
|
}
|
|
|
|
Ok(TemplateNode::Activate { query, result_name, children })
|
|
}
|
|
|
|
fn parse_template_nodes_until_close_or_else(&mut self) -> CompileResult<Vec<TemplateNode>> {
|
|
let mut nodes: Vec<TemplateNode> = Vec::new();
|
|
loop {
|
|
match self.peek().clone() {
|
|
Token::Eof | Token::RBrace => break,
|
|
Token::SlashIdent(_) | Token::ColonIdent(_) => break,
|
|
Token::CloseTag(_) => break,
|
|
Token::LAngle => nodes.push(self.parse_element()?),
|
|
Token::LBrace => {
|
|
self.advance();
|
|
if let Token::RawText(expr) = self.peek().clone() {
|
|
self.advance();
|
|
nodes.push(TemplateNode::Interpolation(expr));
|
|
if *self.peek() == Token::RBrace { self.advance(); }
|
|
}
|
|
}
|
|
Token::HashIdent(kw) => {
|
|
let kw = kw.clone();
|
|
self.advance();
|
|
nodes.push(self.parse_block_tag(&kw)?);
|
|
}
|
|
Token::RawText(t) => {
|
|
let t = t.clone();
|
|
self.advance();
|
|
if !t.trim().is_empty() {
|
|
nodes.push(TemplateNode::Text(t));
|
|
}
|
|
}
|
|
_ => { self.advance(); }
|
|
}
|
|
}
|
|
Ok(nodes)
|
|
}
|
|
}
|
|
|
|
fn token_to_str(tok: &Token) -> String {
|
|
match tok {
|
|
Token::Ident(s) => s.clone(),
|
|
Token::StringLit(s) => format!("\"{}\"", s),
|
|
Token::IntLit(n) => n.to_string(),
|
|
Token::FloatLit(f) => f.to_string(),
|
|
Token::BoolLit(b) => b.to_string(),
|
|
Token::LBrace => "{".into(),
|
|
Token::RBrace => "}".into(),
|
|
Token::LParen => "(".into(),
|
|
Token::RParen => ")".into(),
|
|
Token::LBracket => "[".into(),
|
|
Token::RBracket => "]".into(),
|
|
Token::Colon => ":".into(),
|
|
Token::Semicolon => ";".into(),
|
|
Token::Comma => ",".into(),
|
|
Token::Dot => ".".into(),
|
|
Token::Eq => "=".into(),
|
|
Token::EqEq => "==".into(),
|
|
Token::Bang => "!".into(),
|
|
Token::BangEq => "!=".into(),
|
|
Token::Plus => "+".into(),
|
|
Token::Minus => "-".into(),
|
|
Token::Star => "*".into(),
|
|
Token::Slash => "/".into(),
|
|
Token::Arrow => "->".into(),
|
|
Token::FatArrow => "=>".into(),
|
|
Token::AmpAmp => "&&".into(),
|
|
Token::PipePipe => "||".into(),
|
|
Token::Question => "?".into(),
|
|
Token::RawText(s) => s.clone(),
|
|
Token::Return => "return".into(),
|
|
Token::If => "if".into(),
|
|
Token::Else => "else".into(),
|
|
Token::Fn => "fn".into(),
|
|
Token::Component => "component".into(),
|
|
Token::Props => "props".into(),
|
|
Token::State => "state".into(),
|
|
Token::Template => "template".into(),
|
|
_ => String::new(),
|
|
}
|
|
}
|
|
|
|
fn parse_each_header(raw: &str) -> CompileResult<(String, String)> {
|
|
// e.g., "items as item"
|
|
if let Some(idx) = raw.find(" as ") {
|
|
let items = raw[..idx].trim().to_owned();
|
|
let item_name = raw[idx + 4..].trim().to_owned();
|
|
Ok((items, item_name))
|
|
} else {
|
|
Err(CompileError::Parse { msg: format!("invalid #each header: '{}'", raw) })
|
|
}
|
|
}
|
|
|
|
fn parse_activate_header(raw: &str) -> CompileResult<(String, String)> {
|
|
// e.g., `"query string" as results`
|
|
// Strip outer quotes from query
|
|
let raw = raw.trim();
|
|
if let Some(idx) = raw.rfind(" as ") {
|
|
let query_part = raw[..idx].trim();
|
|
let result_name = raw[idx + 4..].trim().to_owned();
|
|
let query = query_part.trim_matches('"').to_owned();
|
|
Ok((query, result_name))
|
|
} else {
|
|
Err(CompileError::Parse { msg: format!("invalid #activate header: '{}'", raw) })
|
|
}
|
|
}
|