use fixed32::Fp;
use seq_map::SeqMap;
#[derive(Clone, Debug, PartialEq)]
pub enum Node {
Text(String),
Element(Element),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ArgValue {
Int(i64),
Fixed(Fp),
Bool(bool),
Hex(String),
Keyword(String),
Str(String),
}
#[derive(Clone, Debug, PartialEq)]
pub struct Element {
pub name: String,
pub args: SeqMap<String, ArgValue>,
pub children: Vec<Node>,
}
#[derive(Debug)]
pub struct ParseError {
pub message: String,
pub line: usize,
pub column: usize,
}
pub struct Parser<'a> {
input: &'a [u8],
pos: usize,
line: usize,
col: usize,
pub errors: Vec<ParseError>,
}
impl<'a> Parser<'a> {
pub fn new(input: &'a str) -> Self {
Self {
input: input.as_bytes(),
pos: 0,
line: 1,
col: 1,
errors: Vec::new(),
}
}
fn peek(&self) -> Option<u8> {
self.input.get(self.pos).copied()
}
fn next(&mut self) -> Option<u8> {
let b = self.peek()?;
self.pos += 1;
if b == b'\n' {
self.line += 1;
self.col = 1;
} else {
self.col += 1;
}
Some(b)
}
fn eof(&self) -> bool {
self.pos >= self.input.len()
}
fn starts_with(&self, pat: &[u8]) -> bool {
self.input
.get(self.pos..)
.map_or(false, |r| r.starts_with(pat))
}
fn consume_ws(&mut self) {
while let Some(b) = self.peek() {
if b.is_ascii_whitespace() {
self.next();
} else {
break;
}
}
}
pub fn parse(&mut self) -> Vec<Node> {
self.parse_nodes(None)
}
fn parse_nodes(&mut self, stop_tag: Option<&str>) -> Vec<Node> {
let mut nodes = Vec::new();
while !self.eof() {
if let Some(tag) = stop_tag {
if self.starts_with(format!("[/{tag}]").as_bytes()) {
break;
}
}
if self.starts_with(b"[/") {
let (l, c) = (self.line, self.col);
self.errors.push(ParseError {
message: "Unexpected closing tag".into(),
line: l,
column: c,
});
while let Some(b) = self.next() {
if b == b']' {
break;
}
}
continue;
}
if self.peek() == Some(b'[') {
nodes.push(self.parse_element());
} else {
let txt = self.parse_text();
if !txt.is_empty() {
nodes.push(Node::Text(txt));
}
}
}
nodes
}
fn parse_text(&mut self) -> String {
let mut out = String::new();
while let Some(b) = self.peek() {
if b == b'[' {
break;
}
if b == b'\\' {
if let Some(next) = self.input.get(self.pos + 1) {
if *next == b'[' || *next == b']' {
self.next();
let escaped = self.next().unwrap();
out.push(escaped as char);
continue;
}
}
}
out.push(self.next().unwrap() as char);
}
out
}
fn parse_element(&mut self) -> Node {
let (l, c) = (self.line, self.col);
self.next();
self.consume_ws();
let name_start = self.pos;
while let Some(b) = self.peek() {
if b.is_ascii_alphanumeric() {
self.next();
} else {
break;
}
}
let name = String::from_utf8_lossy(&self.input[name_start..self.pos]).to_string();
if name.is_empty() {
self.errors.push(ParseError {
message: "Missing tag name".into(),
line: l,
column: c,
});
}
let mut args = SeqMap::new();
loop {
self.consume_ws();
match self.peek() {
Some(b'=') | Some(b']') | None => break,
Some(_) => {
let val = self.parse_arg_value();
self.consume_ws();
if self.peek() == Some(b'=') {
if let ArgValue::Keyword(k) = val.clone() {
self.next();
self.consume_ws();
let v2 = self.parse_arg_value();
let _ = args.insert(k, v2);
} else {
self.errors.push(ParseError {
message: "Invalid arg name".into(),
line: l,
column: c,
});
}
} else {
let _ = args.insert("value".into(), val);
}
}
}
}
if self.next() != Some(b']') {
self.errors.push(ParseError {
message: format!("Unterminated [{}] tag", name),
line: l,
column: c,
});
}
let children = self.parse_nodes(Some(&name));
if self.starts_with(format!("[/{name}]").as_bytes()) {
for _ in 0..(name.len() + 3) {
self.next();
}
} else {
self.errors.push(ParseError {
message: format!("Missing closing tag [/{name}]"),
line: self.line,
column: self.col,
});
}
Node::Element(Element {
name,
args,
children,
})
}
fn parse_arg_value(&mut self) -> ArgValue {
self.consume_ws();
if self.peek() == Some(b'"') {
self.next();
let start = self.pos;
while let Some(b) = self.peek() {
if b == b'"' {
break;
}
self.next();
}
let s = String::from_utf8_lossy(&self.input[start..self.pos]).to_string();
self.next();
return ArgValue::Str(s);
}
if self.peek() == Some(b'#') {
let start = self.pos;
self.next();
while let Some(b) = self.peek() {
if (b as char).is_ascii_hexdigit() {
self.next();
} else {
break;
}
}
return ArgValue::Hex(
String::from_utf8_lossy(&self.input[start..self.pos]).to_string(),
);
}
let start = self.pos;
while let Some(b) = self.peek() {
if b.is_ascii_whitespace() || b == b']' || b == b'=' {
break;
}
self.next();
}
let tok = String::from_utf8_lossy(&self.input[start..self.pos]).to_string();
if tok == "true" {
ArgValue::Bool(true)
} else if tok == "false" {
ArgValue::Bool(false)
} else if let Ok(i) = tok.parse::<i64>() {
ArgValue::Int(i)
} else if let Ok(f) = tok.parse::<f32>() {
let a: Fp = f.into();
ArgValue::Fixed(a)
} else {
ArgValue::Keyword(tok)
}
}
}
pub fn parse(input: &str) -> Vec<Node> {
Parser::new(input).parse()
}