]>
Commit | Line | Data |
---|---|---|
1 | use std::io::{Error, ErrorKind::Other, Result}; | |
2 | use std::collections::HashMap; | |
3 | use std::fs::File; | |
4 | use std::path::PathBuf; | |
5 | use std::io::Read; | |
6 | ||
7 | const TXT_TEMPLATE: &'static str = include_str!("../templates/index.txt"); | |
8 | const HTML_TEMPLATE: &'static str = include_str!("../templates/index.html"); | |
9 | const GMI_TEMPLATE: &'static str = include_str!("../templates/index.gmi"); | |
10 | const RSS_TEMPLATE: &'static str = include_str!("../templates/feed.xml"); | |
11 | ||
12 | // Parse and Render | |
13 | ||
14 | pub 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 | ||
21 | impl 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)] | |
45 | pub enum TemplateValue { | |
46 | String(String), | |
47 | Unsigned(u64), | |
48 | Bool(bool), | |
49 | Collection(Vec<TemplateContext>), | |
50 | Context(TemplateContext) | |
51 | } | |
52 | ||
53 | impl 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 | ||
64 | pub type TemplateContext = HashMap<String, TemplateValue>; | |
65 | ||
66 | struct TemplateContextGetter {} | |
67 | impl 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 | ||
82 | pub struct ParsedTemplate { | |
83 | pub tokens: Vec<Token> | |
84 | } | |
85 | ||
86 | impl 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 | ||
147 | pub 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 | ||
155 | fn 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 | ||
223 | pub 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 | ||
234 | fn 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 | } |