]> git.r.bdr.sh - rbdr/blog/blame_incremental - src/template.rs
Add CI
[rbdr/blog] / src / template.rs
... / ...
CommitLineData
1use std::io::{Error, ErrorKind::Other, Result};
2use std::collections::HashMap;
3use std::fs::File;
4use std::path::PathBuf;
5use std::io::Read;
6
7const TXT_TEMPLATE: &'static str = include_str!("../templates/index.txt");
8const HTML_TEMPLATE: &'static str = include_str!("../templates/index.html");
9const GMI_TEMPLATE: &'static str = include_str!("../templates/index.gmi");
10const RSS_TEMPLATE: &'static str = include_str!("../templates/feed.xml");
11
12// Parse and Render
13
14pub enum Token {
15 Text(String),
16 DisplayDirective { content: String },
17 ConditionalDirective { condition: String, children: Vec<Token>},
18 IteratorDirective { collection: String, member_label: String, children: Vec<Token> }
19}
20
21impl std::fmt::Display for Token {
22 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
23 match self {
24 Token::Text(label) => write!(f, "Text {}", label),
25 Token::DisplayDirective{content} => write!(f, "DisplayDirective {}", content),
26 Token::ConditionalDirective{condition, children} => {
27 write!(f, "ConditionalDirective {} [[[\n", condition)?;
28 for child in children {
29 write!(f, "\t{}\n", child)?;
30 }
31 write!(f, "\n]]]")
32 },
33 Token::IteratorDirective{collection, member_label, children} => {
34 write!(f, "{} in {}\n", collection, member_label)?;
35 for child in children {
36 write!(f, "\t{}\n", child)?;
37 }
38 write!(f, "\n]]]")
39 },
40 }
41 }
42}
43
44#[derive(Clone)]
45pub enum TemplateValue {
46 String(String),
47 Unsigned(u64),
48 Bool(bool),
49 Collection(Vec<TemplateContext>),
50 Context(TemplateContext)
51}
52
53impl TemplateValue {
54 fn render(&self) -> String {
55 match self {
56 TemplateValue::String(string) => string.to_string(),
57 TemplateValue::Unsigned(number) => format!("{}", number),
58 TemplateValue::Bool(bool) => format!("{}", bool),
59 _ => "".to_string()
60 }
61 }
62}
63
64pub type TemplateContext = HashMap<String, TemplateValue>;
65
66struct TemplateContextGetter {}
67impl TemplateContextGetter {
68 fn get(context: &TemplateContext, path: &str) -> Option<TemplateValue> {
69 let path_parts: Vec<&str> = path.split('.').collect();
70 TemplateContextGetter::recursively_get_value(context, &path_parts)
71 }
72
73 fn recursively_get_value(context: &TemplateContext, path: &[&str]) -> Option<TemplateValue> {
74 match context.get(path[0]) {
75 Some(TemplateValue::Context(next)) if path.len() > 1 => TemplateContextGetter::recursively_get_value(next, &path[1..]),
76 Some(value) if path.len() == 1 => Some(value.clone()),
77 _ => None
78 }
79 }
80}
81
82pub struct ParsedTemplate {
83 pub tokens: Vec<Token>
84}
85
86impl ParsedTemplate {
87 pub fn render(&self, context: &TemplateContext) -> Result<String> {
88 ParsedTemplate::render_tokens(&self.tokens, context)
89 }
90
91 pub fn render_tokens(tokens: &Vec<Token>, context: &TemplateContext) -> Result<String> {
92 let mut rendered_template: String = String::new();
93 for token in tokens {
94 match token {
95 Token::Text(contents) => rendered_template.push_str(&contents),
96 Token::DisplayDirective { content } => {
97 let value = TemplateContextGetter::get(context, &content)
98 .ok_or_else(|| Error::new(Other, format!("{} is not a valid key", content)))?;
99 rendered_template.push_str(&value.render());
100 },
101 Token::ConditionalDirective { condition, children} => {
102 let mut negator = false;
103 let mut condition = condition.to_string();
104 if condition.starts_with('!') {
105 negator = true;
106 condition = condition[1..].to_string();
107 }
108
109 let value = TemplateContextGetter::get(context, &condition)
110 .ok_or_else(|| Error::new(Other, format!("{} is not a valid key", condition)))?;
111
112 match value {
113 TemplateValue::Bool(value) => {
114 if negator ^ value {
115 rendered_template.push_str(&ParsedTemplate::render_tokens(children, context)?)
116 }
117 Ok(())
118 },
119 _ => Err(Error::new(Other, format!("{} is not a boolean value", condition))),
120 }?;
121 },
122 Token::IteratorDirective { collection, member_label, children } => {
123 let value = TemplateContextGetter::get(context, &collection)
124 .ok_or_else(|| Error::new(Other, format!("{} is not a valid key", collection)))?;
125
126 match value {
127 TemplateValue::Collection(collection) => {
128 for member in collection {
129 let mut child_context = context.clone();
130 child_context.insert(
131 member_label.to_string(),
132 TemplateValue::Context(member)
133 );
134 rendered_template.push_str(&ParsedTemplate::render_tokens(&children, &child_context)?)
135 }
136 Ok(())
137 },
138 _ => Err(Error::new(Other, format!("{} is not a collection", collection))),
139 }?;
140 }
141 }
142 }
143 Ok(rendered_template)
144 }
145}
146
147pub fn parse(template: &str) -> Option<ParsedTemplate> {
148 let mut tokens = Vec::new();
149 tokenize(template, &mut tokens).ok()?;
150 Some(ParsedTemplate {
151 tokens
152 })
153}
154
155fn tokenize(template: &str, tokens: &mut Vec<Token>) -> Result<()> {
156 let mut remaining_template = template;
157
158 while !remaining_template.is_empty() && remaining_template.contains("{{") {
159 let directive_start_index = remaining_template.find("{{")
160 .ok_or_else(|| Error::new(Other, "Was expecting at least one tag opener"))?;
161 if directive_start_index > 0 {
162 let text = remaining_template[..directive_start_index].to_string();
163 tokens.push(Token::Text(text.to_string()));
164 }
165 remaining_template = &remaining_template[directive_start_index..];
166
167 let directive_end_index = remaining_template.find("}}")
168 .ok_or_else(|| Error::new(Other, "Was expecting }} after {{"))? + 2;
169 let directive = &remaining_template[..directive_end_index];
170 remaining_template = &remaining_template[directive_end_index..];
171
172 let directive_type = directive.chars().nth(2).unwrap();
173 match directive_type {
174 // Simple Directives
175 '=' => {
176 let content = directive[3..directive.len() - 2].trim();
177 tokens.push(Token::DisplayDirective{
178 content: content.to_string()
179 });
180 },
181 // Block Directives
182 '?' | '~' => {
183 let content = directive[3..directive.len() - 2].trim();
184 let mut children = Vec::new();
185
186 match directive_type {
187 '?' => {
188 let closing_block = remaining_template.find("{{?}}").unwrap();
189 let directive_block = &remaining_template[..closing_block];
190 remaining_template = &remaining_template[closing_block + 5..];
191 tokenize(directive_block, &mut children)?;
192 tokens.push(Token::ConditionalDirective{
193 condition: content.to_string(),
194 children
195 });
196 },
197 '~' => {
198 let parts: Vec<_> = content.splitn(2, ':').collect();
199 let closing_block = remaining_template.find("{{~}}").unwrap();
200 let directive_block = &remaining_template[..closing_block];
201 remaining_template = &remaining_template[closing_block + 5..];
202 tokenize(directive_block, &mut children)?;
203 if parts.len() == 2 {
204 tokens.push(Token::IteratorDirective {
205 collection: parts[0].trim().to_string(),
206 member_label: parts[1].trim().to_string(),
207 children
208 });
209 }
210 },
211 _ => unreachable!()
212 }
213 },
214 _ => unreachable!()
215 }
216 }
217 tokens.push(Token::Text(remaining_template.to_string()));
218 Ok(())
219}
220
221// File helpers.
222
223pub fn find(template_directory: &PathBuf, filename: &str) -> Option<String> {
224 let template_path = template_directory.join(filename);
225 if template_path.exists() {
226 let mut contents = String::new();
227 if File::open(template_path).ok()?.read_to_string(&mut contents).is_ok() {
228 return Some(contents);
229 }
230 }
231 find_default(filename)
232}
233
234fn find_default(filename: &str) -> Option<String> {
235 match filename {
236 "index.txt" => Some(TXT_TEMPLATE.to_string()),
237 "index.html" => Some(HTML_TEMPLATE.to_string()),
238 "index.gmi" => Some(GMI_TEMPLATE.to_string()),
239 "index.rss" => Some(RSS_TEMPLATE.to_string()),
240 &_ => None
241 }
242}