Repository: martinohmann/hcl-rs
Pull request #153 (2023-01-12 14:37:22)

fix(eval): implement correct template literal stripping
This fixes two issues:

- Whitespace should only be stripped from literals up until (and including) the next line break. Previously, all preceding/following line breaks were removed.
- The `for` directive's strip mode must be applied to all templates that are looped over, not just the first and last collection item.


Patch:
diff --git a/src/eval/template.rs b/src/eval/template.rs
index 525ebe7..f5707c5 100644
--- a/src/eval/template.rs
+++ b/src/eval/template.rs
@@ -34,13 +34,7 @@ fn evaluate_element(
 ) -> EvalResult<()> {
     match element {
         Element::Literal(literal) => {
-            let stripped = match strip {
-                StripMode::Both => literal.trim(),
-                StripMode::Start => literal.trim_start(),
-                StripMode::End => literal.trim_end(),
-                StripMode::None => literal,
-            };
-            result.push_str(stripped);
+            result.push_str(strip_literal(literal, strip));
             Ok(())
         }
         Element::Interpolation(interp) => evaluate_interpolation(result, interp, ctx),
@@ -48,6 +42,37 @@ fn evaluate_element(
     }
 }
 
+// Depending on the `StripMode`, strips off leading and trailing spaces up until the first line
+// break that is encountered. The line break is stripped as well.
+fn strip_literal(literal: &str, strip: StripMode) -> &str {
+    fn is_space(ch: char) -> bool {
+        ch.is_whitespace() && ch != '\r' && ch != '\n'
+    }
+
+    fn trim_start(s: &str) -> &str {
+        let s = s.trim_start_matches(is_space);
+
+        s.strip_prefix("\r\n")
+            .or_else(|| s.strip_prefix('\n'))
+            .unwrap_or(s)
+    }
+
+    fn trim_end(s: &str) -> &str {
+        let s = s.trim_end_matches(is_space);
+
+        s.strip_suffix("\r\n")
+            .or_else(|| s.strip_suffix('\n'))
+            .unwrap_or(s)
+    }
+
+    match strip {
+        StripMode::Both => trim_start(trim_end(literal)),
+        StripMode::Start => trim_start(literal),
+        StripMode::End => trim_end(literal),
+        StripMode::None => literal,
+    }
+}
+
 fn evaluate_interpolation(
     result: &mut String,
     interp: &Interpolation,
@@ -91,23 +116,11 @@ fn evaluate_for_directive(
     dir: &ForDirective,
     ctx: &Context,
 ) -> EvalResult<()> {
+    let strip = StripMode::from_adjacent(dir.for_strip, dir.endfor_strip);
     let collection = expr::Collection::from_for_directive(dir, ctx)?;
-    let len = collection.len();
-
-    for (index, ctx) in collection.into_iter().enumerate() {
-        let ctx = &ctx?;
-
-        let strip = if index == 0 && len == 1 {
-            StripMode::from_adjacent(dir.for_strip, dir.endfor_strip)
-        } else if index == 0 {
-            StripMode::from_adjacent(dir.for_strip, StripMode::None)
-        } else if index == len - 1 {
-            StripMode::from_adjacent(StripMode::None, dir.endfor_strip)
-        } else {
-            StripMode::None
-        };
 
-        evaluate_template(result, &dir.template, ctx, strip)?;
+    for ctx in collection {
+        evaluate_template(result, &dir.template, &ctx?, strip)?;
     }
 
     Ok(())


Test patch:
diff --git a/tests/eval.rs b/tests/eval.rs
index 6d6fb62..e2b595e 100644
--- a/tests/eval.rs
+++ b/tests/eval.rs
@@ -337,8 +337,11 @@ fn eval_template() {
         Let's ${~ what ~} :
         %{ for item in items ~}
         - ${item}
+
         %{~ endfor ~}
 
+        Yay!
+
     "#};
 
     let expected = indoc! {r#"
@@ -346,8 +349,10 @@ fn eval_template() {
         - foo
         - bar
         - baz
-    "#}
-    .trim_end();
+
+        Yay!
+
+    "#};
 
     let mut ctx = Context::new();
     ctx.declare_var("what", " render a list");