use crate::ast::*;
pub struct Parser<'a> {
src: &'a [u8],
pos: usize,
pub module: ModuleSoA,
}
impl<'a> Parser<'a> {
pub fn new(src: &'a str) -> Self {
Self::with_module(src, ModuleSoA::new())
}
pub fn with_module(src: &'a str, module: ModuleSoA) -> Self {
Self {
src: src.as_bytes(),
pos: 0,
module,
}
}
#[inline(always)]
fn peek(&self) -> Option<u8> {
self.src.get(self.pos).copied()
}
#[inline(always)]
fn advance(&mut self) {
self.pos += 1;
}
#[inline]
fn consume_if(&mut self, expected: u8) -> bool {
if self.peek() == Some(expected) {
self.pos += 1;
true
} else {
false
}
}
fn skip_whitespace(&mut self) {
let len = self.src.len();
while self.pos < len {
let c = self.src[self.pos];
if c.is_ascii_whitespace() {
self.pos += 1;
} else if c == b'/' && self.pos + 1 < len && self.src[self.pos + 1] == b'/' {
self.pos += 2;
while self.pos < len && self.src[self.pos] != b'\n' {
self.pos += 1;
}
} else {
break;
}
}
}
fn parse_ident(&mut self) -> String {
let start = self.pos;
let len = self.src.len();
while self.pos < len {
let c = self.src[self.pos];
if c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_' | b'.') {
self.pos += 1;
} else {
break;
}
}
unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]).to_string() }
}
fn parse_rhei_expr(&mut self) -> Result<String, String> {
self.skip_whitespace();
if self.consume_if(b'{') {
let start = self.pos;
let mut depth = 1;
let len = self.src.len();
while depth > 0 && self.pos < len {
match self.src[self.pos] {
b'{' => depth += 1,
b'}' => depth -= 1,
_ => {}
}
self.pos += 1;
}
if depth > 0 {
return Err("Unexpected EOF in Rhei block".into());
}
let expr = unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos - 1]) };
Ok(expr.trim().to_string())
} else {
let start = self.pos;
let mut parens = 0;
let mut brackets = 0;
let mut in_str = false;
let mut escape = false;
let len = self.src.len();
while self.pos < len {
let c = self.src[self.pos];
if escape {
escape = false;
self.pos += 1;
continue;
}
match c {
b'\\' => escape = true,
b'"' => in_str = !in_str,
b'(' if !in_str => parens += 1,
b')' if !in_str => {
if parens == 0 { break; }
parens -= 1;
}
b'[' if !in_str => brackets += 1,
b']' if !in_str && brackets > 0 => brackets -= 1,
b',' | b'\n' | b'}' | b'{' if !in_str && parens == 0 && brackets == 0 => break,
_ => {}
}
self.pos += 1;
}
let expr = unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]) };
Ok(expr.trim().to_string())
}
}
fn parse_value(&mut self) -> Result<Value, String> {
self.skip_whitespace();
let c = self.peek().ok_or("Expected value, found EOF")?;
match c {
b'"' => {
self.advance();
let start = self.pos;
let len = self.src.len();
while self.pos < len && self.src[self.pos] != b'"' {
self.pos += 1;
}
if self.pos >= len {
return Err("Unexpected EOF inside string literal".into());
}
let val = std::str::from_utf8(&self.src[start..self.pos]).unwrap().to_string();
self.advance();
Ok(Value::String(val))
}
b'[' => {
self.advance();
let mut items = Vec::new();
loop {
self.skip_whitespace();
if self.consume_if(b']') { break; }
items.push(self.parse_value()?);
self.skip_whitespace();
self.consume_if(b',');
}
Ok(Value::Array(items))
}
b'#' => {
self.advance();
Ok(Value::Color(format!("#{}", self.parse_ident())))
}
b'$' => {
self.advance();
Ok(Value::Variable(self.parse_ident()))
}
b'!' => {
self.advance();
if self.parse_ident() != "rhei" { return Err("Expected !rhei".into()); }
self.consume_if(b':');
Ok(Value::Rhei(self.parse_rhei_expr()?))
}
b'0'..=b'9' | b'-' => {
let s = self.parse_ident();
if s.contains('.') {
s.parse().map(Value::Float).map_err(|_| format!("Invalid float: {}", s))
} else {
s.parse().map(Value::Int).map_err(|_| format!("Invalid int: {}", s))
}
}
_ => {
let s = self.parse_ident();
match s.as_str() {
"true" => Ok(Value::Bool(true)),
"false" => Ok(Value::Bool(false)),
"null" => Ok(Value::Null),
"fs" => {
self.consume_if(b':');
let start = self.pos;
let len = self.src.len();
while self.pos < len {
let cc = self.src[self.pos];
if cc.is_ascii_whitespace() || matches!(cc, b',' | b')' | b'}') {
break;
}
self.pos += 1;
}
let path = unsafe { std::str::from_utf8_unchecked(&self.src[start..self.pos]) }.to_string();
Ok(Value::FsPath(path))
}
_ => Err(format!("Unknown value token: {}", s)),
}
}
}
}
fn parse_properties(&mut self) -> Result<(u32, u32), String> {
let start_idx = self.module.prop_keys.len() as u32;
self.advance();
loop {
self.skip_whitespace();
if self.consume_if(b')') { break; }
let key = self.parse_ident();
if key.is_empty() {
return Err(format!("Expected property identifier, found {:?}", self.peek().map(|b| b as char)));
}
self.skip_whitespace();
if !self.consume_if(b'=') {
return Err(format!("Expected '=' after property key '{}', found {:?}", key, self.peek().map(|b| b as char)));
}
let val = self.parse_value()?;
self.module.prop_keys.push(key);
self.module.prop_values.push(val);
self.skip_whitespace();
self.consume_if(b',');
}
let len = (self.module.prop_keys.len() as u32) - start_idx;
Ok((start_idx, len))
}
fn parse_block(&mut self) -> Result<(u32, u32), String> {
self.skip_whitespace();
if !self.consume_if(b'{') {
return Ok((self.module.hierarchy.len() as u32, 0));
}
let mut direct_children = Vec::new();
loop {
self.skip_whitespace();
if self.consume_if(b'}') { break; }
direct_children.push(self.parse_node()?);
}
let start_idx = self.module.hierarchy.len() as u32;
let len = direct_children.len() as u32;
self.module.hierarchy.extend(direct_children);
Ok((start_idx, len))
}
fn parse_directive(&mut self) -> Result<NodeId, String> {
self.advance();
let name = self.parse_ident();
let dir = match name.as_str() {
"version" => {
self.skip_whitespace();
Directive::Version(self.parse_ident().parse().map_err(|_| "Invalid version")?)
}
"style" => match self.parse_value()? {
Value::String(s) => Directive::Style(s),
_ => return Err("Style must be a string".into()),
},
"global" => {
self.skip_whitespace();
if !self.consume_if(b'$') { return Err("Expected '$' for variable name in @global".into()); }
let name = self.parse_ident();
self.skip_whitespace();
if !self.consume_if(b'=') { return Err(format!("Expected '=' after global variable name '${}'", name)); }
Directive::Global { name, value: self.parse_value()? }
}
"singleton" => {
self.skip_whitespace();
let name = self.parse_ident();
self.skip_whitespace();
let start_idx = self.module.prop_keys.len() as u32;
if !self.consume_if(b'{') { return Err(format!("Expected '{{' for singleton '{}'", name)); }
loop {
self.skip_whitespace();
if self.consume_if(b'}') { break; }
let key = self.parse_ident();
if key.is_empty() { return Err("Expected singleton property identifier".into()); }
self.skip_whitespace();
if !self.consume_if(b'=') { return Err(format!("Expected '=' after singleton property key '{}'", key)); }
let val = self.parse_value()?;
self.module.prop_keys.push(key);
self.module.prop_values.push(val);
self.skip_whitespace();
self.consume_if(b',');
}
let len = (self.module.prop_keys.len() as u32) - start_idx;
Directive::Singleton { name, prop_span: (start_idx, len) }
}
"component" => {
self.skip_whitespace();
let name = self.parse_ident();
let mut params = Vec::new();
self.skip_whitespace();
if self.consume_if(b'(') {
loop {
self.skip_whitespace();
if self.consume_if(b')') { break; }
let pname = self.parse_ident();
if pname.is_empty() { return Err("Expected parameter name in @component".into()); }
self.skip_whitespace();
if !self.consume_if(b':') { return Err(format!("Expected ':' after parameter '{}'", pname)); }
self.skip_whitespace();
let ptype = self.parse_ident();
if ptype.is_empty() { return Err(format!("Expected type after ':' for parameter '{}'", pname)); }
params.push((pname, ptype));
self.skip_whitespace();
self.consume_if(b',');
}
}
Directive::Component { name, params, child_span: self.parse_block()? }
}
"let" => {
self.skip_whitespace();
if !self.consume_if(b'$') { return Err("Expected '$' for variable name in @let".into()); }
let name = self.parse_ident();
self.skip_whitespace();
if !self.consume_if(b'=') { return Err(format!("Expected '=' after variable name '${}'", name)); }
Directive::Let { name, value: self.parse_value()? }
}
"if" => {
self.skip_whitespace();
let condition = self.parse_value()?;
let child_span = self.parse_block()?;
let mut else_span = None;
self.skip_whitespace();
let backup = self.pos;
if self.consume_if(b'@') {
if self.parse_ident() == "else" {
else_span = Some(self.parse_block()?);
} else {
self.pos = backup; }
}
Directive::If { condition, child_span, else_span }
}
"each" => {
self.skip_whitespace();
if !self.consume_if(b'$') { return Err("Expected '$' before item identifier in @each".into()); }
let item = self.parse_ident();
self.skip_whitespace();
if self.parse_ident() != "in" { return Err("Expected 'in' keyword in @each".into()); }
Directive::Each { item, collection: self.parse_value()?, child_span: self.parse_block()? }
}
"on" => {
self.skip_whitespace();
let event = self.parse_ident();
let mut args = Vec::new();
self.skip_whitespace();
if self.consume_if(b'(') {
loop {
self.skip_whitespace();
if self.consume_if(b')') { break; }
let k = self.parse_ident();
if k.is_empty() { return Err("Expected argument name in @on".into()); }
self.skip_whitespace();
if !self.consume_if(b'=') { return Err(format!("Expected '=' after event argument key '{}'", k)); }
args.push((k, self.parse_value()?));
self.skip_whitespace();
self.consume_if(b',');
}
}
Directive::On { event, args, child_span: self.parse_block()? }
}
_ => return Err(format!("Unknown directive: @{}", name)),
};
Ok(NodeId::Directive(self.module.push_directive(dir)))
}
fn parse_element(&mut self) -> Result<NodeId, String> {
let name = self.parse_ident();
let el_id = self.module.push_element(name);
self.skip_whitespace();
if self.peek() == Some(b'(') {
self.module.elem_prop_spans[el_id as usize] = self.parse_properties()?;
}
self.skip_whitespace();
match self.peek() {
Some(b'{') => {
self.module.elem_child_spans[el_id as usize] = self.parse_block()?;
}
Some(ch) if matches!(ch, b'"' | b'#' | b'$' | b'!' | b'-' | b'[' | b'0'..=b'9') => {
self.module.elem_content[el_id as usize] = Some(self.parse_value()?);
}
Some(b'a'..=b'z') => {
let backup = self.pos;
let ident = self.parse_ident();
self.pos = backup;
if matches!(ident.as_str(), "true" | "false" | "null" | "fs") {
self.module.elem_content[el_id as usize] = Some(self.parse_value()?);
}
}
_ => {}
}
Ok(NodeId::Element(el_id))
}
pub fn parse_node(&mut self) -> Result<NodeId, String> {
self.skip_whitespace();
let c = self.peek().ok_or("EOF reached")?;
match c {
b'@' => self.parse_directive(),
b'!' => {
self.advance();
if self.parse_ident() != "rhei" { return Err("Expected rhei".into()); }
self.consume_if(b':');
let expr = self.parse_rhei_expr()?;
Ok(NodeId::Directive(self.module.push_directive(Directive::RheiBlock(expr))))
}
b'A'..=b'Z' => self.parse_element(),
ch if matches!(ch, b'"' | b'#' | b'$' | b'-' | b'[' | b'0'..=b'9') => {
let val = self.parse_value()?;
let el_id = self.module.push_element("#text".to_string());
self.module.elem_content[el_id as usize] = Some(val);
Ok(NodeId::Element(el_id))
}
_ => {
let extract_start = self.pos.saturating_sub(10);
let extract_end = std::cmp::min(self.src.len(), self.pos + 20);
let context = std::str::from_utf8(&self.src[extract_start..extract_end]).unwrap_or("");
Err(format!("Unexpected token: '{}' at position {}. Context: \"...{}...\"", c as char, self.pos, context.trim()))
}
}
}
pub fn parse_all(&mut self) -> Result<(), String> {
self.skip_whitespace();
while self.peek().is_some() {
let node = self.parse_node()?;
self.module.hierarchy.push(node);
self.skip_whitespace();
}
Ok(())
}
}