dialogical/parser/
mod.rs

1//!
2//! Stuff for parsing dg files
3//!
4
5pub type Result<T> = std::result::Result<T, ParseError>;
6
7use std::path::PathBuf;
8
9use crate::comptime::{Script, ScriptPath};
10use crate::consts::{COMPTIME_BORDER, SEPARATOR};
11use crate::pages::{ChoicesState, Interaction, Page, ParseState};
12use crate::InteractionMap;
13
14mod context;
15mod endings;
16mod metaline;
17
18pub use crate::pages::ParseError;
19pub use context::ScriptContext;
20pub use endings::{DialogueChoice, DialogueEnding, Label};
21
22pub struct DgParser {
23    state: ParseState,
24    context: ScriptContext,
25
26    /// Entry file path for resolving imports
27    path: PathBuf,
28
29    /// the end result it's putting together
30    interactions: InteractionMap,
31
32    // temp buffers for parsing
33    // TODO store these inside `ParseState`
34    interaction: Option<Interaction>,
35    ix_id: Option<String>,
36    comptime_script: Vec<String>,
37    page: Page,
38    pagebuf: Vec<String>,
39    page_had_ending: bool,
40}
41
42impl DgParser {
43    pub fn new(path: PathBuf) -> Self {
44        Self {
45            state: ParseState::default(),
46            context: ScriptContext::default(),
47            path,
48
49            interactions: InteractionMap::new(),
50            interaction: None,
51            ix_id: None,
52            page: Page::default(),
53            pagebuf: vec![],
54            comptime_script: vec![],
55            page_had_ending: false,
56        }
57    }
58
59    fn set_ix_id(&mut self, id: &str) -> Result<()> {
60        if self.interaction.is_some() {
61            self.push_ix()?;
62        }
63
64        self.ix_id = Some(id.to_owned());
65        self.interaction = Some(Interaction::default());
66
67        Ok(())
68    }
69
70    fn parse_comptime(&mut self, line: &str) -> Result<()> {
71        // if current line is the closing `---`
72        if line == SEPARATOR && self.comptime_script.last() == Some(&COMPTIME_BORDER.to_owned()) {
73            self.comptime_script.pop();
74
75            let content = self.comptime_script.join("\n");
76            let path = ScriptPath(self.path.clone());
77            let mut script = Script::new(content, path);
78            script.execute(&mut self.context)?;
79
80            // TODO no `self.script`, make the enum variant
81            // store the script built up so far
82            self.comptime_script.clear();
83
84            self.state = match &self.state {
85                ParseState::ComptimeScript(state) => *state.clone(),
86                _ => unreachable!(),
87            };
88        } else {
89            self.comptime_script.push(line.to_owned());
90        }
91
92        Ok(())
93    }
94
95    fn parse_message(&mut self, line: &str) -> Result<()> {
96        // if parsing a message, add it to the result
97        // OR stop parsing if empty line
98        if line.is_empty() {
99            self.state = ParseState::Choices(ChoicesState::Choices);
100        } else {
101            self.pagebuf.push(line.to_string());
102        }
103
104        Ok(())
105    }
106
107    /// push page buffer to the pages vec, then clear the buffer
108    fn push_page(&mut self) -> Result<()> {
109        // join each line with spaces unless they end in the
110        // literal 2 characters "\n", in which case we replace
111        // the \n with an actual newline
112        self.page.content = {
113            let mut it = self.pagebuf.iter().peekable();
114
115            let mut res = String::new();
116            while let Some(line) = it.next() {
117                let to_push = if line.ends_with("\\n") {
118                    line.replace("\\n", "\n")
119                } else if it.peek().is_some() {
120                    format!("{} ", line)
121                } else {
122                    line.clone()
123                };
124
125                res.push_str(&to_push);
126            }
127
128            res
129        };
130
131        let ix = self.interaction.as_mut().ok_or(ParseError::PushPageNoIX)?;
132
133        // you may not add another page after an ending
134        // for more info, see <https://github.com/Lamby777/dialogical/issues/3>
135        let ix_has_ending_yet = ix.ending != DialogueEnding::End;
136        if ix_has_ending_yet {
137            if self.page_had_ending {
138                return Err(ParseError::PageAfterEnding);
139            }
140
141            // "poisons" the current interaction so it remembers
142            // that it had an ending until it's pushed
143            self.page_had_ending = true;
144        }
145
146        ix.pages.push(self.page.clone());
147        self.pagebuf.clear();
148        self.page = Page::default();
149
150        Ok(())
151    }
152
153    /// Finish parsing 1 interaction, and clear the state
154    /// to prepare for another one.
155    ///
156    /// `Err` if the parser is in a state where it's not
157    /// prepared to finish just yet.
158    fn push_ix(&mut self) -> Result<()> {
159        let ix_id = self.ix_id.take();
160        let ix = self.interaction.take();
161        let comptime_imports = self.context.drain_interactions();
162
163        if let (Some(ix_id), Some(ix)) = (ix_id, ix) {
164            if self.interactions.contains_key(&ix_id) {
165                return Err(ParseError::PushDuplicateIX);
166            }
167
168            self.interactions.insert(ix_id, ix);
169        } else if comptime_imports.is_empty() {
170            // empty ix are not allowed... UNLESS there are
171            // imports in a comptime script inside it
172            return Err(ParseError::PushEmptyIX);
173        }
174
175        // push any interactions imported from comptime scripts
176        self.interactions.extend(comptime_imports);
177
178        self.page_had_ending = false;
179
180        Ok(())
181    }
182
183    pub fn parse_all(&mut self, data: &str) -> Result<InteractionMap> {
184        let lines = data.lines();
185
186        self.pagebuf.clear();
187        self.page = Page::default();
188
189        for line in lines {
190            use ParseState::*;
191
192            let line = line.trim();
193
194            match self.state {
195                // besides the start, a block can either be
196                // a comptime script or a message section
197                ComptimeScript(_) => self.parse_comptime(line)?,
198
199                Metadata => metaline::parse(self, line)?,
200                Message => self.parse_message(line)?,
201                Choices(ChoicesState::Choices) => endings::parse_choice(self, line)?,
202            };
203        }
204
205        self.push_ix()?;
206        let res = self.interactions.clone();
207        self.interactions.clear();
208        Ok(res)
209    }
210}
211
212#[cfg(test)]
213mod tests;