Files
el/ui/vessels/el-ui-compiler/src/lexer.rs
T

569 lines
21 KiB
Rust

//! Lexer for el-ui component syntax.
//!
//! Produces a flat `Vec<Token>` from source text.
//! The lexer is context-sensitive: it switches between "code mode"
//! and "template mode" when it encounters the `template` keyword and `{` / `}`.
use crate::error::{CompileError, CompileResult};
#[derive(Debug, Clone, PartialEq)]
pub enum Token {
// Keywords
Component,
Props,
State,
Fn,
Template,
If,
Else,
Return,
// Identifiers and literals
Ident(String),
StringLit(String),
IntLit(i64),
FloatLit(f64),
BoolLit(bool),
// Punctuation
LBrace, // {
RBrace, // }
LParen, // (
RParen, // )
LAngle, // <
RAngle, // >
LBracket, // [
RBracket, // ]
Colon, // :
Semicolon,// ;
Comma, // ,
Dot, // .
Eq, // =
EqEq, // ==
Bang, // !
BangEq, // !=
Plus, // +
Minus, // -
Star, // *
Slash, // /
Arrow, // ->
FatArrow, // =>
Ampersand,// &
Pipe, // |
AmpAmp, // &&
PipePipe, // ||
Question, // ?
Hash, // #
At, // @
// Template-specific
SlashIdent(String), // /if /each /activate
ColonIdent(String), // :else
HashIdent(String), // #if #each #activate
OnColon(String), // on:click on:input etc.
SelfClose, // />
CloseTag(String), // </div>
// Raw text in templates
RawText(String),
Eof,
}
pub fn tokenize(source: &str) -> CompileResult<Vec<Token>> {
let mut lexer = Lexer::new(source);
lexer.run()
}
struct Lexer<'a> {
src: &'a [u8],
pos: usize,
}
impl<'a> Lexer<'a> {
fn new(source: &'a str) -> Self {
Self { src: source.as_bytes(), pos: 0 }
}
fn peek(&self) -> Option<u8> {
self.src.get(self.pos).copied()
}
fn peek2(&self) -> Option<u8> {
self.src.get(self.pos + 1).copied()
}
fn advance(&mut self) -> Option<u8> {
let ch = self.src.get(self.pos).copied();
if ch.is_some() {
self.pos += 1;
}
ch
}
fn skip_whitespace_and_comments(&mut self) {
loop {
// Skip whitespace
while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
self.advance();
}
// Skip // line comments
if self.peek() == Some(b'/') && self.peek2() == Some(b'/') {
while self.peek().is_some() && self.peek() != Some(b'\n') {
self.advance();
}
continue;
}
break;
}
}
fn read_ident(&mut self) -> String {
let start = self.pos;
while matches!(self.peek(), Some(b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_')) {
self.advance();
}
String::from_utf8_lossy(&self.src[start..self.pos]).into_owned()
}
fn read_string(&mut self) -> CompileResult<String> {
// Opening quote already consumed
let mut s = String::new();
loop {
match self.advance() {
None => return Err(CompileError::Lex { pos: self.pos, msg: "unterminated string".into() }),
Some(b'"') => break,
Some(b'\\') => {
match self.advance() {
Some(b'n') => s.push('\n'),
Some(b't') => s.push('\t'),
Some(b'r') => s.push('\r'),
Some(b'"') => s.push('"'),
Some(b'\\') => s.push('\\'),
Some(b'0') => s.push('\0'),
Some(c) => s.push(c as char),
None => return Err(CompileError::Lex { pos: self.pos, msg: "unterminated escape".into() }),
}
}
Some(c) => s.push(c as char),
}
}
Ok(s)
}
fn read_number(&mut self, first: u8) -> Token {
let mut s = String::new();
s.push(first as char);
while matches!(self.peek(), Some(b'0'..=b'9' | b'_')) {
let c = self.advance().unwrap();
if c != b'_' {
s.push(c as char);
}
}
if self.peek() == Some(b'.') && matches!(self.peek2(), Some(b'0'..=b'9')) {
s.push('.');
self.advance();
while matches!(self.peek(), Some(b'0'..=b'9')) {
s.push(self.advance().unwrap() as char);
}
Token::FloatLit(s.parse().unwrap_or(0.0))
} else {
Token::IntLit(s.parse().unwrap_or(0))
}
}
/// Read a template block — everything between the outer `{` and matching `}`
/// of `template { ... }`. Returns the raw text inside.
fn read_template_inner(&mut self) -> CompileResult<Vec<Token>> {
// We are positioned right after `template` keyword and the `{` that opened it.
// We tokenize the template body using a template-aware mini-lexer.
let mut toks: Vec<Token> = Vec::new();
let mut depth = 1i32; // we've consumed the opening {
let mut text_buf = String::new();
macro_rules! flush_text {
() => {
if !text_buf.is_empty() {
let t = text_buf.trim().to_owned();
if !t.is_empty() {
toks.push(Token::RawText(t));
}
text_buf.clear();
}
};
}
loop {
match self.peek() {
None => return Err(CompileError::Lex { pos: self.pos, msg: "unterminated template block".into() }),
Some(b'{') => {
self.advance();
// Could be interpolation {expr}, or {#if}, {/if}, {:else}
// Peek at what follows
// Skip whitespace inside
while matches!(self.peek(), Some(b' ' | b'\t')) {
self.advance();
}
if self.peek() == Some(b'#') {
// Block tag: {#if ...} {#each ...} {#activate ...}
self.advance(); // consume #
let kw = self.read_ident();
flush_text!();
toks.push(Token::HashIdent(kw));
// Read the rest up to }
let mut inner = String::new();
let mut brace_d = 1i32;
loop {
match self.peek() {
None => break,
Some(b'{') => { brace_d += 1; inner.push('{'); self.advance(); }
Some(b'}') => {
brace_d -= 1;
self.advance();
if brace_d == 0 { break; }
inner.push('}');
}
Some(c) => { inner.push(c as char); self.advance(); }
}
}
toks.push(Token::RawText(inner.trim().to_owned()));
} else if self.peek() == Some(b'/') {
// Close tag: {/if} {/each} {/activate}
self.advance(); // consume /
let kw = self.read_ident();
flush_text!();
toks.push(Token::SlashIdent(kw));
while self.peek() == Some(b'}') { self.advance(); break; }
} else if self.peek() == Some(b':') {
// {:else}
self.advance(); // consume :
let kw = self.read_ident();
flush_text!();
toks.push(Token::ColonIdent(kw));
while self.peek() == Some(b'}') { self.advance(); break; }
} else {
// Regular interpolation or outer brace tracking
// Check if this closes the template
if depth == 1 && self.peek() == Some(b'}') {
// Empty brace—skip
self.advance();
let _ = depth - 1; // depth tracked by outer loop
break;
}
// Read the expression until matching }
let mut expr = String::new();
let mut brace_d = 1i32;
loop {
match self.peek() {
None => break,
Some(b'{') => { brace_d += 1; expr.push('{'); self.advance(); }
Some(b'}') => {
brace_d -= 1;
self.advance();
if brace_d == 0 { break; }
expr.push('}');
}
Some(c) => { expr.push(c as char); self.advance(); }
}
}
let expr = expr.trim().to_owned();
if !expr.is_empty() {
flush_text!();
toks.push(Token::LBrace);
toks.push(Token::RawText(expr));
toks.push(Token::RBrace);
}
}
}
Some(b'}') => {
depth -= 1;
self.advance();
if depth == 0 {
flush_text!();
break;
}
text_buf.push('}');
}
Some(b'<') => {
// HTML element or close tag
self.advance();
if self.peek() == Some(b'/') {
// Close tag </div>
self.advance();
let tag = self.read_tag_name();
while self.peek() != Some(b'>') && self.peek().is_some() {
self.advance();
}
self.advance(); // consume >
flush_text!();
toks.push(Token::CloseTag(tag));
} else {
// Open tag
let tag = self.read_tag_name();
flush_text!();
toks.push(Token::LAngle);
toks.push(Token::Ident(tag));
// Read attributes
self.read_attrs_into(&mut toks)?;
}
}
Some(b'\n' | b'\r') => {
self.advance();
text_buf.push(' ');
}
Some(c) => {
text_buf.push(c as char);
self.advance();
}
}
}
Ok(toks)
}
fn read_tag_name(&mut self) -> String {
while matches!(self.peek(), Some(b' ' | b'\t' | b'\n')) {
self.advance();
}
let start = self.pos;
while matches!(self.peek(), Some(b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_')) {
self.advance();
}
String::from_utf8_lossy(&self.src[start..self.pos]).into_owned()
}
/// Read attributes until `>` or `/>`.
fn read_attrs_into(&mut self, toks: &mut Vec<Token>) -> CompileResult<()> {
loop {
// Skip whitespace
while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
self.advance();
}
match self.peek() {
None => break,
Some(b'/') if self.peek2() == Some(b'>') => {
self.advance(); self.advance();
toks.push(Token::SelfClose);
break;
}
Some(b'>') => {
self.advance();
toks.push(Token::RAngle);
break;
}
Some(b'o') if self.src.get(self.pos..self.pos+3) == Some(b"on:") => {
// on:event={handler}
self.pos += 3; // skip "on:"
let event = self.read_tag_name();
// skip whitespace and =
while matches!(self.peek(), Some(b' ' | b'=')) { self.advance(); }
// read {expr}
let expr = if self.peek() == Some(b'{') {
self.advance();
self.read_until_brace_close()?
} else {
self.read_quoted_string()?
};
toks.push(Token::OnColon(event));
toks.push(Token::RawText(expr));
}
_ => {
// Regular attribute: name="val" or name={expr}
let name = self.read_attr_name();
if name.is_empty() { break; }
// Skip whitespace and =
while matches!(self.peek(), Some(b' ' | b'\t')) { self.advance(); }
if self.peek() != Some(b'=') {
// Boolean attribute with no value
toks.push(Token::Ident(name));
continue;
}
self.advance(); // consume =
while matches!(self.peek(), Some(b' ' | b'\t')) { self.advance(); }
let value = if self.peek() == Some(b'"') {
self.advance(); // consume "
let s = self.read_string()?;
toks.push(Token::Ident(name.clone()));
toks.push(Token::Eq);
toks.push(Token::StringLit(s));
continue;
} else if self.peek() == Some(b'{') {
self.advance();
self.read_until_brace_close()?
} else {
self.read_attr_name()
};
toks.push(Token::Ident(name));
toks.push(Token::Eq);
toks.push(Token::RawText(value));
}
}
}
Ok(())
}
fn read_attr_name(&mut self) -> String {
let start = self.pos;
while matches!(self.peek(), Some(b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' | b':')) {
self.advance();
}
String::from_utf8_lossy(&self.src[start..self.pos]).into_owned()
}
fn read_quoted_string(&mut self) -> CompileResult<String> {
if self.peek() == Some(b'"') {
self.advance();
self.read_string()
} else {
Ok(self.read_attr_name())
}
}
fn read_until_brace_close(&mut self) -> CompileResult<String> {
let mut s = String::new();
let mut depth = 1i32;
loop {
match self.peek() {
None => return Err(CompileError::Lex { pos: self.pos, msg: "unterminated {".into() }),
Some(b'{') => { depth += 1; s.push('{'); self.advance(); }
Some(b'}') => {
depth -= 1;
self.advance();
if depth == 0 { break; }
s.push('}');
}
Some(c) => { s.push(c as char); self.advance(); }
}
}
Ok(s)
}
fn run(&mut self) -> CompileResult<Vec<Token>> {
let mut tokens: Vec<Token> = Vec::new();
loop {
self.skip_whitespace_and_comments();
if self.peek().is_none() {
tokens.push(Token::Eof);
break;
}
let ch = self.advance().unwrap();
match ch {
b'a'..=b'z' | b'A'..=b'Z' | b'_' => {
let mut ident = String::new();
ident.push(ch as char);
while matches!(self.peek(), Some(b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_')) {
ident.push(self.advance().unwrap() as char);
}
let tok = match ident.as_str() {
"component" => Token::Component,
"props" => Token::Props,
"state" => Token::State,
"fn" => Token::Fn,
"template" => Token::Template,
"if" => Token::If,
"else" => Token::Else,
"return" => Token::Return,
"true" => Token::BoolLit(true),
"false" => Token::BoolLit(false),
other => Token::Ident(other.to_owned()),
};
// Special handling: after `template`, read the block specially
if tok == Token::Template {
tokens.push(tok);
self.skip_whitespace_and_comments();
if self.peek() == Some(b'{') {
self.advance(); // consume {
tokens.push(Token::LBrace);
let inner = self.read_template_inner()?;
tokens.extend(inner);
tokens.push(Token::RBrace);
}
} else {
tokens.push(tok);
}
}
b'"' => {
let s = self.read_string()?;
tokens.push(Token::StringLit(s));
}
b'0'..=b'9' => {
let tok = self.read_number(ch);
tokens.push(tok);
}
b'{' => tokens.push(Token::LBrace),
b'}' => tokens.push(Token::RBrace),
b'(' => tokens.push(Token::LParen),
b')' => tokens.push(Token::RParen),
b'[' => tokens.push(Token::LBracket),
b']' => tokens.push(Token::RBracket),
b':' => tokens.push(Token::Colon),
b';' => tokens.push(Token::Semicolon),
b',' => tokens.push(Token::Comma),
b'.' => tokens.push(Token::Dot),
b'=' => {
if self.peek() == Some(b'=') {
self.advance(); tokens.push(Token::EqEq);
} else if self.peek() == Some(b'>') {
self.advance(); tokens.push(Token::FatArrow);
} else {
tokens.push(Token::Eq);
}
}
b'!' => {
if self.peek() == Some(b'=') {
self.advance(); tokens.push(Token::BangEq);
} else {
tokens.push(Token::Bang);
}
}
b'+' => tokens.push(Token::Plus),
b'-' => {
if self.peek() == Some(b'>') {
self.advance(); tokens.push(Token::Arrow);
} else {
tokens.push(Token::Minus);
}
}
b'*' => tokens.push(Token::Star),
b'/' => {
if self.peek() == Some(b'/') {
// Line comment (shouldn't reach here after skip, but guard)
while self.peek().is_some() && self.peek() != Some(b'\n') {
self.advance();
}
} else {
tokens.push(Token::Slash);
}
}
b'&' => {
if self.peek() == Some(b'&') {
self.advance(); tokens.push(Token::AmpAmp);
} else {
tokens.push(Token::Ampersand);
}
}
b'|' => {
if self.peek() == Some(b'|') {
self.advance(); tokens.push(Token::PipePipe);
} else {
tokens.push(Token::Pipe);
}
}
b'?' => tokens.push(Token::Question),
b'#' => tokens.push(Token::Hash),
b'@' => tokens.push(Token::At),
b'<' => tokens.push(Token::LAngle),
b'>' => tokens.push(Token::RAngle),
_ => {
// Ignore unknown characters (whitespace already skipped)
}
}
}
Ok(tokens)
}
}