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 } }