dialogue_rs/script/
block.rs

1//! # Blocks
2//!
3//! Blocks are used to organize dialogue. They are indented by 4 spaces and can contain any number of lines or inner blocks. Blocks can be nested to any depth, though you should avoid nesting deeply, as it makes scripts difficult to read. The |CHOICE| and |GOTO| commands show examples of how blocks can be used. When a block is entered, dialogue will continue from the first line of the block. When a block is exited, dialogue will continue from the first line after the block.
4//!
5//! ```text
6//! %START%
7//! |SAY| 1
8//!     |SAY| 2
9//!         |SAY| 3
10//!             |SAY| 4
11//!                 |SAY| 5
12//! %END%
13//! ```
14//!
15//! is equivalent to
16//!
17//! ```text
18//! %START%
19//! |SAY| 1
20//! |SAY| 2
21//! |SAY| 3
22//! |SAY| 4
23//! |SAY| 5
24//! %END%
25//! ```
26//!
27//! Blocks should always be used to organize choices, and commands that result from a choice should be in a block after that choice.
28
29use crate::script::{
30    line::Line,
31    parser::{Parser, Rule},
32    TopLevelElement,
33};
34use anyhow::bail;
35use pest::{iterators::Pair, Parser as PestParser};
36use std::fmt;
37
38/// A block in a script, containing a collection of [top level elements](TopLevelElement).
39#[derive(Debug, PartialEq, Eq, Clone)]
40pub struct Block {
41    inner: Vec<TopLevelElement>,
42}
43
44impl fmt::Display for Block {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        self.fmt_with_indent(f, 1)
47    }
48}
49
50impl Block {
51    /// Create a new [Block] from a [Vec] of [top level elements](TopLevelElement).
52    pub fn new(inner: Vec<TopLevelElement>) -> Self {
53        Self { inner }
54    }
55
56    /// Create a new empty [Block].
57    pub fn empty() -> Self {
58        Self::new(Vec::new())
59    }
60
61    /// Get the [top level elements](TopLevelElement) in this [Block].
62    pub fn elements(&self) -> &[TopLevelElement] {
63        &self.inner
64    }
65
66    /// Parse a [Block] from a string.
67    pub fn parse(block_str: &str) -> Result<Self, anyhow::Error> {
68        let mut pairs = Parser::parse(Rule::Block, block_str)?;
69        let pair = pairs.next().expect("a pair exists");
70        assert_eq!(
71            pairs.next(),
72            None,
73            "parsing a block should only ever return one block"
74        );
75
76        pair.try_into()
77    }
78
79    /// Format this [Block] with the given indentation.
80    pub fn fmt_with_indent(&self, f: &mut fmt::Formatter<'_>, indent: usize) -> fmt::Result {
81        for el in &self.inner {
82            match el {
83                TopLevelElement::Block(block) => block.fmt_with_indent(f, indent + 1)?,
84                TopLevelElement::Line(line) => {
85                    for _ in 0..indent {
86                        write!(f, "    ")?;
87                    }
88                    write!(f, "{line}")?;
89                }
90                TopLevelElement::Comment(comment) => {
91                    for _ in 0..indent {
92                        write!(f, "    ")?;
93                    }
94                    write!(f, "{comment}")?;
95                }
96            }
97        }
98
99        Ok(())
100    }
101}
102
103impl TryFrom<Pair<'_, Rule>> for Block {
104    type Error = anyhow::Error;
105
106    fn try_from(pair: Pair<'_, Rule>) -> Result<Self, Self::Error> {
107        match pair.as_rule() {
108            Rule::Block => {
109                let inner = pair
110                    .into_inner()
111                    .map(|pair| match pair.as_rule() {
112                        Rule::Block => Block::try_from(pair).map(TopLevelElement::Block),
113                        Rule::Line => Line::try_from(pair).map(TopLevelElement::Line),
114                        _ => unreachable!(
115                            "Blocks can't contain anything other than inner blocks or lines"
116                        ),
117                    })
118                    .collect::<Result<Vec<_>, _>>()?;
119
120                Ok(Self { inner })
121            }
122            _ => bail!("Pair is not a block: {:#?}", pair),
123        }
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::Block;
130    use crate::script::{command::Command, line::Line};
131    use pretty_assertions::assert_eq;
132
133    #[test]
134    fn test_single_line_parses_correctly() {
135        let input = "    |CHOICE| do the thing\n";
136        let expected = Block {
137            inner: vec![Line::command(Command::parse("|CHOICE| do the thing").unwrap()).into()],
138        };
139        let actual = Block::parse(input).expect("block is valid");
140        assert_eq!(expected, actual);
141    }
142
143    #[test]
144    fn test_multiple_lines_parse_correctly_1() {
145        let input = "    |CHOICE| do the thing
146    |CHOICE| do the other thing
147    |CHOICE| do the third thing
148";
149        let expected = Block {
150            inner: vec![
151                Line::command(Command::parse("|CHOICE| do the thing").unwrap()).into(),
152                Line::command(Command::parse("|CHOICE| do the other thing").unwrap()).into(),
153                Line::command(Command::parse("|CHOICE| do the third thing").unwrap()).into(),
154            ],
155        };
156        let actual = Block::parse(input).expect("block is valid");
157        assert_eq!(expected, actual);
158    }
159
160    #[test]
161    fn test_multiple_lines_parse_correctly_2() {
162        let input = "    |TEST| A1
163        |TEST| B1
164        |TEST| B2
165        |TEST| B3
166";
167        let expected = Block {
168            inner: vec![
169                Line::command(Command::parse("|TEST| A1").unwrap()).into(),
170                Block {
171                    inner: vec![
172                        Line::command(Command::parse("|TEST| B1").unwrap()).into(),
173                        Line::command(Command::parse("|TEST| B2").unwrap()).into(),
174                        Line::command(Command::parse("|TEST| B3").unwrap()).into(),
175                    ],
176                }
177                .into(),
178            ],
179        };
180        let actual = Block::parse(input).expect("block is valid");
181        assert_eq!(expected, actual);
182    }
183
184    #[test]
185    fn test_nested_blocks_parse_correctly() {
186        let input = "    |SAY| First level
187        |SAY| Second level
188            |SAY| Third level
189";
190        let expected = Block::new(vec![
191            Command::new("SAY", None, Some("First level")).into(),
192            Block::new(vec![
193                Command::new("SAY", None, Some("Second level")).into(),
194                Block::new(vec![Command::new("SAY", None, Some("Third level")).into()]).into(),
195            ])
196            .into(),
197        ]);
198
199        let actual = Block::parse(input).expect("block is valid");
200
201        assert_eq!(expected, actual);
202    }
203
204    #[test]
205    fn test_one_line_round_trip() {
206        let input = "    |CHOICE| do the thing\n";
207        let block = Block::parse(input).expect("block is valid");
208        let output = block.to_string();
209
210        assert_eq!(input, output);
211    }
212
213    #[test]
214    fn test_multiline_round_trip_1() {
215        let input = "    |CHOICE| do the thing
216    |CHOICE| do the other thing
217    |CHOICE| do the third thing
218";
219        let block = Block::parse(input).expect("block is valid");
220        let output = block.to_string();
221
222        assert_eq!(input, output);
223    }
224
225    #[test]
226    fn test_multiline_round_trip_2() {
227        let input = "    |TEST| A1
228        |TEST| B1
229        |TEST| B2
230        |TEST| B3
231";
232        let block = Block::parse(input).unwrap();
233        print!("{block:#?}");
234
235        let output = block.to_string();
236
237        assert_eq!(input, output);
238    }
239
240    #[test]
241    fn test_multiline_round_trip_3() {
242        let input = "    |SAY| First level
243        |SAY| Second level
244            |SAY| Third level
245";
246        let block = Block::parse(input).expect("block is valid");
247        let output = block.to_string();
248
249        assert_eq!(input, output);
250    }
251}