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}