Repository: sunng87/handlebars-rust (rust)
Issue: Unexpected trimming of whitespace after loop
Given a template like this:

```hbs
{{#each routes}}
import { default as {{this.handler}} } from '{{this.file_path}}'
{{/each}}

addEventListener('fetch', (event) => {
  event.respondWith(handleEvent(event))
})
```

handlebars 4.1.3 will remove the empty line after the loop (in fact, it will remove all empty lines if there are any):

```js
import { default as __hello_handler } from './hello.js'
import { default as __world_index_handler } from './world/index.js'
import { default as __index_handler } from './index.js'
addEventListener('fetch', (event) => {
  event.respondWith(handleEvent(event))
})
```

I'm wondering why that happens though? I don't use `~` or anything else that controls whitespace. My understanding is that rendering should not change any non-templating bits.

Pull request description: fix: trim at most one line directives #472
Fixes #472 

Patch:
diff --git a/src/grammar.rs b/src/grammar.rs
index 23f51a2..fd3ec09 100644
--- a/src/grammar.rs
+++ b/src/grammar.rs
@@ -14,6 +14,17 @@ pub(crate) fn newline_matcher(c: char) -> bool {
     c == '\n' || c == '\r'
 }
 
+#[inline]
+pub(crate) fn strip_first_newline(s: &str) -> &str {
+    if let Some(s) = s.strip_prefix("\r\n") {
+        s
+    } else if let Some(s) = s.strip_prefix('\n') {
+        s
+    } else {
+        s
+    }
+}
+
 pub(crate) fn ends_with_empty_line(text: &str) -> bool {
     let s = text.trim_end_matches(whitespace_matcher);
     // also matches when text is just whitespaces
diff --git a/src/template.rs b/src/template.rs
index 1f70292..ce35a93 100644
--- a/src/template.rs
+++ b/src/template.rs
@@ -449,11 +449,8 @@ impl Template {
         if trim_start {
             RawString(s.trim_start().to_owned())
         } else if trim_start_line {
-            RawString(
-                s.trim_start_matches(grammar::whitespace_matcher)
-                    .trim_start_matches(grammar::newline_matcher)
-                    .to_owned(),
-            )
+            let s = s.trim_start_matches(grammar::whitespace_matcher);
+            RawString(grammar::strip_first_newline(s).to_owned())
         } else {
             RawString(s)
         }


Test patch:
diff --git a/tests/helper_with_space.rs b/tests/helper_with_space.rs
index 5a55ab1..4920071 100644
--- a/tests/helper_with_space.rs
+++ b/tests/helper_with_space.rs
@@ -34,3 +34,34 @@ fn test_helper_with_space_param() {
         .unwrap();
     assert_eq!(s, "Output: Mozilla Firefox, Google Chrome".to_owned());
 }
+
+#[test]
+fn test_empty_lines_472() {
+    let mut r = Handlebars::new();
+
+    r.register_template_string(
+        "t1",
+        r#"{{#each routes}}
+import { default as {{this.handler}} } from '{{this.file_path}}'
+{{/each}}
+
+addEventListener('fetch', (event) => {
+  event.respondWith(handleEvent(event))
+})"#,
+    )
+    .unwrap();
+
+    let data = json!({"routes": [{"handler": "__hello_handler", "file_path": "./hello.js"},
+                                 {"handler": "__world_index_handler", "file_path": "./world/index.js"},
+                                 {"handler": "__index_handler", "file_path": "./index.js"}]});
+
+    let exp = r#"import { default as __hello_handler } from './hello.js'
+import { default as __world_index_handler } from './world/index.js'
+import { default as __index_handler } from './index.js'
+
+addEventListener('fetch', (event) => {
+  event.respondWith(handleEvent(event))
+})"#;
+
+    assert_eq!(r.render("t1", &data).unwrap(), exp);
+}