]>
Commit | Line | Data |
---|---|---|
f6a545b0 | 1 | use std::collections::HashMap; |
29982470 RBR |
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} => { | |
f6a545b0 | 33 | write!(f, "{} in {}\n", collection, member_label)?; |
29982470 RBR |
34 | for child in children { |
35 | write!(f, "\t{}\n", child)?; | |
36 | } | |
37 | write!(f, "\n]]]") | |
38 | }, | |
39 | } | |
40 | } | |
41 | } | |
42 | ||
f6a545b0 RBR |
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 | ||
29982470 RBR |
81 | pub struct ParsedTemplate { |
82 | pub tokens: Vec<Token> | |
83 | } | |
84 | ||
f6a545b0 RBR |
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 | ||
29982470 RBR |
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 | } |