use std::collections::HashMap;
use std::str::FromStr;
use crate::error::{BaseParseError, ParseError};
use crate::lexing::{AtomOr, Atoms, Data, Line};
use crate::{Dialog, DialogNode, DialogTree};
type Res<I, O, E = BaseParseError> = Result<(I, O), E>;
fn not_blank_dialog_line(mut s: &str) -> Res<&str, Line<&str, &str, &str>> {
loop {
let (new_s, line) = crate::lexing::line(s)?;
s = new_s;
if let Data::Blank = line.data {
continue;
}
return Ok((s, line));
}
}
fn at_same_indent(s: &str, indent: usize) -> Res<&str, Line<&str, &str, &str>> {
let (s, line) = not_blank_dialog_line(s)?;
if line.indent > indent {
return Err(BaseParseError::UnexpectedIndent);
}
if line.indent < indent {
return Err(BaseParseError::UnexpectedDedent);
}
Ok((s, line))
}
#[allow(clippy::type_complexity)]
fn character_says(s: &str, indent: usize) -> Res<&str, (&str, Vec<AtomOr<&str, &str>>)> {
let (s, line) = at_same_indent(s, indent)?;
if let Data::CharacterSays { character, text } = line.data {
Ok((s, (character, text)))
} else {
Err(BaseParseError::UnexpectedLine(
"character_says",
line.into_strings(),
))
}
}
fn message(s: &str, indent: usize) -> Res<&str, Vec<AtomOr<&str, &str>>> {
let (s, line) = at_same_indent(s, indent)?;
if let Data::JustText(text) = line.data {
Ok((s, text))
} else {
Err(BaseParseError::UnexpectedLine(
"message",
line.into_strings(),
))
}
}
#[allow(clippy::type_complexity)]
fn response<DA: FromStr, IF: FromStr, TE: FromStr>(
s: &str,
indent: usize,
) -> Res<
&str,
(Vec<AtomOr<&str, &str>>, Option<IF>, DialogTree<DA, IF, TE>),
ParseError<DA::Err, IF::Err, TE::Err>,
> {
let (mut s, line) = at_same_indent(s, indent)?;
if let Data::Response {
text,
only_if,
go_to,
} = line.data
{
let resp = (
text,
only_if
.map(|i| i.parse().map_err(ParseError::IF))
.transpose()?,
if let Some(gt) = go_to {
DialogTree {
nodes: vec![DialogNode::GoTo(gt.into())],
}
} else {
let new_indent = not_blank_dialog_line(s)
.map(|(_, Line { indent, .. })| indent)
.unwrap_or(indent + 1);
if new_indent == indent {
DialogTree { nodes: Vec::new() }
} else if let Ok((new_s, tree)) = dialog_tree(s, new_indent) {
s = new_s;
tree
} else {
DialogTree { nodes: Vec::new() }
}
},
);
Ok((s, resp))
} else {
Err(ParseError::Base(BaseParseError::UnexpectedLine(
"response",
line.into_strings(),
)))
}
}
#[allow(clippy::type_complexity)]
fn responses<DA: FromStr, IF: FromStr, TE: FromStr>(
mut s: &str,
indent: usize,
) -> Res<&str, Vec<(Vec<AtomOr<&str, &str>>, Option<IF>, DialogTree<DA, IF, TE>)>> {
let mut responses = Vec::new();
while let Ok((new_s, resp)) = response(s, indent) {
s = new_s;
responses.push(resp);
}
if responses.is_empty() {
return Err(BaseParseError::UnexpectedLine(
"responses",
not_blank_dialog_line(s)?.1.into_strings(),
));
}
Ok((s, responses))
}
enum Cond {
If,
Elif,
Else,
}
#[allow(clippy::type_complexity)]
fn conditional<DA: FromStr, IF: FromStr, TE: FromStr>(
s: &str,
indent: usize,
) -> Res<&str, (Cond, Option<IF>, DialogTree<DA, IF, TE>), ParseError<DA::Err, IF::Err, TE::Err>> {
let (mut s, line) = at_same_indent(s, indent)?;
let (ty, cond) = match line.data {
Data::If(cond) => (Cond::If, Some(cond)),
Data::Elif(cond) => (Cond::Elif, Some(cond)),
Data::Else => (Cond::Else, None),
_ => {
return Err(ParseError::Base(BaseParseError::UnexpectedLine(
"response",
line.into_strings(),
)))
}
};
let new_indent = not_blank_dialog_line(s)
.map(|(_, Line { indent, .. })| indent)
.unwrap_or(indent + 1);
let subtree = if let Ok((new_s, tree)) = dialog_tree(s, new_indent) {
s = new_s;
tree
} else {
DialogTree { nodes: Vec::new() }
};
Ok((
s,
(
ty,
cond.map(|c| c.parse().map_err(ParseError::IF))
.transpose()?,
subtree,
),
))
}
#[allow(clippy::type_complexity)]
fn conditionals<DA: FromStr, IF: FromStr, TE: FromStr>(
mut s: &str,
indent: usize,
) -> Res<&str, Vec<(Option<IF>, DialogTree<DA, IF, TE>)>> {
let mut saw_else = false;
let mut conditionals = Vec::new();
while let Ok((new_s, (ty, cond, tree))) = conditional(s, indent) {
let valid = match (ty, conditionals.len(), saw_else) {
(Cond::If, 0, false) => true,
(Cond::Elif, i, false) if i > 0 => true,
(Cond::Else, i, false) if i > 0 => {
saw_else = true;
true
}
_ => false,
};
if !valid {
return Err(BaseParseError::UnexpectedLine(
"If line",
not_blank_dialog_line(s)?.1.into_strings(),
));
}
s = new_s;
conditionals.push((cond, tree));
}
if conditionals.is_empty() {
return Err(BaseParseError::UnexpectedLine(
"conditionals",
not_blank_dialog_line(s)?.1.into_strings(),
));
}
Ok((s, conditionals))
}
fn go_to(s: &str, indent: usize) -> Res<&str, &str> {
let (s, line) = at_same_indent(s, indent)?;
if let Data::GoTo(gt) = line.data {
Ok((s, gt))
} else {
Err(BaseParseError::UnexpectedLine("go_to", line.into_strings()))
}
}
fn do_action(s: &str, indent: usize) -> Res<&str, &str, BaseParseError> {
let (s, line) = at_same_indent(s, indent)?;
if let Data::DoAction(action) = line.data {
Ok((s, action))
} else {
Err(BaseParseError::UnexpectedLine(
"do_action",
line.into_strings(),
))
}
}
#[allow(clippy::type_complexity)]
fn dialog_node<DA: FromStr, IF: FromStr, TE: FromStr>(
s: &str,
indent: usize,
) -> Res<&str, DialogNode<DA, IF, TE>, ParseError<DA::Err, IF::Err, TE::Err>> {
if let Ok((s, (character, text))) = character_says(s, indent) {
Ok((
s,
DialogNode::CharacterSays(character.into(), text.parse().map_err(ParseError::TE)?),
))
} else if let Ok((s, text)) = message(s, indent) {
Ok((
s,
DialogNode::Message(text.parse().map_err(ParseError::TE)?),
))
} else if let Ok((s, responses)) = responses::<DA, IF, TE>(s, indent) {
Ok((
s,
DialogNode::Responses(
responses
.into_iter()
.map(|(text, i, gt)| Ok((text.parse().map_err(ParseError::TE)?, i, gt)))
.collect::<Result<_, ParseError<DA::Err, IF::Err, TE::Err>>>()?,
),
))
} else if let Ok((s, conditionals)) = conditionals(s, indent) {
Ok((s, DialogNode::Conditional(conditionals)))
} else if let Ok((s, gt)) = go_to(s, indent) {
Ok((s, DialogNode::GoTo(gt.into())))
} else if let Ok((s, action)) = do_action(s, indent) {
Ok((
s,
DialogNode::DoAction(action.parse().map_err(ParseError::DoAction)?),
))
} else {
Err(ParseError::Base(BaseParseError::NoDialogNodeFound(
not_blank_dialog_line(s)?.1.into_strings(),
)))
}
}
#[allow(clippy::type_complexity)]
fn dialog_tree<DA: FromStr, IF: FromStr, TE: FromStr>(
mut s: &str,
indent: usize,
) -> Res<&str, DialogTree<DA, IF, TE>, ParseError<DA::Err, IF::Err, TE::Err>> {
let mut nodes = Vec::new();
loop {
match dialog_node(s, indent) {
Ok((new_s, node)) => {
s = new_s;
nodes.push(node);
}
Err(e) => match e {
ParseError::DoAction(_) | ParseError::IF(_) | ParseError::TE(_) => {
tracing::info!("DoAction or IF parse error.");
return Err(e);
}
_ => {
tracing::info!("Base error");
break;
}
},
}
}
if nodes.is_empty() {
return Err(ParseError::Base(BaseParseError::EmptyDialogTree));
}
Ok((s, DialogTree { nodes }))
}
fn get_section(s: &str) -> Res<&str, &str> {
let (s, line) = not_blank_dialog_line(s)?;
if let Data::SectionStart(section_name) = line.data {
if line.indent == 0 {
Ok((s, section_name))
} else {
Err(BaseParseError::IndentedSectionStartError)
}
} else {
Err(BaseParseError::ExpectedSectionStart(line.into_strings()))
}
}
#[allow(clippy::type_complexity)]
pub fn dialog<DA: FromStr, IF: FromStr, TE: FromStr>(
mut s: &str,
) -> Result<Dialog<DA, IF, TE>, ParseError<DA::Err, IF::Err, TE::Err>> {
let mut sections = HashMap::new();
loop {
let (new_s, section) = get_section(s)?;
let (new_s, tree) = dialog_tree(new_s, 0)?;
if sections.contains_key(section) {
return Err(ParseError::Base(BaseParseError::DuplicateSectionKey(
section.into(),
)));
}
sections.insert(section.into(), tree);
s = new_s;
if s.is_empty() {
return Ok(Dialog { sections });
}
}
}
#[cfg(test)]
mod tests {
use crate::Atom;
use super::*;
use pretty_assertions::assert_eq;
use test_log::test;
#[test]
fn test_dialog_section() -> Result<(), anyhow::Error> {
assert_eq!(
dialog::<String, String, String>(
r#":: title
Ch1: Hello! `terp`
- Hello!
- Fuck you
Ch1: doot ~doot~
"#
),
Ok(Dialog {
sections: HashMap::from([(
"title".into(),
DialogTree {
nodes: vec![
DialogNode::CharacterSays(
"Ch1".into(),
vec![
AtomOr::Atom(Atom::Text("Hello! ".into())),
AtomOr::Interpolate("terp".into())
]
),
DialogNode::Responses(vec![
(
vec![AtomOr::Atom(Atom::Text("Hello!".into()))],
None,
DialogTree { nodes: vec![] }
),
(
vec![AtomOr::Atom(Atom::Text("Fuck you".into()))],
None,
DialogTree {
nodes: vec![DialogNode::CharacterSays(
"Ch1".into(),
vec![
AtomOr::Atom(Atom::Text("doot ".into())),
AtomOr::Atom(Atom::Wave("doot".into()))
]
)]
}
)
])
]
}
)])
})
);
Ok(())
}
}