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 {
src: src.as_bytes(),
pos: 0,
module: ModuleSoA::new(),
}
}
#[inline]
fn peek(&self) -> Option<u8> {
self.src.get(self.pos).copied()
}
#[inline]
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) {
while let Some(c) = self.peek() {
if c.is_ascii_whitespace() {
self.advance();
} else if c == b'/' && self.src.get(self.pos + 1) == Some(&b'/') {
while let Some(cc) = self.peek() {
if cc == b'\n' {
break;
}
self.advance();
}
} else {
break;
}
}
}
fn parse_ident(&mut self) -> String {
let start = self.pos;
while let Some(c) = self.peek() {
if c.is_ascii_alphanumeric() || c == b'-' || c == b'_' || c == b'.' {
self.advance();
} else {
break;
}
}
std::str::from_utf8(&self.src[start..self.pos])
.unwrap()
.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;
while depth > 0 {
let c = self.peek().ok_or("Unexpected EOF in Rhei block")?;
self.advance();
if c == b'{' { depth += 1; }
if c == b'}' { depth -= 1; }
}
let expr = std::str::from_utf8(&self.src[start..self.pos - 1])
.unwrap().trim().to_string();
Ok(expr)
} else {
let start = self.pos;
let mut parens = 0;
let mut brackets = 0;
let mut in_str = false;
let mut escape = false;
while let Some(c) = self.peek() {
if escape {
escape = false;
self.advance();
continue;
}
if c == b'\\' {
escape = true;
self.advance();
continue;
}
if c == b'"' { in_str = !in_str; }
if !in_str {
if c == b'(' { parens += 1; }
if c == b')' {
if parens == 0 { break; }
parens -= 1;
}
if c == b'[' { brackets += 1; }
if c == b']' {
if brackets > 0 { brackets -= 1; }
}
if parens == 0 && brackets == 0 {
if c == b',' || c == b'\n' || c == b'}' || c == b'{' {
break;
}
}
}
self.advance();
}
let expr = std::str::from_utf8(&self.src[start..self.pos])
.unwrap().trim().to_string();
Ok(expr)
}
}
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;
while self.peek() != Some(b'"') {
if self.peek().is_none() {
return Err("Unexpected EOF inside string literal".into());
}
self.advance();
}
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; }
let val = self.parse_value()?;
items.push(val);
self.skip_whitespace();
self.consume_if(b',');
}
Ok(Value::Array(items))
}
b'#' => {
self.advance();
let mut color = String::from("#");
color.push_str(&self.parse_ident());
Ok(Value::Color(color))
}
b'$' => {
self.advance();
Ok(Value::Variable(self.parse_ident()))
}
b'!' => {
self.advance();
let ident = self.parse_ident();
if 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('.') {
Ok(Value::Float(s.parse().unwrap()))
} else {
Ok(Value::Int(s.parse().unwrap()))
}
}
_ => {
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;
while let Some(cc) = self.peek() {
if cc.is_ascii_whitespace() || cc == b',' || cc == b')' || cc == b'}' {
break;
}
self.advance();
}
let path = std::str::from_utf8(&self.src[start..self.pos]).unwrap().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'=') {
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;
}
let node = self.parse_node()?;
direct_children.push(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();
let v = self.parse_ident().parse().unwrap();
Directive::Version(v)
}
"style" => Directive::Style(match self.parse_value()? {
Value::String(s) => s,
_ => return Err("Style must be a string".into()),
}),
"global" => {
self.skip_whitespace();
if !self.consume_if(b'$') {
return Err("Expected $ var".into());
}
let var_name = self.parse_ident();
self.skip_whitespace();
self.consume_if(b'=');
let val = self.parse_value()?;
Directive::Global {
name: var_name,
value: val,
}
}
"singleton" => {
self.skip_whitespace();
let sname = self.parse_ident();
self.skip_whitespace();
let start_idx = self.module.prop_keys.len() as u32;
if self.consume_if(b'{') {
loop {
self.skip_whitespace();
if self.consume_if(b'}') {
break;
}
let key = self.parse_ident();
if key.is_empty() {
return Err(format!(
"Expected singleton property identifier, found {:?}",
self.peek().map(|b| b as char)
));
}
self.skip_whitespace();
self.consume_if(b'=');
let val = self.parse_value()?;
self.module.prop_keys.push(key);
self.module.prop_values.push(val);
}
}
let len = (self.module.prop_keys.len() as u32) - start_idx;
Directive::Singleton {
name: sname,
prop_span: (start_idx, len),
}
}
"component" => {
self.skip_whitespace();
let cname = 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(format!(
"Expected parameter name in @component, found {:?}",
self.peek().map(|b| b as char)
));
}
self.skip_whitespace();
self.consume_if(b':');
self.skip_whitespace();
let ptype = self.parse_ident();
params.push((pname, ptype));
self.skip_whitespace();
self.consume_if(b',');
}
}
let child_span = self.parse_block()?;
Directive::Component {
name: cname,
params,
child_span,
}
}
"let" => {
self.skip_whitespace();
self.consume_if(b'$');
let var_name = self.parse_ident();
self.skip_whitespace();
self.consume_if(b'=');
let val = self.parse_value()?;
Directive::Let {
name: var_name,
value: val,
}
}
"if" => {
self.skip_whitespace();
let expr_str = self.parse_rhei_expr()?;
let condition = Value::Rhei(expr_str);
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();
self.consume_if(b'$');
let item = self.parse_ident();
self.skip_whitespace();
let in_kw = self.parse_ident();
if in_kw != "in" {
return Err("Expected 'in' in @each".into());
}
let collection = self.parse_value()?;
let child_span = self.parse_block()?;
Directive::Each {
item,
collection,
child_span,
}
}
"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();
self.skip_whitespace();
self.consume_if(b'=');
let v = self.parse_value()?;
args.push((k, v));
self.skip_whitespace();
self.consume_if(b',');
}
}
let child_span = self.parse_block()?;
Directive::On {
event,
args,
child_span,
}
}
_ => 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'(') {
let span = self.parse_properties()?;
self.module.elem_prop_spans[el_id as usize] = span;
}
self.skip_whitespace();
let p = self.peek();
if p == Some(b'{') {
let span = self.parse_block()?;
self.module.elem_child_spans[el_id as usize] = span;
} else if let Some(ch) = p {
if ch == b'"' || ch == b'#' || ch == b'$' || ch == b'!' || ch == b'-' || ch.is_ascii_digit() || ch == b'[' || ch.is_ascii_lowercase() {
let val = self.parse_value()?;
self.module.elem_content[el_id as usize] = Some(val);
}
}
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();
let i = self.parse_ident();
if i != "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(),
b'"' | b'#' | b'$' | b'-' | b'0'..=b'9' | b'[' => {
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))
}
ch if ch.is_ascii_lowercase() => {
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 around: \"...{}...\"",
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(())
}
}