stuart_core/process/
mod.rs

1//! Provides processing functionality.
2
3pub mod iter;
4pub mod stack;
5
6pub use crate::error::ProcessError;
7use crate::error::TracebackError;
8
9use self::iter::TokenIter;
10use self::stack::StackFrame;
11
12use crate::fs::{Node, ParsedContents};
13use crate::parse::{LocatableToken, ParsedMarkdown, Token};
14use crate::{Environment, Error, Stuart};
15
16use humphrey_json::Value;
17use pulldown_cmark::{html, Options, Parser};
18
19/// Represents the scope of a function execution.
20pub struct Scope<'a> {
21    /// The token iterator.
22    ///
23    /// This allows functions to consume more tokens if necessary, as well as peek at their own token.
24    /// For example, the `for` function continues consuming tokens until it reaches `end(for)`.
25    pub tokens: &'a mut TokenIter<'a>,
26
27    /// The call stack.
28    ///
29    /// This allows functions to execute other functions, and to control the scope of their variables.
30    /// For example, the `for` function's iteration variable is dropped when the function exits.
31    pub stack: &'a mut Vec<StackFrame>,
32
33    /// The Stuart instance that is processing the build.
34    pub processor: &'a Stuart,
35
36    /// The sections of the file.
37    ///
38    /// These are started with `begin("section name")` and ended with `end("section name")`.
39    /// This should not be manipulated by custom functions.
40    pub sections: &'a mut Vec<(String, Vec<u8>)>,
41}
42
43/// The output of the processing stage.
44#[derive(Default)]
45pub struct ProcessOutput {
46    /// The new contents of the file, if they are to be changed.
47    pub new_contents: Option<Vec<u8>>,
48    /// The new name of the file, if it is to be changed.
49    pub new_name: Option<String>,
50}
51
52impl Node {
53    /// Processes a node, returning an output node.
54    pub fn process(&self, processor: &Stuart, env: Environment) -> Result<Node, Error> {
55        let output = if self.name() != "root.html" && self.name() != "md.html" {
56            match self.parsed_contents() {
57                ParsedContents::Html(tokens) => self
58                    .process_html(tokens, processor, env)
59                    .map_err(Error::Process)?,
60                ParsedContents::Markdown(md) => self
61                    .process_markdown(md, processor, env)
62                    .map_err(Error::Process)?,
63                ParsedContents::Custom(custom) => {
64                    custom.process(processor, env).map_err(Error::Plugin)?
65                }
66                _ => ProcessOutput::default(),
67            }
68        } else {
69            ProcessOutput::default()
70        };
71
72        Ok(Node::File {
73            name: output.new_name.unwrap_or_else(|| self.name().to_string()),
74            contents: output
75                .new_contents
76                .unwrap_or_else(|| self.contents().unwrap().to_vec()),
77            parsed_contents: ParsedContents::None,
78            metadata: if processor.config.save_metadata {
79                self.parsed_contents().to_json()
80            } else {
81                None
82            },
83            source: self.source().to_path_buf(),
84        })
85    }
86
87    /// Processes an HTML node, returning the processed output.
88    fn process_html(
89        &self,
90        tokens: &[LocatableToken],
91        processor: &Stuart,
92        env: Environment,
93    ) -> Result<ProcessOutput, TracebackError<ProcessError>> {
94        let root = env.root.ok_or(TracebackError {
95            path: self.source().to_path_buf(),
96            line: 0,
97            column: 0,
98            kind: ProcessError::MissingHtmlRoot,
99        })?;
100
101        let mut token_iter = TokenIter::new(tokens);
102        let mut stack: Vec<StackFrame> = vec![processor.base.as_ref().unwrap().clone()];
103        let mut sections: Vec<(String, Vec<u8>)> = Vec::new();
104        let mut scope = Scope {
105            tokens: &mut token_iter,
106            stack: &mut stack,
107            processor,
108            sections: &mut sections,
109        };
110
111        while let Some(token) = scope.tokens.next() {
112            token.process(&mut scope)?;
113        }
114
115        if !scope
116            .stack
117            .pop()
118            .map(|frame| frame.name == "base")
119            .unwrap_or(false)
120        {
121            return Err(TracebackError {
122                path: self.source().to_path_buf(),
123                line: 0,
124                column: 0,
125                kind: ProcessError::StackError,
126            });
127        }
128
129        let mut token_iter = TokenIter::new(root);
130
131        scope.stack.push(processor.base.as_ref().unwrap().clone());
132        scope.tokens = &mut token_iter;
133
134        while let Some(token) = scope.tokens.next() {
135            token.process(&mut scope)?;
136        }
137
138        Ok(ProcessOutput {
139            new_contents: Some(stack.pop().unwrap().output),
140            new_name: None,
141        })
142    }
143
144    /// Processes a markdown node, returning the processed output.
145    fn process_markdown(
146        &self,
147        md: &ParsedMarkdown,
148        processor: &Stuart,
149        env: Environment,
150    ) -> Result<ProcessOutput, TracebackError<ProcessError>> {
151        let root = env.root.ok_or(TracebackError {
152            path: self.source().to_path_buf(),
153            line: 0,
154            column: 0,
155            kind: ProcessError::MissingHtmlRoot,
156        })?;
157
158        let md_tokens = env.md.ok_or(TracebackError {
159            path: self.source().to_path_buf(),
160            line: 0,
161            column: 0,
162            kind: ProcessError::MissingMarkdownRoot,
163        })?;
164
165        let mut token_iter = TokenIter::new(md_tokens);
166
167        let mut stack: Vec<StackFrame> = vec![processor
168            .base
169            .as_ref()
170            .unwrap()
171            .clone()
172            .with_variable("self", md.to_value())];
173
174        let mut sections: Vec<(String, Vec<u8>)> = Vec::new();
175        let mut scope = Scope {
176            tokens: &mut token_iter,
177            stack: &mut stack,
178            processor,
179            sections: &mut sections,
180        };
181
182        while let Some(token) = scope.tokens.next() {
183            token.process(&mut scope)?;
184        }
185
186        if !scope
187            .stack
188            .pop()
189            .map(|frame| frame.name == "base")
190            .unwrap_or(false)
191        {
192            return Err(TracebackError {
193                path: self.source().to_path_buf(),
194                line: 0,
195                column: 0,
196                kind: ProcessError::StackError,
197            });
198        }
199
200        let mut token_iter = TokenIter::new(root);
201
202        scope.stack.push(processor.base.as_ref().unwrap().clone());
203        scope.tokens = &mut token_iter;
204
205        while let Some(token) = scope.tokens.next() {
206            token.process(&mut scope)?;
207        }
208
209        let new_name = format!("{}.html", self.name().strip_suffix(".md").unwrap());
210
211        Ok(ProcessOutput {
212            new_contents: Some(stack.pop().unwrap().output),
213            new_name: Some(new_name),
214        })
215    }
216
217    /// Preprocess the markdown node, executing functions within the raw markdown and
218    /// converting it to HTML. The implementation of this is currently quite dodgy but
219    /// it works for the time being.
220    pub(crate) fn preprocess_markdown(
221        &mut self,
222        processor: &Stuart,
223    ) -> Result<(), TracebackError<ProcessError>> {
224        let source = self.source().to_path_buf();
225
226        let md = match self.parsed_contents_mut() {
227            ParsedContents::Markdown(md) => md,
228            _ => return Ok(()),
229        };
230
231        let mut token_iter = TokenIter::new(&md.markdown);
232        let mut stack: Vec<StackFrame> = vec![processor.base.as_ref().unwrap().clone()];
233        let mut sections: Vec<(String, Vec<u8>)> = Vec::new();
234        let mut scope = Scope {
235            tokens: &mut token_iter,
236            stack: &mut stack,
237            processor,
238            sections: &mut sections,
239        };
240
241        while let Some(token) = scope.tokens.next() {
242            token.process(&mut scope)?;
243        }
244
245        if let Some(frame) = scope.stack.pop() {
246            if frame.name == "base" {
247                let processed_markdown =
248                    String::from_utf8(frame.output).map_err(|_| TracebackError {
249                        path: source.clone(),
250                        line: 0,
251                        column: 0,
252                        kind: ProcessError::StackError,
253                    })?;
254
255                let parser = Parser::new_ext(&processed_markdown, Options::all());
256                let mut processed_html = String::new();
257                html::push_html(&mut processed_html, parser);
258
259                md.html = Some(processed_html);
260                return Ok(());
261            }
262        }
263
264        Err(TracebackError {
265            path: self.source().to_path_buf(),
266            line: 0,
267            column: 0,
268            kind: ProcessError::StackError,
269        })
270    }
271}
272
273impl LocatableToken {
274    /// Processes a token, updating the scope.
275    pub fn process(&self, scope: &mut Scope) -> Result<(), TracebackError<ProcessError>> {
276        match &self.inner {
277            Token::Raw(raw) => scope
278                .stack
279                .last_mut()
280                .unwrap()
281                .output
282                .extend_from_slice(raw.as_bytes()),
283
284            Token::Function(function) => function.execute(scope)?,
285
286            Token::Variable(variable) => {
287                let mut variable_iter = variable.split('.');
288                let variable_name = variable_iter.next().unwrap();
289                let variable_indexes = variable_iter.collect::<Vec<_>>();
290
291                let mut string = None;
292
293                for frame in scope.stack.iter().rev() {
294                    if let Some(value) = frame
295                        .get_variable(variable_name)
296                        .map(|v| stack::get_value(&variable_indexes, v))
297                    {
298                        let e = |found: &str| {
299                            Err(ProcessError::InvalidDataType {
300                                variable: variable.to_string(),
301                                expected: "string".to_string(),
302                                found: found.to_string(),
303                            })
304                        };
305
306                        match value {
307                            Value::String(s) => {
308                                string = Some(s);
309                                break;
310                            }
311
312                            Value::Null => Err(ProcessError::NullError(variable.to_string())),
313                            Value::Bool(_) => e("bool"),
314                            Value::Number(_) => e("number"),
315                            Value::Array(_) => e("array"),
316                            Value::Object(_) => e("object"),
317                        }
318                        .map_err(|e| self.traceback(e))?;
319                    }
320                }
321
322                if let Some(s) = string {
323                    scope
324                        .stack
325                        .last_mut()
326                        .unwrap()
327                        .output
328                        .extend_from_slice(s.as_bytes());
329                } else {
330                    return Err(
331                        self.traceback(ProcessError::UndefinedVariable(variable.to_string()))
332                    );
333                }
334            }
335        }
336
337        Ok(())
338    }
339}
340
341impl Scope<'_> {
342    /// Gets a variable from the scope by looking down the stack.
343    pub fn get_variable(&self, name: &str) -> Option<Value> {
344        let mut variable_iter = name.split('.');
345        let variable_name = variable_iter.next().unwrap();
346        let variable_indexes = variable_iter.collect::<Vec<_>>();
347
348        let mut variable = None;
349
350        for frame in self.stack.iter().rev() {
351            if let Some(value) = frame
352                .get_variable(variable_name)
353                .map(|v| crate::process::stack::get_value(&variable_indexes, v))
354            {
355                variable = Some(value);
356                break;
357            }
358        }
359
360        variable
361    }
362
363    /// Adds to the output of the current stack frame.
364    pub fn output(&mut self, output: impl AsRef<[u8]>) -> Result<(), ProcessError> {
365        self.stack
366            .last_mut()
367            .ok_or(ProcessError::StackError)?
368            .output
369            .extend_from_slice(output.as_ref());
370
371        Ok(())
372    }
373}