pub(super) mod assignments;
pub(super) mod body;
pub(super) mod command;
pub(super) mod stmt;
pub(super) mod text;
use std::sync::Arc;
use indexmap::IndexMap;
use crate::compiler::ast::{Expr, Node, Stmt};
use crate::error::{DialogueError, Result};
type HeaderMap = IndexMap<String, String>;
type WhenExpr = Option<Arc<Expr>>;
use self::assignments::parse_expr_arc;
pub(super) fn into_stmt_list(body: Vec<Stmt>) -> crate::compiler::ast::StmtList {
Arc::from(body)
}
pub fn parse(file: &str, source: &str) -> Result<Vec<Node>> {
let mut p = Parser::new(file, source);
p.parse_file()
}
pub(super) struct Parser<'src> {
pub(super) file: &'src str,
pub(super) lines: Vec<(usize, &'src str)>,
pub(super) pos: usize,
pub(super) id_counter: usize,
}
impl<'src> Parser<'src> {
fn new(file: &'src str, source: &'src str) -> Self {
let lines = source
.lines()
.enumerate()
.map(|(i, l)| (i + 1, l))
.collect();
Self {
file,
lines,
pos: 0,
id_counter: 0,
}
}
pub(super) fn next_id(&mut self) -> String {
self.id_counter += 1;
format!("blk{}", self.id_counter)
}
pub(super) fn peek(&self) -> Option<(usize, &'src str)> {
self.lines.get(self.pos).copied()
}
pub(super) fn advance(&mut self) -> Option<(usize, &'src str)> {
let line = self.lines.get(self.pos).copied();
self.pos += 1;
line
}
pub(super) fn err(&self, line: usize, msg: impl Into<String>) -> DialogueError {
DialogueError::Parse {
file: self.file.to_owned(),
line,
message: msg.into(),
}
}
pub(super) fn skip_blank_and_comments(&mut self) {
while let Some((_, content)) = self.peek() {
let t = content.trim();
if t.is_empty() || t.starts_with("//") {
self.advance();
} else {
break;
}
}
}
pub(super) fn last_lineno(&self) -> usize {
if self.pos == 0 {
self.lines.first().map_or(1, |&(n, _)| n)
} else {
self.lines
.get(self.pos - 1)
.or_else(|| self.lines.last())
.map_or(1, |&(n, _)| n)
}
}
}
impl Parser<'_> {
fn parse_file(&mut self) -> Result<Vec<Node>> {
let mut nodes = Vec::new();
loop {
self.skip_blank_and_comments();
if self.peek().is_none() {
break;
}
nodes.push(self.parse_node()?);
}
Ok(nodes)
}
fn parse_node(&mut self) -> Result<Node> {
let (headers, when) = self.parse_headers()?;
let title = headers
.get("title")
.cloned()
.ok_or_else(|| DialogueError::Parse {
file: self.file.to_owned(),
line: self.last_lineno(),
message: "node is missing a `title:` header".into(),
})?;
let tags = headers
.get("tags")
.map(|s| s.split_whitespace().map(str::to_owned).collect())
.unwrap_or_default();
let mut extra = headers;
extra.shift_remove("title");
extra.shift_remove("tags");
self.expect_body_start()?;
let body = self.parse_body(0)?;
self.expect_node_end()?;
Ok(Node {
title,
tags,
headers: extra,
when,
body: into_stmt_list(body),
})
}
fn parse_headers(&mut self) -> Result<(HeaderMap, WhenExpr)> {
let mut map = IndexMap::new();
let mut when: WhenExpr = None;
let file = self.file;
loop {
match self.peek() {
None => break,
Some((lineno, content)) => {
let t = content.trim();
if t == "---" || t.is_empty() || t.starts_with("//") {
break;
}
if t == "===" {
return Err(self.err(
lineno,
"found `===` where `---` body delimiter was expected - \
is the `---` line missing?",
));
}
if let Some(colon) = t.find(':') {
let key = t[..colon].trim().to_owned();
let val = t[colon + 1..].trim().to_owned();
if key == "when" {
when = Some(parse_expr_arc(&val, "when:", lineno, file)?);
} else {
map.insert(key, val);
}
self.advance();
} else {
return Err(self.err(lineno, format!("invalid header line: `{t}`")));
}
}
}
}
Ok((map, when))
}
fn expect_body_start(&mut self) -> Result<()> {
match self.advance() {
Some((_, l)) if l.trim() == "---" => Ok(()),
Some((n, l)) => Err(self.err(n, format!("expected `---`, got `{}`", l.trim()))),
None => Err(self.err(self.last_lineno(), "unexpected end of file, expected `---`")),
}
}
fn expect_node_end(&mut self) -> Result<()> {
loop {
match self.peek() {
None => {
return Err(
self.err(self.last_lineno(), "unexpected end of file, expected `===`")
);
}
Some((_, l)) if l.trim().is_empty() || l.trim().starts_with("//") => {
self.advance();
}
Some((_, l)) if l.trim() == "===" => {
self.advance();
return Ok(());
}
Some((n, l)) => {
let t = l.trim();
let hint = if matches!(t, "<<endif>>" | "<<endonce>>") {
format!(
" - `{t}` has no matching opening block; \
check indentation and that every `<<if>>` or `<<once>>` \
has a corresponding `{t}`"
)
} else if t == "<<else>>" || t.starts_with("<<elseif") {
" - unexpected `<<else>>`/`<<elseif>>`; \
check that the matching `<<if>>` is correct"
.to_owned()
} else {
String::new()
};
return Err(self.err(n, format!("expected `===`, got `{t}`{hint}")));
}
}
}
}
}