aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/template.rs452
1 files changed, 444 insertions, 8 deletions
diff --git a/src/template.rs b/src/template.rs
index 33bc662..53639d9 100644
--- a/src/template.rs
+++ b/src/template.rs
@@ -11,6 +11,7 @@ const RSS_TEMPLATE: &str = include_str!("../templates/feed.xml");
// Parse and Render
+#[derive(Debug, PartialEq)]
pub enum Token {
Text(String),
DisplayDirective {
@@ -38,20 +39,20 @@ impl std::fmt::Display for Token {
} => {
writeln!(f, "ConditionalDirective {condition} [[[")?;
for child in children {
- writeln!(f, "\t{child}")?;
+ writeln!(f, "\t{child}").unwrap();
}
- write!(f, "\n]]]")
+ write!(f, "]]]")
}
Token::IteratorDirective {
collection,
member_label,
children,
} => {
- writeln!(f, "{collection} in {member_label}")?;
+ writeln!(f, "IteratorDirective {member_label} in {collection} [[[")?;
for child in children {
- writeln!(f, "\t{child}")?;
+ writeln!(f, "\t{child}").unwrap();
}
- write!(f, "\n]]]")
+ write!(f, "]]]")
}
}
}
@@ -97,6 +98,7 @@ impl ContextGetter {
}
}
+#[derive(Debug)]
pub struct Parsed {
pub tokens: Vec<Token>,
}
@@ -220,7 +222,12 @@ fn tokenize(template: &str, tokens: &mut Vec<Token>) -> Result<()> {
match directive_type {
'?' => {
- let closing_block = remaining_template.find("{{?}}").unwrap();
+ let closing_block = remaining_template.find("{{?}}").ok_or_else(|| {
+ Error::new(
+ Other,
+ "Could not find closing tag, expecting {{?}}, found end-of-file",
+ )
+ })?;
let directive_block = &remaining_template[..closing_block];
remaining_template = &remaining_template[closing_block + 5..];
tokenize(directive_block, &mut children)?;
@@ -231,7 +238,12 @@ fn tokenize(template: &str, tokens: &mut Vec<Token>) -> Result<()> {
}
'~' => {
let parts: Vec<_> = content.splitn(2, ':').collect();
- let closing_block = remaining_template.find("{{~}}").unwrap();
+ let closing_block = remaining_template.find("{{~}}").ok_or_else(|| {
+ Error::new(
+ Other,
+ "Could not find closing tag, expecting {{?}}, found end-of-file",
+ )
+ })?;
let directive_block = &remaining_template[..closing_block];
remaining_template = &remaining_template[closing_block + 5..];
tokenize(directive_block, &mut children)?;
@@ -246,7 +258,12 @@ fn tokenize(template: &str, tokens: &mut Vec<Token>) -> Result<()> {
_ => unreachable!(),
}
}
- _ => unreachable!(),
+ _ => {
+ return Err(Error::new(
+ Other,
+ "{{ }} is not a valid directive. Need one of: =, ? or ~.",
+ ))
+ }
}
}
tokens.push(Token::Text(remaining_template.to_string()));
@@ -279,3 +296,422 @@ fn find_default(filename: &str) -> Option<String> {
&_ => None,
}
}
+
+#[cfg(test)]
+mod tests {
+ use std::path::PathBuf;
+
+ use test_utilities::*;
+
+ use super::*;
+
+ // Template Parser
+
+ #[test]
+ fn test_empty_string_can_be_parsed() {
+ let template = "";
+
+ if let Some(template) = parse(&template) {
+ assert_eq!(template.tokens.len(), 1);
+ assert_eq!(template.tokens[0], Token::Text("".to_string()));
+ } else {
+ panic!("Expected empty template to be parsed");
+ }
+ }
+
+ #[test]
+ fn fails_on_malformed_directive() {
+ let template = "{{ Left door open";
+
+ let parsed_template = parse(&template);
+ assert!(parsed_template.is_none());
+ }
+
+ #[test]
+ fn fails_on_malformed_conditional() {
+ let template = "{{? hello }} NO TERMINATOR!!";
+
+ let parsed_template = parse(&template);
+ assert!(parsed_template.is_none());
+ }
+
+ #[test]
+ fn fails_on_malformed_iterator() {
+ let template = "{{~ hello }} NO TERMINATOR!!";
+
+ let parsed_template = parse(&template);
+ assert!(parsed_template.is_none());
+ }
+
+ #[test]
+ fn fails_on_malformed_conditional_body() {
+ let template = "{{? hello }} {{ someone forgot {{?}}";
+
+ let parsed_template = parse(&template);
+ assert!(parsed_template.is_none());
+ }
+
+ #[test]
+ fn fails_on_malformed_iterator_body() {
+ let template = "{{~ hello }} {{ my right hand claw {{~}}";
+
+ let parsed_template = parse(&template);
+ assert!(parsed_template.is_none());
+ }
+
+ #[test]
+ fn test_fails_on_invalid_directives() {
+ let template = "My name is {{ name }}";
+
+ let context: Context =
+ HashMap::from([("name".to_string(), Value::String("Scrooge".to_string()))]);
+
+ let template = parse(&template);
+ assert!(template.is_none());
+ }
+
+ #[test]
+ fn test_recognizes_display_directive() {
+ let template = "My name is: {{= blog }}";
+
+ if let Some(template) = parse(&template) {
+ assert_eq!(template.tokens.len(), 3);
+ assert_eq!(template.tokens[0], Token::Text("My name is: ".to_string()));
+ assert_eq!(
+ template.tokens[1],
+ Token::DisplayDirective {
+ content: "blog".to_string()
+ }
+ );
+ assert_eq!(template.tokens[2], Token::Text("".to_string()));
+ } else {
+ panic!("Expected empty template to be parsed");
+ }
+ }
+
+ #[test]
+ fn test_not_allowed_to_nest_directives_in_display() {
+ let template = "{{= {{ hello }} }}";
+
+ if let Some(template) = parse(&template) {
+ assert_eq!(template.tokens.len(), 2);
+ assert_eq!(
+ template.tokens[0],
+ Token::DisplayDirective {
+ content: "{{ hello".to_string()
+ }
+ );
+ assert_eq!(template.tokens[1], Token::Text(" }}".to_string()));
+ } else {
+ panic!("Expected empty template to be parsed");
+ }
+ }
+
+ #[test]
+ fn test_recognizes_conditional_directive() {
+ let template = "{{? what }}OK {{= hello }}{{?}}";
+
+ if let Some(template) = parse(&template) {
+ assert_eq!(template.tokens.len(), 2);
+ assert_eq!(
+ template.tokens[0],
+ Token::ConditionalDirective {
+ condition: "what".to_string(),
+ children: vec![
+ Token::Text("OK ".to_string()),
+ Token::DisplayDirective {
+ content: "hello".to_string()
+ },
+ Token::Text("".to_string()),
+ ]
+ }
+ );
+ assert_eq!(template.tokens[1], Token::Text("".to_string()));
+ } else {
+ panic!("Expected empty template to be parsed");
+ }
+ }
+
+ #[test]
+ fn test_recognizes_iterator_directive() {
+ let template = "{{~ murder:crow }}Sound: {{= caw }}{{~}}";
+
+ if let Some(template) = parse(&template) {
+ assert_eq!(template.tokens.len(), 2);
+ assert_eq!(
+ template.tokens[0],
+ Token::IteratorDirective {
+ collection: "murder".to_string(),
+ member_label: "crow".to_string(),
+ children: vec![
+ Token::Text("Sound: ".to_string()),
+ Token::DisplayDirective {
+ content: "caw".to_string()
+ },
+ Token::Text("".to_string()),
+ ]
+ }
+ );
+ assert_eq!(template.tokens[1], Token::Text("".to_string()));
+ } else {
+ panic!("Expected empty template to be parsed");
+ }
+ }
+
+ // Template Debug Formatting
+ #[test]
+ fn test_displays_text_token_in_readable_format() {
+ let token = Token::Text("Bugs are cool".to_string());
+ assert_eq!(format!("{token}"), "Text Bugs are cool");
+ }
+
+ #[test]
+ fn test_displays_display_directive_token_in_readable_format() {
+ let token = Token::DisplayDirective {
+ content: "Wait, not that kind!".to_string(),
+ };
+ assert_eq!(format!("{token}"), "DisplayDirective Wait, not that kind!");
+ }
+
+ #[test]
+ fn test_displays_conditional_directive_token_in_readable_format() {
+ let token = Token::ConditionalDirective {
+ condition: "Woah".to_string(),
+ children: vec![Token::Text("The kind with six to eight legs".to_string())],
+ };
+ assert_eq!(
+ format!("{token}"),
+ "ConditionalDirective Woah [[[
+\tText The kind with six to eight legs
+]]]"
+ );
+ }
+
+ #[test]
+ fn test_displays_iterator_directive_token_in_readable_format() {
+ let token = Token::IteratorDirective {
+ collection: "bugs".to_string(),
+ member_label: "bug".to_string(),
+ children: vec![Token::Text("yeah, that's right!".to_string())],
+ };
+ assert_eq!(
+ format!("{token}"),
+ "IteratorDirective bug in bugs [[[
+\tText yeah, that's right!
+]]]"
+ );
+ }
+
+ // Template Rendering
+
+ #[test]
+ fn test_renders_a_parsed_template() {
+ let template = "\
+# This is my template.
+My name is {{= name }}
+{{? positive }}Big if true: {{= size }}.{{?}}
+{{? !positive }}False, {{= name }}.{{?}}
+{{~ bits:bit }}Bit: {{= bit.z }}.{{~}}
+The truth is {{= positive }}
+But this is not: {{= bits }}.
+One last {{= last.one }}
+";
+
+ let context: Context = HashMap::from([
+ ("name".to_string(), Value::String("Aceituno".to_string())),
+ (
+ "last".to_string(),
+ Value::Context(HashMap::from([(
+ "one".to_string(),
+ Value::String("OK".to_string()),
+ )])),
+ ),
+ ("positive".to_string(), Value::Bool(true)),
+ ("size".to_string(), Value::Unsigned(28)),
+ (
+ "bits".to_string(),
+ Value::Collection(vec![
+ HashMap::from([("z".to_string(), Value::String("Z".to_string()))]),
+ HashMap::from([("z".to_string(), Value::String("Zz".to_string()))]),
+ HashMap::from([("z".to_string(), Value::String("ZzZ".to_string()))]),
+ ]),
+ ),
+ ]);
+
+ if let Some(template) = parse(&template) {
+ match template.render(&context) {
+ Err(error) => panic!("{error}"),
+ Ok(output) => assert_eq!(
+ output,
+ "\
+# This is my template.
+My name is Aceituno
+Big if true: 28.
+
+Bit: Z.Bit: Zz.Bit: ZzZ.
+The truth is true
+But this is not: .
+One last OK
+"
+ ),
+ }
+ } else {
+ panic!("Expected template to be parsed");
+ }
+ }
+
+ #[test]
+ fn test_rendering_fails_on_incorrect_keys() {
+ let template = "My name is {{= name }}";
+
+ let context: Context = HashMap::new();
+
+ if let Some(template) = parse(&template) {
+ let output = template.render(&context);
+ assert!(output.is_err());
+ } else {
+ panic!("Expected template to be parsed");
+ }
+ }
+
+ #[test]
+ fn test_rendering_fails_on_incorrect_conditions() {
+ let template = "{{? what_is_to_be_done }}not_this{{?}}";
+
+ let context: Context = HashMap::new();
+
+ if let Some(template) = parse(&template) {
+ let output = template.render(&context);
+ assert!(output.is_err());
+ } else {
+ panic!("Expected template to be parsed");
+ }
+ }
+
+ #[test]
+ fn test_rendering_fails_on_non_boolean_conditions() {
+ let template = "{{? question }}answer{{?}}";
+
+ let context: Context = HashMap::from([(
+ "question".to_string(),
+ Value::String("more a comment than a question".to_string()),
+ )]);
+
+ if let Some(template) = parse(&template) {
+ let output = template.render(&context);
+ assert!(output.is_err());
+ } else {
+ panic!("Expected template to be parsed");
+ }
+ }
+
+ #[test]
+ fn test_rendering_fails_on_incorrect_collections() {
+ let template = "{{~ nothings:sweet }}ugh{{~}}";
+
+ let context: Context = HashMap::new();
+
+ if let Some(template) = parse(&template) {
+ let output = template.render(&context);
+ assert!(output.is_err());
+ } else {
+ panic!("Expected template to be parsed");
+ }
+ }
+
+ #[test]
+ fn test_rendering_fails_on_incorrect_iterator_children() {
+ let template = "{{~ somethings:sour }}{{= sour.nok }}{{~}}";
+
+ let context: Context = HashMap::from([(
+ "somethings".to_string(),
+ Value::Collection(vec![HashMap::from([(
+ "ok".to_string(),
+ Value::String("Very much".to_string()),
+ )])]),
+ )]);
+
+ if let Some(template) = parse(&template) {
+ let output = template.render(&context);
+ assert!(output.is_err());
+ } else {
+ panic!("Expected template to be parsed");
+ }
+ }
+
+ #[test]
+ fn test_rendering_fails_on_non_collection_collections() {
+ let template = "{{~ sheeps:sheep }}{{= sheep.bah }}{{~}}";
+
+ let context: Context =
+ HashMap::from([("sheeps".to_string(), Value::String("wolf".to_string()))]);
+
+ if let Some(template) = parse(&template) {
+ let output = template.render(&context);
+ assert!(output.is_err());
+ } else {
+ panic!("Expected template to be parsed");
+ }
+ }
+
+ // Template Finder
+
+ #[test]
+ fn test_loads_template_from_path_if_exists() {
+ let test_dir = setup_test_dir();
+ create_test_file(&test_dir.join("index.html"), "Hello");
+
+ if let Some(template) = find(&test_dir, "index.html") {
+ assert_eq!(template, "Hello");
+ } else {
+ panic!("Could not find provided index.");
+ }
+
+ cleanup_test_dir(&test_dir);
+ }
+
+ #[test]
+ fn test_there_is_no_default_for_unknown_types() {
+ let template = find(
+ &PathBuf::from("/norealpath/ifthisfails/then/wow"),
+ "image.png",
+ );
+ assert!(template.is_none());
+ }
+
+ #[test]
+ fn test_finds_default_html_templalte() {
+ let template = find(
+ &PathBuf::from("/norealpath/ifthisfails/then/wow"),
+ "index.html",
+ );
+ assert!(!template.is_none());
+ }
+
+ #[test]
+ fn test_finds_default_txt_templalte() {
+ let template = find(
+ &PathBuf::from("/norealpath/ifthisfails/then/wow"),
+ "index.txt",
+ );
+ assert!(!template.is_none());
+ }
+
+ #[test]
+ fn test_finds_default_gmi_templalte() {
+ let template = find(
+ &PathBuf::from("/norealpath/ifthisfails/then/wow"),
+ "index.gmi",
+ );
+ assert!(!template.is_none());
+ }
+
+ #[test]
+ fn test_finds_default_rss_templalte() {
+ let template = find(
+ &PathBuf::from("/norealpath/ifthisfails/then/wow"),
+ "index.rss",
+ );
+ assert!(!template.is_none());
+ }
+}