]> git.r.bdr.sh - rbdr/blog/blob - src/template.rs
37906ede58842f9fb321747090a8bd5501c3af71
[rbdr/blog] / src / template.rs
1 use std::fs::File;
2 use std::path::PathBuf;
3 use std::io::Read;
4
5 const TXT_TEMPLATE: &'static str = include_str!("../templates/index.txt");
6 const HTML_TEMPLATE: &'static str = include_str!("../templates/index.html");
7 const GMI_TEMPLATE: &'static str = include_str!("../templates/index.gmi");
8 const RSS_TEMPLATE: &'static str = include_str!("../templates/feed.xml");
9
10 // Parse and Render
11
12 pub enum Token {
13 Text(String),
14 DisplayDirective { content: String },
15 ConditionalDirective { condition: String, children: Vec<Token>},
16 IteratorDirective { collection: String, member_label: String, children: Vec<Token> }
17 }
18
19 impl std::fmt::Display for Token {
20 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
21 match self {
22 Token::Text(label) => write!(f, "Text {}", label),
23 Token::DisplayDirective{content} => write!(f, "DisplayDirective {}", content),
24 Token::ConditionalDirective{condition, children} => {
25 write!(f, "ConditionalDirective {} [[[\n", condition)?;
26 for child in children {
27 write!(f, "\t{}\n", child)?;
28 }
29 write!(f, "\n]]]")
30 },
31 Token::IteratorDirective{collection, member_label, children} => {
32 write!(f, "IteratorDirective {}: {} [[[\n", collection, member_label)?;
33 for child in children {
34 write!(f, "\t{}\n", child)?;
35 }
36 write!(f, "\n]]]")
37 },
38 }
39 }
40 }
41
42 pub struct ParsedTemplate {
43 pub tokens: Vec<Token>
44 }
45
46 pub fn parse(template: &str) -> ParsedTemplate {
47 let mut tokens = Vec::new();
48 tokenize(template, &mut tokens);
49 ParsedTemplate {
50 tokens
51 }
52 }
53
54 fn tokenize(template: &str, tokens: &mut Vec<Token>) {
55 let mut remaining_template = template;
56
57 while !remaining_template.is_empty() && remaining_template.contains("{{") {
58 let directive_start_index = remaining_template.find("{{")
59 .expect("Was expecting at least one tag opener");
60 if directive_start_index > 0 {
61 let text = remaining_template[..directive_start_index].to_string();
62 tokens.push(Token::Text(text.to_string()));
63 }
64 remaining_template = &remaining_template[directive_start_index..];
65
66 let directive_end_index = remaining_template.find("}}")
67 .expect("Was expecting }} after {{") + 2;
68 let directive = &remaining_template[..directive_end_index];
69 remaining_template = &remaining_template[directive_end_index..];
70
71 let directive_type = directive.chars().nth(2).unwrap();
72 match directive_type {
73 // Simple Directives
74 '=' => {
75 let content = directive[3..directive.len() - 2].trim();
76 tokens.push(Token::DisplayDirective{
77 content: content.to_string()
78 });
79 },
80 // Block Directives
81 '?' | '~' => {
82 let content = directive[3..directive.len() - 2].trim();
83 let mut children = Vec::new();
84
85 match directive_type {
86 '?' => {
87 let closing_block = remaining_template.find("{{?}}").unwrap();
88 let directive_block = &remaining_template[..closing_block];
89 remaining_template = &remaining_template[closing_block + 5..];
90 tokenize(directive_block, &mut children);
91 tokens.push(Token::ConditionalDirective{
92 condition: content.to_string(),
93 children
94 });
95 },
96 '~' => {
97 let parts: Vec<_> = content.splitn(2, ':').collect();
98 let closing_block = remaining_template.find("{{~}}").unwrap();
99 let directive_block = &remaining_template[..closing_block];
100 remaining_template = &remaining_template[closing_block + 5..];
101 tokenize(directive_block, &mut children);
102 if parts.len() == 2 {
103 tokens.push(Token::IteratorDirective {
104 collection: parts[0].trim().to_string(),
105 member_label: parts[1].trim().to_string(),
106 children
107 });
108 }
109 },
110 _ => unreachable!()
111 }
112 },
113 _ => unreachable!()
114 }
115 }
116 tokens.push(Token::Text(remaining_template.to_string()));
117 }
118
119 // File helpers.
120
121 pub fn find(template_directory: &PathBuf, filename: &str) -> Option<String> {
122 let template_path = template_directory.join(filename);
123 if template_path.exists() {
124 let mut contents = String::new();
125 if File::open(template_path).ok()?.read_to_string(&mut contents).is_ok() {
126 return Some(contents);
127 }
128 }
129 find_default(filename)
130 }
131
132 fn find_default(filename: &str) -> Option<String> {
133 match filename {
134 "index.txt" => Some(TXT_TEMPLATE.to_string()),
135 "index.html" => Some(HTML_TEMPLATE.to_string()),
136 "index.gmi" => Some(GMI_TEMPLATE.to_string()),
137 "index.rss" => Some(RSS_TEMPLATE.to_string()),
138 &_ => None
139 }
140 }