1#![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
52pub struct Stuart {
54 pub dir: PathBuf,
56 pub input: Option<Node>,
58 pub output: Option<Node>,
60 pub config: Config,
62 pub base: Option<StackFrame>,
64 pub plugins: Option<Box<dyn Manager>>,
66}
67
68#[derive(Copy, Clone, Debug)]
70pub struct Environment<'a> {
71 pub vars: &'a [(String, String)],
73 pub root: Option<&'a [LocatableToken]>,
75 pub md: Option<&'a [LocatableToken]>,
77}
78
79impl Stuart {
80 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 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 pub fn with_config(mut self, config: Config) -> Self {
112 self.config = config;
113 self
114 }
115
116 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 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 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 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 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 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 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 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 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}