antwerp/lib.rs
1//! ### Antwerp
2//! [Antwerp](https://crates.io/crates/antwerp) is an open-source framework ported from JavaScript to Rust for GitHub pages and built with the Marcus HTML to MarkDown parser.
3//! It outputs static web pages in `dist/` using HTML and MarkDown templates in `public/`, which are converted to HTML using the [Marcus](https://crates.io/crates/marcus) MarkDown to HTML parser.
4//!
5//! References & Getting Started:
6//! - <https://crates.io/crates/antwerp>
7//! - <https://github.com/Malekaia/Antwerp>
8//! - <https://docs.rs/antwerp/latest/antwerp/>
9//! - <https://crates.io/crates/marcus>
10//! - <https://github.com/Malekaia/Marcus/>
11//! - <https://docs.rs/marcus/latest/marcus/>
12//! - <https://developer.mozilla.org/en-US/docs/Web/HTML>
13//! - <https://www.markdownguide.org/>
14
15mod filters;
16mod parser;
17use crate::filters::filter_output;
18use crate::parser::parse_templates;
19use std::collections::HashMap;
20use std::fs::{create_dir_all, write};
21
22/// Type alias representing a `Vector` of `String` for filter lists in block declarations.
23///
24/// For example, the following block:
25///
26/// `{% block ... filter_1 | filter_2 | filter_n %}...{% endblock ... %}`
27///
28/// Would produce a `Vector` equivalent to:
29///
30/// `vec![String::from("filter_1"), String::from("filter_2"), String::from("filter_n")]`
31pub type Filters = Vec<String>;
32
33/// Contains information for blocks derived from block declarations in templates.
34///
35/// Fields:
36/// - `filters`: a `Vector` of `String` for filter lists in block declarations (see [`Filters`])
37/// - `content_outer`: a String containing the outer content of a block (see `content_outer` of [`Block`])
38/// - `content`: a String containing the inner content of a block (see `content` of [`Block`])
39#[derive(Debug)]
40pub struct Block {
41 /// A `Vector` of `String` for filter lists in block declarations (see [Filters])
42 pub filters: Filters,
43 /// A String containing the outer content of a block.
44 ///
45 /// For example, an instance of `Block` derived from the following template:
46 ///
47 /// `{% block test filter_1 | filter_2 | filter_n %}This is a test block{% endblock test %}`
48 ///
49 /// Would have a `content_outer` field equal to:
50 ///
51 /// `String::from("{% block test filter_1 | filter_2 | filter_n %}This is a test block{% endblock test %}")`
52 pub content_outer: String,
53 /// A String containing the inner content of a block.
54 ///
55 /// For example, an instance of `Block` derived from the following template:
56 ///
57 /// `{% block test filter_1 | filter_2 | filter_n %}This is a test block{% endblock test %}`
58 ///
59 /// Would have a `content` field equal to:
60 ///
61 /// `String::from("This is a test block")`
62 pub content: String
63}
64
65/// Type alias representing a `HashMap` where key: `String` and value: [`Block`].
66///
67/// A `HashMap` with this type is used to contain the blocks (organised by name) of a given HTML or MarkDown template.
68///
69/// For example, a template with a single block declaration:
70///
71/// `{% block test filter_1 | filter_2 | filter_n %}This is a test block{% endblock test %}`
72///
73/// Would produce the following `HashMap`:
74///
75/// ```rust
76/// Blocks {
77/// "test": Block {
78/// filters: vec!["filter_1", "filter_2", "filter_n"],
79/// content_outer: "{% block test filter_1 | filter_2 | filter_n %}This is a test block{% endblock test %}",
80/// content: "This is a test block"
81/// }
82/// }
83/// ```
84pub type Blocks = HashMap<String, Block>;
85
86/// Contains template information derived from HTML and MarkDown templates.
87///
88/// Fields:
89/// - `extends`: a `String` containing the file path provided by an extends statement (see `extends` of [`Template`])
90/// - `output`: a `String` containing the file path of the output file
91/// - `output_dir`: a `String` containing the directory of the output file
92/// - `content`: a `String` containing the file content of the given template
93/// - `blocks`: contains the blocks (organised by name) of a given HTML or MarkDown template (see [`Blocks`])
94#[derive(Debug)]
95pub struct Template {
96 /// A `String` containing the file path provided by an extends statement
97 ///
98 /// For example, an instance of `Template` for a file with the following extends statement:
99 ///
100 /// `{% extends "base.html" %}`
101 ///
102 /// Would have an `extends` field with a value equal to:
103 ///
104 /// `String::from("/home/<USERNAME>/<PATH>/<TO>/<PROJECT>/public/base.html")`
105 ///
106 /// **Note**: Absolute paths are obtained using the [`project-root`](https://crates.io/crates/project-root) crate, which finds the root directory of a project, relative to the location of the nearest [`Cargo.lock`](https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html).
107 ///
108 /// **Note**: Paths are handled using [`Path`](https://doc.rust-lang.org/stable/std/path/struct.Path.html) and [`PathBuf`](https://doc.rust-lang.org/std/path/struct.PathBuf.html) and support Unix and Windows paths.
109 pub extends: String,
110 /// A `String` containing the file path of the output file
111 pub output: String,
112 /// A `String` containing the directory of the output file
113 pub output_dir: String,
114 /// A `String` containing the file content of the given template
115 pub content: String,
116 /// Contains the blocks (organised by name) of a given HTML or MarkDown template (see [`Blocks`])
117 pub blocks: Blocks
118}
119
120/// Type alias representing a `HashMap` where key: `String` and value: [`Template`].
121///
122/// A `HashMap` with this type is used to contain the template information (organised by file path) of a given HTML or MarkDown template.
123///
124/// For example, the following Markdown template containing an extends statement and a single block:
125///
126/// ```markdown
127/// {% extends "base.html" %}
128/// {% block title %}Homepage{% endblock title %}
129/// ```
130///
131/// Would produce the a `HashMap`similar to:
132///
133/// ```rust
134/// Templates {
135/// "/home/<USERNAME>/<PATH>/<TO>/<PROJECT>/public/file.md": Template {
136/// extends: "/home/<USERNAME>/<PATH>/<TO>/<PROJECT>/public/base.html",
137/// output: "/home/<USERNAME>/<PATH>/<TO>/<PROJECT>/dist/file.html",
138/// output_dir: "/home/<USERNAME>/<PATH>/<TO>/<PROJECT>/dist/",
139/// content: "{% extends "base.html" %}\n{% block title %}Homepage{% endblock title %}",
140/// blocks: {
141/// title: Block {
142/// filters: vec![],
143/// content_outer: "{% block title %}Homepage{% endblock title %}",
144/// content: "Homepage"
145/// }
146/// }
147/// }
148/// }
149/// ```
150pub type Templates = HashMap<String, Template>;
151
152/// Type aliases for filter methods
153pub (crate) type FilterMethod = fn(String) -> String;
154pub (crate) type FilterMethods = HashMap<String, FilterMethod>;
155
156/// [`Build`](build) Parses HTML and MarkDown templates in `public/` and writes the HTML output to `dist/`
157///
158/// 1. Extracts template data from templates in `public/`
159/// 3. Replaces block declarations in base template with content from child templates
160/// 4. Inserts default block content for undefined blocks in child templates
161/// 2. Uses filters to modify content before inserting into parent template
162/// 5. Creates the output directory and writes the HTML template to the output file
163///
164/// **Note**: For sample templates and input, see [README.md](https://github.com/Malekaia/Antwerp#readme).
165pub fn build() {
166 // Get the parent templates
167 let parent_templates: Templates = parse_templates("public/**/*.html");
168
169 // Define output filter methods for later referencing (prevents multiple defines in loop and use of `lazy_static`)
170 let mut filter_methods: FilterMethods = HashMap::new();
171 // Trim the output
172 filter_methods.insert(String::from("trim"), | output: String | output.trim().to_string());
173 // Parse MarkDown to HTML
174 filter_methods.insert(String::from("html"), | output: String | marcus::to_string(output));
175 // Return the raw text
176 filter_methods.insert(String::from("text"), | output: String | output);
177
178 // Iterate the child templates
179 for (file_path, template) in parse_templates("public/**/*.md") {
180 // Ensure the parent template exists
181 if !parent_templates.contains_key(&template.extends) {
182 panic!("TemplateError: unknown template \"{}\"", &template.extends);
183 }
184
185 // Get the base template
186 let (parent_file_path, parent): (&String, &Template) = parent_templates.get_key_value(&template.extends).unwrap();
187
188 // Create and replace the HTML string
189 let mut html: String = parent.content.to_owned();
190
191 // Iterate the blocks
192 for (name, block) in &template.blocks {
193 // Ensure the block exists in the base template
194 if !parent.blocks.contains_key(name) {
195 panic!("TemplateError: block \"{}\" in \"{}\" not defined in base template (\"{}\")", name, file_path, parent_file_path);
196 }
197 // Replace the block content with the given template
198 let parent_block: &Block = parent.blocks.get(name).unwrap();
199 // Use parent filters by default if no child filters are defined
200 let filters: &Vec<String> = if !block.filters.is_empty() {
201 &block.filters
202 } else {
203 &parent_block.filters
204 };
205 // Replace the HTML with the filtered output
206 html = html.replace(&parent_block.content_outer, &filter_output(&filter_methods, filters, &block.content));
207 }
208
209 // Iterate the parent blocks
210 for (_, block) in &parent.blocks {
211 // Replace unspecified blocks with their default value
212 if html.contains(&block.content_outer) {
213 html = html.replace(&block.content_outer, &filter_output(&filter_methods, &block.filters, &block.content));
214 }
215 }
216
217 // Create the output file's directory
218 assert!(create_dir_all(&template.output_dir).is_ok(), "CreateDirAllError: failed to create directory \"{}\"", &template.output_dir);
219 // Write the HTML to the output file
220 assert!(write(&template.output, &html).is_ok(), "WriteError: write \"html\" to \"{}\"", &template.output);
221 }
222}
223
224/// Test for GitHub Workflows (`.github/workflows/build.yaml`)
225#[cfg(test)]
226mod tests {
227 use crate as antwerp;
228 #[test]
229 fn antwerp() {
230 antwerp::build();
231 }
232}