stuart_core/
lib.rs

1//! Stuart: A Blazingly-Fast Static Site Generator.
2
3#![warn(missing_docs)]
4#![warn(clippy::missing_docs_in_private_items)]
5
6pub mod config;
7pub mod error;
8pub mod fs;
9pub mod parse;
10pub mod plugins;
11pub mod process;
12
13#[macro_use]
14pub mod functions;
15
16#[cfg(test)]
17mod tests;
18
19pub use config::Config;
20pub use error::{Error, TracebackError};
21pub use fs::Node;
22
23use crate::fs::ParsedContents;
24use crate::parse::LocatableToken;
25use crate::plugins::Manager;
26use crate::process::stack::StackFrame;
27
28use humphrey_json::{prelude::*, Value};
29
30use std::fmt::Debug;
31use std::path::{Path, PathBuf};
32
33define_functions![
34    functions::parsers::Begin,
35    functions::parsers::DateFormat,
36    functions::parsers::Else,
37    functions::parsers::End,
38    functions::parsers::Excerpt,
39    functions::parsers::For,
40    functions::parsers::IfDefined,
41    functions::parsers::Import,
42    functions::parsers::Insert,
43    functions::parsers::TimeToRead,
44    functions::parsers::IfEq,
45    functions::parsers::IfNe,
46    functions::parsers::IfGt,
47    functions::parsers::IfGe,
48    functions::parsers::IfLt,
49    functions::parsers::IfLe,
50];
51
52/// The project builder.
53pub struct Stuart {
54    /// The input directory.
55    pub dir: PathBuf,
56    /// The input virtual filesystem tree.
57    pub input: Option<Node>,
58    /// The output virtual filesystem tree.
59    pub output: Option<Node>,
60    /// The configuration of the project.
61    pub config: Config,
62    /// The base stack frame for each node.
63    pub base: Option<StackFrame>,
64    /// The plugins to be used by Stuart.
65    pub plugins: Option<Box<dyn Manager>>,
66}
67
68/// The environment of the build.
69#[derive(Copy, Clone, Debug)]
70pub struct Environment<'a> {
71    /// The environment variables.
72    pub vars: &'a [(String, String)],
73    /// The root HTML file.
74    pub root: Option<&'a [LocatableToken]>,
75    /// The root markdown HTML file.
76    pub md: Option<&'a [LocatableToken]>,
77}
78
79impl Stuart {
80    /// Creates a new builder from an input directory.
81    pub fn new(dir: impl AsRef<Path>) -> Self {
82        Self {
83            dir: dir.as_ref().to_path_buf(),
84            input: None,
85            output: None,
86            config: Config::default(),
87            base: None,
88            plugins: None,
89        }
90    }
91
92    /// Creates a new builder from a virtual filesystem tree. (for tests)
93    pub fn new_from_node(mut node: Node) -> Self {
94        let mut stuart = Self {
95            dir: node.source().to_path_buf(),
96            input: Some(node.clone()),
97            output: None,
98            config: Config::default(),
99            base: Some(StackFrame::new("base")),
100            plugins: None,
101        };
102
103        stuart.preprocess_markdown_node(&mut node).unwrap();
104
105        stuart.input = Some(node);
106
107        stuart
108    }
109
110    /// Sets the configuration to use.
111    pub fn with_config(mut self, config: Config) -> Self {
112        self.config = config;
113        self
114    }
115
116    /// Sets the plugin manager to use.
117    pub fn with_plugins<T>(mut self, plugins: T) -> Self
118    where
119        T: Manager + 'static,
120    {
121        self.plugins = Some(Box::new(plugins));
122        self
123    }
124
125    /// Attempts to build the project.
126    pub fn build(&mut self, stuart_env: String) -> Result<(), Error> {
127        let mut input = match self.plugins {
128            Some(ref plugins) => Node::new_with_plugins(&self.dir, true, plugins.as_ref())?,
129            None => Node::new(&self.dir, true)?,
130        };
131
132        // This needs some explaining...
133        // We have to clone the input node here so that we can have an immutable copy in case
134        // something tries to change it during the markdown preprocessing stage.
135        // I hate this as much as you, TODO: come up with a better solution.
136        self.input = Some(input.clone());
137
138        let vars = {
139            let mut env = std::env::vars().collect::<Vec<_>>();
140            env.push(("STUART_ENV".into(), stuart_env));
141            env
142        };
143
144        let base = StackFrame::new("base").with_variable(
145            "env",
146            Value::Object(
147                vars.iter()
148                    .map(|(k, v)| (k.clone(), Value::String(v.clone())))
149                    .collect(),
150            ),
151        );
152
153        self.base = Some(base);
154
155        self.preprocess_markdown_node(&mut input)?;
156        self.input = Some(input);
157
158        let env = Environment {
159            vars: &vars,
160            md: None,
161            root: None,
162        }
163        .update_from_children(self.input.as_ref().unwrap().children().unwrap());
164
165        self.output = Some(self.build_node(self.input.as_ref().unwrap(), env)?);
166
167        Ok(())
168    }
169
170    /// Merges an output node with the built result.
171    ///
172    /// This is used for merging static content with the build output.
173    pub fn merge_output(&mut self, node: Node) -> Result<(), Error> {
174        self.output
175            .as_mut()
176            .ok_or(Error::NotBuilt)
177            .and_then(|out| out.merge(node))
178    }
179
180    /// Saves the build output to a directory.
181    pub fn save(&self, path: impl AsRef<Path>) -> Result<(), Error> {
182        if let Some(out) = &self.output {
183            out.save(&path, &self.config)
184        } else {
185            Err(Error::NotBuilt)
186        }
187    }
188
189    /// Saves the build metadata to a file.
190    pub fn save_metadata(&self, path: impl AsRef<Path>) -> Result<(), Error> {
191        if !self.config.save_metadata {
192            return Err(Error::MetadataNotEnabled);
193        }
194
195        if let Some(out) = &self.output {
196            let base = json!({
197                "name": (self.config.name.clone()),
198                "author": (self.config.author.clone())
199            });
200
201            out.save_metadata(base, &path)
202        } else {
203            Err(Error::NotBuilt)
204        }
205    }
206
207    /// Recursively builds an input node and its descendants, returning an output node.
208    fn build_node(&self, node: &Node, env: Environment) -> Result<Node, Error> {
209        match node {
210            Node::Directory {
211                name,
212                children,
213                source,
214            } => {
215                let env = env.update_from_children(children);
216                let children = children
217                    .iter()
218                    .map(|n| self.build_node(n, env))
219                    .collect::<Result<Vec<_>, Error>>()?;
220
221                Ok(Node::Directory {
222                    name: name.clone(),
223                    children,
224                    source: source.clone(),
225                })
226            }
227            Node::File { .. } => node.process(self, env),
228        }
229    }
230
231    /// Preprocess the given markdown node and its descendants, executing functions
232    /// and adding the result to the node's metadata in place.
233    fn preprocess_markdown_node(&mut self, node: &mut Node) -> Result<(), Error> {
234        match node {
235            Node::Directory { children, .. } => {
236                for child in children.iter_mut() {
237                    self.preprocess_markdown_node(child)?;
238                }
239
240                Ok(())
241            }
242            Node::File {
243                parsed_contents: ParsedContents::Markdown(_),
244                ..
245            } => node.preprocess_markdown(self).map_err(Error::Process),
246            _ => Ok(()),
247        }
248    }
249}
250
251impl<'a> Environment<'a> {
252    /// Updates the environment from a list of children, adding the closest root HTML files.
253    fn update_from_children(&self, children: &'a [Node]) -> Self {
254        let mut env = *self;
255
256        for child in children {
257            match child.name() {
258                "root.html" => {
259                    env.root = match child.parsed_contents() {
260                        ParsedContents::Html(tokens) => Some(tokens),
261                        _ => None,
262                    }
263                }
264                "md.html" => {
265                    env.md = match child.parsed_contents() {
266                        ParsedContents::Html(tokens) => Some(tokens),
267                        _ => None,
268                    }
269                }
270                _ => (),
271            }
272        }
273
274        env
275    }
276}