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