X-Git-Url: https://git.r.bdr.sh/rbdr/blog/blobdiff_plain/0e276d039b8613de0cbd302bf328bb660ab063b6..2998247083406f914b3647cedd19abf5507bf2c6:/src/template.rs diff --git a/src/template.rs b/src/template.rs new file mode 100644 index 0000000..37906ed --- /dev/null +++ b/src/template.rs @@ -0,0 +1,140 @@ +use std::fs::File; +use std::path::PathBuf; +use std::io::Read; + +const TXT_TEMPLATE: &'static str = include_str!("../templates/index.txt"); +const HTML_TEMPLATE: &'static str = include_str!("../templates/index.html"); +const GMI_TEMPLATE: &'static str = include_str!("../templates/index.gmi"); +const RSS_TEMPLATE: &'static str = include_str!("../templates/feed.xml"); + +// Parse and Render + +pub enum Token { + Text(String), + DisplayDirective { content: String }, + ConditionalDirective { condition: String, children: Vec}, + IteratorDirective { collection: String, member_label: String, children: Vec } +} + +impl std::fmt::Display for Token { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Token::Text(label) => write!(f, "Text {}", label), + Token::DisplayDirective{content} => write!(f, "DisplayDirective {}", content), + Token::ConditionalDirective{condition, children} => { + write!(f, "ConditionalDirective {} [[[\n", condition)?; + for child in children { + write!(f, "\t{}\n", child)?; + } + write!(f, "\n]]]") + }, + Token::IteratorDirective{collection, member_label, children} => { + write!(f, "IteratorDirective {}: {} [[[\n", collection, member_label)?; + for child in children { + write!(f, "\t{}\n", child)?; + } + write!(f, "\n]]]") + }, + } + } +} + +pub struct ParsedTemplate { + pub tokens: Vec +} + +pub fn parse(template: &str) -> ParsedTemplate { + let mut tokens = Vec::new(); + tokenize(template, &mut tokens); + ParsedTemplate { + tokens + } +} + +fn tokenize(template: &str, tokens: &mut Vec) { + let mut remaining_template = template; + + while !remaining_template.is_empty() && remaining_template.contains("{{") { + let directive_start_index = remaining_template.find("{{") + .expect("Was expecting at least one tag opener"); + if directive_start_index > 0 { + let text = remaining_template[..directive_start_index].to_string(); + tokens.push(Token::Text(text.to_string())); + } + remaining_template = &remaining_template[directive_start_index..]; + + let directive_end_index = remaining_template.find("}}") + .expect("Was expecting }} after {{") + 2; + let directive = &remaining_template[..directive_end_index]; + remaining_template = &remaining_template[directive_end_index..]; + + let directive_type = directive.chars().nth(2).unwrap(); + match directive_type { + // Simple Directives + '=' => { + let content = directive[3..directive.len() - 2].trim(); + tokens.push(Token::DisplayDirective{ + content: content.to_string() + }); + }, + // Block Directives + '?' | '~' => { + let content = directive[3..directive.len() - 2].trim(); + let mut children = Vec::new(); + + match directive_type { + '?' => { + let closing_block = remaining_template.find("{{?}}").unwrap(); + let directive_block = &remaining_template[..closing_block]; + remaining_template = &remaining_template[closing_block + 5..]; + tokenize(directive_block, &mut children); + tokens.push(Token::ConditionalDirective{ + condition: content.to_string(), + children + }); + }, + '~' => { + let parts: Vec<_> = content.splitn(2, ':').collect(); + let closing_block = remaining_template.find("{{~}}").unwrap(); + let directive_block = &remaining_template[..closing_block]; + remaining_template = &remaining_template[closing_block + 5..]; + tokenize(directive_block, &mut children); + if parts.len() == 2 { + tokens.push(Token::IteratorDirective { + collection: parts[0].trim().to_string(), + member_label: parts[1].trim().to_string(), + children + }); + } + }, + _ => unreachable!() + } + }, + _ => unreachable!() + } + } + tokens.push(Token::Text(remaining_template.to_string())); +} + +// File helpers. + +pub fn find(template_directory: &PathBuf, filename: &str) -> Option { + let template_path = template_directory.join(filename); + if template_path.exists() { + let mut contents = String::new(); + if File::open(template_path).ok()?.read_to_string(&mut contents).is_ok() { + return Some(contents); + } + } + find_default(filename) +} + +fn find_default(filename: &str) -> Option { + match filename { + "index.txt" => Some(TXT_TEMPLATE.to_string()), + "index.html" => Some(HTML_TEMPLATE.to_string()), + "index.gmi" => Some(GMI_TEMPLATE.to_string()), + "index.rss" => Some(RSS_TEMPLATE.to_string()), + &_ => None + } +}