use std::{fmt::Display, str::FromStr};
use crate::error::LexError;
use nom::{
branch::alt,
bytes::complete::{is_not, tag, take_until},
character::complete::{
alpha1, alphanumeric1, char, line_ending, not_line_ending, space0, space1,
},
combinator::{all_consuming, cond, eof, map, map_parser, recognize},
error::{context, VerboseError, VerboseErrorKind},
multi::many0_count,
sequence::{pair, tuple},
Err, IResult,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize, Clone)]
#[serde(rename = "a")]
pub enum Atom<T = String> {
#[serde(rename = "t")]
Text(T),
#[serde(rename = "s")]
Shake(T),
#[serde(rename = "w")]
Wave(T),
#[serde(rename = "sp")]
Sparkle(T),
#[serde(rename = "sb")]
StartBold,
#[serde(rename = "eb")]
EndBold,
#[serde(rename = "si")]
StartItalic,
#[serde(rename = "ei")]
EndItalic,
}
impl<T> Atom<T> {
fn map<F, T2>(self, f: F) -> Atom<T2>
where
F: FnOnce(T) -> T2,
{
use Atom::*;
match self {
Text(t) => Text(f(t)),
Shake(s) => Shake(f(s)),
Wave(w) => Wave(f(w)),
Sparkle(s) => Sparkle(f(s)),
StartBold => StartBold,
EndBold => EndBold,
StartItalic => StartItalic,
EndItalic => EndItalic,
}
}
}
impl<T: std::fmt::Display> std::fmt::Display for Atom<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use Atom::*;
match self {
Text(t) => write!(f, "{t}"),
Shake(t) => write!(f, "#{t}#"),
Wave(t) => write!(f, "~{t}~"),
Sparkle(t) => write!(f, "&&{t}&&"),
StartBold | EndBold => write!(f, "**"),
StartItalic | EndItalic => write!(f, "*"),
}
}
}
#[derive(Debug, Hash, PartialEq, Eq, Serialize, Deserialize, Clone)]
#[serde(rename = "ao")]
pub enum AtomOr<A = String, I = String> {
#[serde(rename = "a")]
Atom(Atom<A>),
#[serde(rename = "i")]
Interpolate(I),
}
impl<A: Display, I: Display> Display for AtomOr<A, I> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use AtomOr::*;
match self {
Atom(a) => write!(f, "{a}"),
Interpolate(i) => write!(f, "`{i}`"),
}
}
}
impl<A, I> AtomOr<A, I> {
fn map_atom<F, A2>(self, f: F) -> AtomOr<A2, I>
where
F: FnOnce(Atom<A>) -> Atom<A2>,
{
match self {
AtomOr::Atom(a) => AtomOr::Atom(f(a)),
AtomOr::Interpolate(i) => AtomOr::Interpolate(i),
}
}
fn map_interpolate<F, I2>(self, f: F) -> AtomOr<A, I2>
where
F: FnOnce(I) -> I2,
{
match self {
AtomOr::Interpolate(i) => AtomOr::Interpolate(f(i)),
AtomOr::Atom(a) => AtomOr::Atom(a),
}
}
}
impl<A, I, E> AtomOr<A, Result<I, E>> {
fn invert(self) -> Result<AtomOr<A, I>, E> {
match self {
AtomOr::Atom(a) => Ok(AtomOr::Atom(a)),
AtomOr::Interpolate(Ok(o)) => Ok(AtomOr::Interpolate(o)),
AtomOr::Interpolate(Err(e)) => Err(e),
}
}
}
pub trait Atoms {
fn parse<I: FromStr>(self) -> Result<Vec<AtomOr<String, I>>, I::Err>;
}
impl<A: Into<String>, S: AsRef<str>> Atoms for Vec<AtomOr<A, S>> {
fn parse<I: FromStr>(self) -> Result<Vec<AtomOr<String, I>>, I::Err> {
self.into_iter()
.map(|ao| {
ao.map_atom(|atom| atom.map(A::into))
.map_interpolate(|i| i.as_ref().parse())
.invert()
})
.collect()
}
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub(crate) enum Data<S, A, I> {
SectionStart(S),
CharacterSays {
character: S,
text: Vec<AtomOr<A, I>>,
},
Response {
text: Vec<AtomOr<A, I>>,
only_if: Option<S>,
go_to: Option<S>,
},
GoTo(S),
JustText(Vec<AtomOr<A, I>>),
DoAction(S),
If(S),
Elif(S),
Else,
Blank,
}
impl<S: Display, A: Display, I: Display> Display for Data<S, A, I> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use Data::*;
let fmt = |txt: &Vec<AtomOr<A, I>>| txt.iter().map(|t| format!("{t}")).collect::<String>();
match self {
SectionStart(name) => write!(f, ":: {}", name),
CharacterSays { character, text } => write!(f, "{character}: {}", fmt(text)),
Response {
text,
only_if: Some(oi),
go_to: Some(to),
} => write!(f, "- {} [{oi}] => {to}", fmt(text)),
Response {
text,
only_if: Some(oi),
..
} => write!(f, "- {} [{oi}]", fmt(text)),
Response {
text,
go_to: Some(to),
..
} => write!(f, "- {} => {to}", fmt(text)),
Response { text, .. } => write!(f, "- {}", fmt(text)),
GoTo(to) => write!(f, "=> {to}"),
JustText(ref text) => write!(f, "{}", fmt(text)),
DoAction(ref text) => write!(f, "do {text}"),
If(i) => write!(f, "if {i}"),
Elif(i) => write!(f, "elif {i}"),
Else => write!(f, "else"),
Blank => write!(f, "<blank line>"),
}
}
}
impl<S, A, I> Data<S, A, I> {
fn into_strings(self) -> Data<String, String, String>
where
S: Into<String>,
A: Into<String>,
I: AsRef<str>,
{
use Data::*;
match self {
SectionStart(s) => SectionStart(s.into()),
CharacterSays { character, text } => CharacterSays {
character: character.into(),
text: text.parse().expect("parsing to string never fails"),
},
Response {
text,
only_if,
go_to,
} => Response {
text: text.parse().expect("parsing to string never fails"),
only_if: only_if.map(S::into),
go_to: go_to.map(S::into),
},
GoTo(s) => GoTo(s.into()),
JustText(t) => JustText(t.parse().expect("parsing to string never fails")),
DoAction(a) => DoAction(a.into()),
If(i) => If(i.into()),
Elif(ei) => Elif(ei.into()),
Else => Else,
Blank => Blank,
}
}
}
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct Line<S, A, I> {
pub(crate) indent: usize,
pub(crate) data: Data<S, A, I>,
}
impl<S, A, I> Line<S, A, I> {
pub fn into_strings(self) -> Line<String, String, String>
where
S: Into<String>,
A: Into<String>,
I: AsRef<str>,
{
Line {
indent: self.indent,
data: self.data.into_strings(),
}
}
}
impl<S: Display, A: Display, I: Display> Display for Line<S, A, I> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}{}", " ".repeat(self.indent), self.data)
}
}
type Res<T, U> = IResult<T, U, VerboseError<T>>;
fn fenced2<'a>(start: &'a str, end: &'a str) -> impl FnMut(&'a str) -> Res<&'a str, &'a str> {
map(tuple((tag(start), take_until(end), tag(end))), |t| t.1)
}
fn fenced<'a>(wrapping: &'a str) -> impl FnMut(&'a str) -> Res<&'a str, &'a str> {
fenced2(wrapping, wrapping)
}
fn bold_italic(s: &str) -> Res<&str, &str> {
context("bold_italic", fenced("***"))(s)
}
fn bold(s: &str) -> Res<&str, &str> {
context("bold", fenced("**"))(s)
}
fn italic(s: &str) -> Res<&str, &str> {
context("italic", fenced("*"))(s)
}
fn wave(s: &str) -> Res<&str, &str> {
context("wave", fenced("~"))(s)
}
fn shake(s: &str) -> Res<&str, &str> {
context("shake", fenced("#"))(s)
}
fn sparkle(s: &str) -> Res<&str, &str> {
context("sparkle", fenced("&&"))(s)
}
fn interpolate(s: &str) -> Res<&str, &str> {
context("interpolate", fenced("`"))(s)
}
fn indent(s: &str) -> Res<&str, usize> {
context("indent", map(space0, str::len))(s)
}
fn directive(s: &str) -> Res<&str, Vec<AtomOr<&str, &str>>> {
context(
"directive",
alt((
map_parser(bold_italic, |bi| {
let mut v = vec![
AtomOr::Atom(Atom::StartBold),
AtomOr::Atom(Atom::StartItalic),
];
let (s, inner) = text_data(bi)?;
v.extend(inner);
v.extend([AtomOr::Atom(Atom::EndItalic), AtomOr::Atom(Atom::EndBold)]);
Ok((s, v))
}),
map_parser(bold, |b| {
let mut v = vec![AtomOr::Atom(Atom::StartBold)];
let (s, inner) = text_data(b)?;
v.extend(inner);
v.push(AtomOr::Atom(Atom::EndBold));
Ok((s, v))
}),
map_parser(italic, |b| {
let mut v = vec![AtomOr::Atom(Atom::StartItalic)];
let (s, inner) = text_data(b)?;
v.extend(inner);
v.push(AtomOr::Atom(Atom::EndItalic));
Ok((s, v))
}),
map(interpolate, |i| vec![AtomOr::Interpolate(i)]),
map(shake, |s| vec![AtomOr::Atom(Atom::Shake(s))]),
map(wave, |s| vec![AtomOr::Atom(Atom::Wave(s))]),
map(sparkle, |s| vec![AtomOr::Atom(Atom::Sparkle(s))]),
)),
)(s)
}
fn ident(s: &str) -> Res<&str, &str> {
recognize(pair(
alt((alpha1, tag("_"))),
many0_count(alt((alphanumeric1, tag("_")))),
))(s)
}
fn if_guard(s: &str) -> Res<&str, &str> {
context("if_guard", fenced2("[if ", "]"))(s)
}
fn go_to(s: &str) -> Res<&str, &str> {
context(
"go_to",
map(tuple((tag("=>"), space1, ident)), |(_, _, go_to)| go_to),
)(s)
}
fn end_of_line_bits(s: &str) -> Res<&str, (Option<&str>, Option<&str>)> {
let (s, guard) = match if_guard(s) {
Ok((s, guard)) => (s, Some(guard)),
Err(_) => (s, None),
};
let (s, gt) = match tuple((cond(guard.is_some(), space1), go_to))(s) {
Ok((s, (_, g))) => (s, Some(g)),
Err(_) => (s, None),
};
Ok((s, (guard, gt)))
}
#[allow(clippy::type_complexity)]
fn text_data_with_ending_stuff(
s: &str,
) -> Res<&str, (Vec<AtomOr<&str, &str>>, (Option<&str>, Option<&str>))> {
let mut output = Vec::new();
let mut ending_stuff = (None, None);
let (excess, mut current_input) = not_line_ending(s)?;
while !current_input.is_empty() {
let mut found_directive = false;
for (current_index, _) in current_input.char_indices() {
match (
directive(¤t_input[current_index..]),
all_consuming(tuple((space1, end_of_line_bits)))(¤t_input[current_index..]),
) {
(_, Ok((remaining, (_, eolb)))) => {
let leading_text = ¤t_input[0..current_index];
if !leading_text.is_empty() {
output.push(AtomOr::Atom(Atom::Text(leading_text)));
}
ending_stuff = eolb;
current_input = remaining;
found_directive = true;
break;
}
(Ok((remaining, parsed)), _) => {
let leading_text = ¤t_input[0..current_index];
if !leading_text.is_empty() {
output.push(AtomOr::Atom(Atom::Text(leading_text)));
}
output.extend(parsed);
current_input = remaining;
found_directive = true;
break;
}
(Err(nom::Err::Error(_)), _) => {}
(Err(e), _) => return Err(e),
}
}
if !found_directive {
output.push(AtomOr::Atom(Atom::Text(current_input)));
break;
}
}
if output.is_empty() {
return Err(Err::Error(VerboseError {
errors: vec![(excess, VerboseErrorKind::Context("text_data"))],
}));
}
Ok((excess, (output, ending_stuff)))
}
fn text_data(s: &str) -> Res<&str, Vec<AtomOr<&str, &str>>> {
let (s, (atoms, section)) = text_data_with_ending_stuff(s)?;
if section.0.is_some() || section.1.is_some() {
return Err(Err::Error(VerboseError {
errors: vec![(s, VerboseErrorKind::Context("excess section"))],
}));
}
Ok((s, atoms))
}
fn section_name(s: &str) -> Res<&str, &str> {
context(
"section_name",
map(tuple((tag("::"), space1, ident)), |t| t.2),
)(s)
}
#[allow(clippy::type_complexity)]
fn character_says(s: &str) -> Res<&str, (&str, Vec<AtomOr<&str, &str>>)> {
context(
"character_says",
map(
tuple((is_not("~#:\n\r"), char(':'), space0, text_data)),
|(character, _, _, text)| (character, text),
),
)(s)
}
#[allow(clippy::type_complexity)]
fn question(s: &str) -> Res<&str, (Vec<AtomOr<&str, &str>>, (Option<&str>, Option<&str>))> {
context(
"question",
map(
tuple((tag("-"), space0, text_data_with_ending_stuff)),
|t| t.2,
),
)(s)
}
fn do_action_line(s: &str) -> Res<&str, &str> {
context(
"do_action_line",
map(
tuple((tag("do"), space1, not_line_ending)),
|(_, _, action)| action,
),
)(s)
}
fn if_line(s: &str) -> Res<&str, &str> {
context(
"if_line",
map(
tuple((tag("if"), space1, not_line_ending)),
|(_, _, check)| check,
),
)(s)
}
fn elif_line(s: &str) -> Res<&str, &str> {
context(
"elif_line",
map(
tuple((tag("elif"), space1, not_line_ending)),
|(_, _, check)| check,
),
)(s)
}
fn else_line(s: &str) -> Res<&str, ()> {
context("else", map(tag("else"), |_| ()))(s)
}
fn not_blank(s: &str) -> Res<&str, Data<&str, &str, &str>> {
map(
tuple((
alt((
map(section_name, Data::SectionStart),
map(question, |(text, (only_if, go_to))| Data::Response {
text,
only_if,
go_to,
}),
map(character_says, |(character, text)| Data::CharacterSays {
character,
text,
}),
map(go_to, Data::GoTo),
map(do_action_line, Data::DoAction),
map(if_line, Data::If),
map(elif_line, Data::Elif),
map(else_line, |_| Data::Else),
map(text_data, Data::JustText),
)),
space0,
alt((line_ending, eof)),
)),
|(data, _, _)| data,
)(s)
}
fn blank(s: &str) -> Res<&str, Data<&str, &str, &str>> {
map(
alt((tuple((space0, line_ending)), tuple((space1, eof)))),
|_| Data::Blank,
)(s)
}
fn data(s: &str) -> Res<&str, Data<&str, &str, &str>> {
alt((not_blank, blank))(s)
}
pub(crate) fn line(s: &str) -> Result<(&str, Line<&str, &str, &str>), LexError> {
if s.is_empty() {
return Err(LexError::Eof);
}
map(tuple((indent, data)), |(indent, data)| Line {
indent,
data,
})(s)
.map_err(|_| LexError::Fail)
}
#[cfg(test)]
mod tests {
use super::*;
use nom::{
error::{ErrorKind, VerboseErrorKind},
Err,
};
use pretty_assertions::assert_eq;
#[test]
fn test_wave() {
assert_eq!(
wave(""),
Err(Err::Error(VerboseError {
errors: vec![
("", VerboseErrorKind::Nom(ErrorKind::Tag)),
("", VerboseErrorKind::Context("wave"))
]
}))
);
assert_eq!(
wave("no wave"),
Err(Err::Error(VerboseError {
errors: vec![
("no wave", VerboseErrorKind::Nom(ErrorKind::Tag)),
("no wave", VerboseErrorKind::Context("wave"))
]
}))
);
assert_eq!(
wave("~mismatch"),
Err(Err::Error(VerboseError {
errors: vec![
("mismatch", VerboseErrorKind::Nom(ErrorKind::TakeUntil)),
("~mismatch", VerboseErrorKind::Context("wave"))
]
}))
);
assert_eq!(wave("~wave~"), Ok(("", "wave")));
assert_eq!(wave("~wave~extra"), Ok(("extra", "wave")));
}
#[test]
fn test_shake() {
assert_eq!(
shake(""),
Err(Err::Error(VerboseError {
errors: vec![
("", VerboseErrorKind::Nom(ErrorKind::Tag)),
("", VerboseErrorKind::Context("shake"))
]
}))
);
assert_eq!(
shake("no shake"),
Err(Err::Error(VerboseError {
errors: vec![
("no shake", VerboseErrorKind::Nom(ErrorKind::Tag)),
("no shake", VerboseErrorKind::Context("shake"))
]
}))
);
assert_eq!(
shake("#mismatch"),
Err(Err::Error(VerboseError {
errors: vec![
("mismatch", VerboseErrorKind::Nom(ErrorKind::TakeUntil)),
("#mismatch", VerboseErrorKind::Context("shake"))
]
}))
);
assert_eq!(shake("#shake#"), Ok(("", "shake")));
assert_eq!(shake("#shake#extra"), Ok(("extra", "shake")));
}
#[test]
fn test_interpolate() {
assert_eq!(interpolate("`terp`"), Ok(("", "terp")));
}
#[test]
fn test_indent() {
assert_eq!(indent(""), Ok(("", 0)));
assert_eq!(indent("text"), Ok(("text", 0)));
assert_eq!(indent(" indented"), Ok(("indented", 2)));
}
#[test]
fn test_directive() {
assert_eq!(
directive(""),
Err(Err::Error(VerboseError {
errors: vec![
("", VerboseErrorKind::Nom(ErrorKind::Tag)),
("", VerboseErrorKind::Context("sparkle")),
("", VerboseErrorKind::Nom(ErrorKind::Alt)),
("", VerboseErrorKind::Context("directive"))
]
}))
);
assert_eq!(
directive("no directive"),
Err(Err::Error(VerboseError {
errors: vec![
("no directive", VerboseErrorKind::Nom(ErrorKind::Tag)),
("no directive", VerboseErrorKind::Context("sparkle")),
("no directive", VerboseErrorKind::Nom(ErrorKind::Alt)),
("no directive", VerboseErrorKind::Context("directive"))
]
}))
);
assert_eq!(
directive("*~wave~*"),
Ok((
"",
vec![
AtomOr::Atom(Atom::StartItalic),
AtomOr::Atom(Atom::Wave("wave")),
AtomOr::Atom(Atom::EndItalic)
]
))
);
assert_eq!(
directive("**#shake#**"),
Ok((
"",
vec![
AtomOr::Atom(Atom::StartBold),
AtomOr::Atom(Atom::Shake("shake")),
AtomOr::Atom(Atom::EndBold),
]
))
);
assert_eq!(
directive("&&sparkle&&"),
Ok(("", vec![AtomOr::Atom(Atom::Sparkle("sparkle"))]))
);
assert_eq!(
directive("`terp`"),
Ok(("", vec![AtomOr::Interpolate("terp")]))
);
}
#[test]
fn test_text_data() {
assert_eq!(
text_data(""),
Err(Err::Error(VerboseError {
errors: vec![("", VerboseErrorKind::Context("text_data"))],
}))
);
assert_eq!(
text_data("just normal text"),
Ok(("", vec![AtomOr::Atom(Atom::Text("just normal text"))]))
);
assert_eq!(
text_data("text ~wave~ more **bold** text #shake# `terp` not terp"),
Ok((
"",
vec![
AtomOr::Atom(Atom::Text("text ")),
AtomOr::Atom(Atom::Wave("wave")),
AtomOr::Atom(Atom::Text(" more ")),
AtomOr::Atom(Atom::StartBold),
AtomOr::Atom(Atom::Text("bold")),
AtomOr::Atom(Atom::EndBold),
AtomOr::Atom(Atom::Text(" text ")),
AtomOr::Atom(Atom::Shake("shake")),
AtomOr::Atom(Atom::Text(" ")),
AtomOr::Interpolate("terp"),
AtomOr::Atom(Atom::Text(" not terp")),
]
))
);
}
#[test]
fn test_section_name() {
assert_eq!(
section_name("not section name"),
Err(Err::Error(VerboseError {
errors: vec![
("not section name", VerboseErrorKind::Nom(ErrorKind::Tag)),
(
"not section name",
VerboseErrorKind::Context("section_name")
)
]
}))
);
assert_eq!(section_name(":: section_name"), Ok(("", "section_name")));
assert_eq!(section_name(":: invalid name"), Ok((" name", "invalid")));
assert_eq!(
section_name(":: section_name_long\n"),
Ok(("\n", "section_name_long"))
)
}
#[test]
fn test_character_says() {
assert_eq!(
character_says(""),
Err(Err::Error(VerboseError {
errors: vec![
("", VerboseErrorKind::Nom(ErrorKind::IsNot)),
("", VerboseErrorKind::Context("character_says"))
]
}))
);
assert_eq!(
character_says("Name Alone:\nTest"),
Err(Err::Error(VerboseError {
errors: vec![
("\nTest", VerboseErrorKind::Context("text_data")),
(
"Name Alone:\nTest",
VerboseErrorKind::Context("character_says")
)
]
}))
);
assert_eq!(
character_says("Name: Text\nRest"),
Ok(("\nRest", ("Name", vec![AtomOr::Atom(Atom::Text("Text"))])))
);
}
#[test]
fn test_question() {
assert_eq!(
question(""),
Err(Err::Error(VerboseError {
errors: vec![
("", VerboseErrorKind::Nom(ErrorKind::Tag)),
("", VerboseErrorKind::Context("question"))
]
}))
);
assert_eq!(
question("- hi"),
Ok(("", (vec![AtomOr::Atom(Atom::Text("hi"))], (None, None))))
);
assert_eq!(
question("- hi\n"),
Ok(("\n", (vec![AtomOr::Atom(Atom::Text("hi"))], (None, None))))
);
assert_eq!(
question("- hi => redirect"),
Ok((
"",
(
vec![AtomOr::Atom(Atom::Text("hi"))],
(None, Some("redirect"))
)
))
);
assert_eq!(
question("- hi [if guard]"),
Ok((
"",
(vec![AtomOr::Atom(Atom::Text("hi"))], (Some("guard"), None))
))
);
assert_eq!(
question("- hi [if guard] => redirect"),
Ok((
"",
(
vec![AtomOr::Atom(Atom::Text("hi"))],
(Some("guard"), Some("redirect"))
)
))
);
assert_eq!(
question("Text: Not Question"),
Err(Err::Error(VerboseError {
errors: vec![
("Text: Not Question", VerboseErrorKind::Nom(ErrorKind::Tag)),
("Text: Not Question", VerboseErrorKind::Context("question"))
]
}))
);
}
#[test]
fn test_dialog_line() {
assert_eq!(line(""), Err(LexError::Eof));
assert_eq!(
line("Plain\nextra\n"),
Ok((
"extra\n",
Line {
indent: 0,
data: Data::JustText(vec![AtomOr::Atom(Atom::Text("Plain"))])
}
))
);
assert_eq!(
line(":: section_name"),
Ok((
"",
Line {
indent: 0,
data: Data::SectionStart("section_name")
}
))
);
assert_eq!(
line(" Name: Contents"),
Ok((
"",
Line {
indent: 2,
data: Data::CharacterSays {
character: "Name",
text: vec![AtomOr::Atom(Atom::Text("Contents"))]
}
}
))
);
assert_eq!(
line(" - Question"),
Ok((
"",
Line {
indent: 1,
data: Data::Response {
text: vec![AtomOr::Atom(Atom::Text("Question"))],
only_if: None,
go_to: None
}
}
))
);
assert_eq!(
line(" - Question => go_to"),
Ok((
"",
Line {
indent: 1,
data: Data::Response {
text: vec![AtomOr::Atom(Atom::Text("Question"))],
only_if: None,
go_to: Some("go_to")
}
}
))
);
}
}