1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
|
//! template processing
use std::str::FromStr;
use bempline::{Document, Options, variables};
use paige::Frontmatter;
use timeparse::OffsetDateTime;
mod features;
use crate::{
config::Settings,
fs::{RequestPath, SafePath},
state::SplormState,
templated::features::Feature,
};
/// variables extracted from the [`Frontmatter`]
///
/// Contains all known-variables taken from the [`Frontmatter`] as well as
/// methods to apply those variables to the template.
///
/// Process a file with [`Self::build()`] and then use the `apply_` methods
/// to fill in the template
///
/// See the `TEMPLATES.md` file in the repository root to see what these
/// variables mean
pub struct TemplateVariables<'fm> {
template: Option<&'fm str>,
markup: Option<MarkupStyle>,
features: Vec<Feature>,
style: Vec<&'fm str>,
title: &'fm str,
description: Option<&'fm str>,
published: Option<OffsetDateTime>,
skip_path: Option<usize>,
}
impl<'fm> TemplateVariables<'fm> {
/// Grabs variables from the [`Frontmatter`], filling in defaults
/// if a variable is required and missing, or if it fails to parse.
pub fn build(safe: SafePath<'fm>, fm: &'fm Frontmatter) -> Self {
let filename = safe.file_name().unwrap_or("unknown");
Self {
template: fm.get("template"),
markup: fm
.get("markup")
.map(|s| MarkupStyle::from_str(s).ok())
.flatten(),
features: fm
.get_many("features")
.into_iter()
.filter_map(|s| Feature::from_str(s).ok())
.collect(),
title: fm.get("title").unwrap_or_else(|| filename),
style: fm.get_many("style"),
description: fm.get("description"),
published: fm
.get("published")
.map(|raw| timeparse::parse(raw).ok())
.flatten(),
skip_path: fm.get("skip-path").map(|s| s.parse().ok()).flatten(),
}
}
pub fn template(&self) -> Option<&str> {
self.template
}
pub fn features(&self) -> &[Feature] {
&self.features
}
/// Apply `style` varaibles
pub fn apply_styles(&self, doc: &mut Document) {
if let Some(styles) = doc.get_pattern("styles") {
self.style.iter().for_each(|style| {
let mut cloned = styles.clone();
cloned.set("style", style);
doc.set_pattern(cloned);
});
}
}
/// Apply the page's `title`
pub fn apply_title(&self, doc: &mut Document) {
doc.set("title", self.title);
}
/// Use the `markup` variable to determine how to process the content of
/// the template
pub fn apply_markup(&self, doc: &mut Document, content: &str) -> String {
match self.markup {
None | Some(MarkupStyle::Legacy) => {
let markedup = markup::legacy::process(&content);
markedup.fill_document(doc);
markedup.into_content()
}
Some(MarkupStyle::Breakdown) => unimplemented!(),
}
}
/// Apply the `published` variable
pub fn apply_published(&self, doc: &mut Document) {
if let Some(mut published_pattern) = doc.get_pattern("published") {
if let Some(published) = self.published.as_ref() {
let published_human = timeparse::format::human_long(published);
let published_machine = timeparse::format::iso8601(published);
variables!(published_pattern, published_human, published_machine);
doc.set_pattern(published_pattern);
}
}
}
/// Apply path-based navigation
pub fn apply_pathnav(&self, doc: &mut Document, path: &RequestPath) {
let webpath = path.web_path();
let mut components: Vec<&str> = webpath.split('/').filter(|s| !s.is_empty()).collect();
// we don't want the name itself
components.pop();
if let Some(path_pattern) = doc.get_pattern("pathnav") {
let mut home = path_pattern.clone();
home.set("pathnav_link", "/");
home.set("pathnav_name", "home");
doc.set_pattern(home);
let count = components.len().saturating_sub(self.skip_path.unwrap_or(0));
let mut link = String::from("/");
for part in components.iter().take(count) {
link.push_str(&format!("{part}/"));
let mut pat = path_pattern.clone();
pat.set("pathnav_link", &link);
pat.set("pathnav_name", part);
doc.set_pattern(pat);
}
}
}
/// Apply opengraph tag related varaibles
pub fn apply_opengraph(&self, doc: &mut Document, settings: &Settings, path: &RequestPath) {
if let Some(desc) = self.description {
let og_title = self.title;
let og_site_name = settings.hostname();
let og_url = format!("https://{}{}", settings.hostname(), path.web_path());
let og_description = desc;
variables!(doc, og_title, og_url, og_site_name, og_description);
}
}
/// Parse the provided content as a template and apply selected features.
pub async fn apply_features(&self, content: &str, state: &SplormState) -> String {
let mut doc = Document::from_str(&content, Options::default()).unwrap();
for feat in &self.features {
match feat {
Feature::SystemStats => features::apply_system_stats(&mut doc, state).await,
}
}
doc.compile()
}
}
/// markup style a template is using
pub enum MarkupStyle {
Legacy,
Breakdown,
}
impl FromStr for MarkupStyle {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"legacy" => Ok(MarkupStyle::Legacy),
"breakdown" => Ok(MarkupStyle::Breakdown),
_ => Err("invalid markup style"),
}
}
}
|