dialogue_rs/
script.rs

1//! Script parsing and representation. Scripts are a collection of lines and blocks.
2
3pub mod block;
4pub mod command;
5pub mod comment;
6pub mod element;
7pub mod line;
8pub mod marker;
9pub(crate) mod parser;
10
11use self::{block::Block, element::TopLevelElement, line::Line};
12use anyhow::bail;
13use parser::{Parser, Rule};
14use pest::{iterators::Pair, Parser as PestParser};
15use std::fmt;
16
17/// A collection of lines and blocks, acting as a state machine for dialogue.
18#[derive(Debug, Default)]
19pub struct Script(pub Vec<TopLevelElement>);
20
21impl fmt::Display for Script {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        for el in &self.0 {
24            write!(f, "{el}")?;
25        }
26
27        Ok(())
28    }
29}
30
31impl Script {
32    /// Create an empty `Script`.
33    pub fn empty() -> Self {
34        Default::default()
35    }
36
37    /// Parse a `Script` from a string.
38    pub fn parse(script_str: &str) -> Result<Self, anyhow::Error> {
39        let mut pairs = Parser::parse(Rule::Script, script_str)?;
40        let pair = pairs.next().expect("a pair exists");
41        assert_eq!(pairs.next(), None);
42
43        pair.try_into()
44    }
45}
46
47impl TryFrom<Pair<'_, Rule>> for Script {
48    type Error = anyhow::Error;
49
50    fn try_from(pair: Pair<'_, Rule>) -> Result<Self, Self::Error> {
51        match pair.as_rule() {
52            Rule::Script => {
53                let inner = pair
54                    .into_inner()
55                    .map(|pair| match pair.as_rule() {
56                        Rule::Block => Block::parse(pair.as_str()).map(Into::into),
57                        Rule::Line => Line::parse(pair.as_str()).map(Into::into),
58                        _ => unreachable!(
59                        "Scripts can't contain anything other than blocks or lines but found {:?}",
60                        pair.as_rule()
61                    ),
62                    })
63                    .collect::<Result<Vec<_>, _>>()?;
64
65                Ok(Self(inner))
66            }
67            _ => bail!("Pair is not a script: {:#?}", pair),
68        }
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::Script;
75    use pretty_assertions::assert_eq;
76
77    #[test]
78    fn test_complex_is_nesting_parsed_correctly() {
79        let input = "%START%
80|TEST| A
81    |TEST| B
82        |TEST| C
83            |TEST| D
84    |TEST| E
85    |TEST| F
86|TEST| G
87%END%
88";
89        let script = Script::parse(input).expect("a script can be parsed");
90        let actual = script.to_string();
91
92        assert_eq!(input, actual);
93    }
94
95    #[test]
96    fn test_script_to_string_matches_input_script_1() {
97        let input = std::fs::read_to_string("example_scripts/daisy-and-luigi.script")
98            .expect("example script exists");
99
100        let script = Script::parse(&input).expect("a script can be parsed");
101        // Empty lines are swallowed by the parser, so our test scripts don't have any.
102        assert_eq!(input, script.to_string());
103    }
104
105    #[test]
106    fn test_script_to_string_matches_input_script_2() {
107        let input = std::fs::read_to_string("example_scripts/capital-of-spain.script")
108            .expect("example script exists");
109
110        let script = Script::parse(&input).expect("a script can be parsed");
111        // Empty lines are swallowed by the parser, so our test scripts don't have any.
112        assert_eq!(input, script.to_string());
113    }
114
115    #[test]
116    fn test_script_to_string_matches_input_script_3() {
117        let input =
118            std::fs::read_to_string("example_scripts/jimi.script").expect("example script exists");
119
120        let script = Script::parse(&input).expect("a script can be parsed");
121        // Empty lines are swallowed by the parser, so our test scripts don't have any.
122        assert_eq!(input, script.to_string());
123    }
124}