add test and readme
diff --git a/README.md b/README.md
index 8156469..e9b9236 100644
--- a/README.md
+++ b/README.md
@@ -228,6 +228,18 @@
 // Implemented type checks: isArray, isBoolean, isFloat, isInteger, isNumber, isObject, isString,
 ```
 
+### Whitespace Control
+
+In the default configuration, no whitespace is removed while rendering the file. To support a more readable template style, you can configure the environment to control whitespaces before and after a statement automatically. While enabling `set_trim` removes the first newline after a statement, `set_lstrip` strips tabs and spaces from the beginning of a line to the start of a block.
+
+```c++
+Environment env;
+env.set_trim(true);
+env.set_lstrip(true);
+```
+
+With both `trim` and `lstrip` enabled, you can put statements on their own lines.
+
 ### Callbacks
 
 You can create your own and more complex functions with callbacks.
diff --git a/single_include/inja/inja.hpp b/single_include/inja/inja.hpp
index 0cb64f2..80b3d68 100644
--- a/single_include/inja/inja.hpp
+++ b/single_include/inja/inja.hpp
@@ -1370,6 +1370,9 @@
   std::string comment_close {"#}"};
   std::string open_chars {"#{"};
 
+  bool trim_blocks {false};
+  bool lstrip_blocks {false};
+
   void update_open_chars() {
     open_chars = "";
     if (open_chars.find(line_statement[0]) == std::string::npos) {
@@ -1793,12 +1796,15 @@
 
         // try to match one of the opening sequences, and get the close
         nonstd::string_view open_str = m_in.substr(m_pos);
+        bool must_lstrip = false;
         if (inja::string_view::starts_with(open_str, m_config.expression_open)) {
           m_state = State::ExpressionStart;
         } else if (inja::string_view::starts_with(open_str, m_config.statement_open)) {
           m_state = State::StatementStart;
+          must_lstrip = m_config.lstrip_blocks;
         } else if (inja::string_view::starts_with(open_str, m_config.comment_open)) {
           m_state = State::CommentStart;
+          must_lstrip = m_config.lstrip_blocks;
         } else if ((m_pos == 0 || m_in[m_pos - 1] == '\n') &&
                    inja::string_view::starts_with(open_str, m_config.line_statement)) {
           m_state = State::LineStart;
@@ -1806,8 +1812,13 @@
           m_pos += 1; // wasn't actually an opening sequence
           goto again;
         }
-        if (m_pos == m_tok_start) goto again;  // don't generate empty token
-        return make_token(Token::Kind::Text);
+
+        nonstd::string_view text = string_view::slice(m_in, m_tok_start, m_pos);
+        if (must_lstrip)
+          text = clear_final_line_if_whitespace(text);
+
+        if (text.empty()) goto again;  // don't generate empty token
+        return Token(Token::Kind::Text, text);
       }
       case State::ExpressionStart: {
         m_state = State::ExpressionBody;
@@ -1834,7 +1845,7 @@
       case State::LineBody:
         return scan_body("\n", Token::Kind::LineStatementClose);
       case State::StatementBody:
-        return scan_body(m_config.statement_close, Token::Kind::StatementClose);
+        return scan_body(m_config.statement_close, Token::Kind::StatementClose, m_config.trim_blocks);
       case State::CommentBody: {
         // fast-scan to comment close
         size_t end = m_in.substr(m_pos).find(m_config.comment_close);
@@ -1845,7 +1856,10 @@
         // return the entire comment in the close token
         m_state = State::Text;
         m_pos += end + m_config.comment_close.size();
-        return make_token(Token::Kind::CommentClose);
+        Token tok = make_token(Token::Kind::CommentClose);
+        if (m_config.trim_blocks)
+          skip_newline();
+        return tok;
       }
     }
   }
@@ -1853,7 +1867,7 @@
   const LexerConfig& get_config() const { return m_config; }
 
  private:
-  Token scan_body(nonstd::string_view close, Token::Kind closeKind) {
+  Token scan_body(nonstd::string_view close, Token::Kind closeKind, bool trim = false) {
   again:
     // skip whitespace (except for \n as it might be a close)
     if (m_tok_start >= m_in.size()) return make_token(Token::Kind::Eof);
@@ -1867,7 +1881,10 @@
     if (inja::string_view::starts_with(m_in.substr(m_tok_start), close)) {
       m_state = State::Text;
       m_pos = m_tok_start + close.size();
-      return make_token(closeKind);
+      Token tok = make_token(closeKind);
+      if (trim)
+        skip_newline();
+      return tok;
     }
 
     // skip \n
@@ -1988,6 +2005,34 @@
   Token make_token(Token::Kind kind) const {
     return Token(kind, string_view::slice(m_in, m_tok_start, m_pos));
   }
+
+  void skip_newline() {
+    if (m_pos < m_in.size()) {
+      char ch = m_in[m_pos];
+      if (ch == '\n')
+        m_pos += 1;
+      else if (ch == '\r') {
+        m_pos += 1;
+        if (m_pos < m_in.size() && m_in[m_pos] == '\n')
+          m_pos += 1;
+      }
+    }
+  }
+
+  static nonstd::string_view clear_final_line_if_whitespace(nonstd::string_view text)
+  {
+    nonstd::string_view result = text;
+    while (!result.empty()) {
+      char ch = result.back();
+      if (ch == ' ' || ch == '\t')
+       result.remove_suffix(1);
+      else if (ch == '\n' || ch == '\r')
+        break;
+      else
+        return text;
+    }
+    return result;
+  }
 };
 
 }
@@ -3261,6 +3306,16 @@
     m_impl->lexer_config.update_open_chars();
   }
 
+  /// Sets whether to remove the first newline after a block
+  void set_trim_blocks(bool trim_blocks) {
+    m_impl->lexer_config.trim_blocks = trim_blocks;
+  }
+
+  /// Sets whether to strip the spaces and tabs from the start of a line to a block
+  void set_lstrip_blocks(bool lstrip_blocks) {
+    m_impl->lexer_config.lstrip_blocks = lstrip_blocks;
+  }
+
   /// Sets the element notation syntax
   void set_element_notation(ElementNotation notation) {
     m_impl->parser_config.notation = notation;
diff --git a/test/data/nested-whitespace/data.json b/test/data/nested-whitespace/data.json
new file mode 100755
index 0000000..f7e4949
--- /dev/null
+++ b/test/data/nested-whitespace/data.json
@@ -0,0 +1,4 @@
+{
+    "xarray": [0, 1],
+    "yarray": [0, 1, 2]
+}
\ No newline at end of file
diff --git a/test/data/nested-whitespace/result.txt b/test/data/nested-whitespace/result.txt
new file mode 100755
index 0000000..109ebdb
--- /dev/null
+++ b/test/data/nested-whitespace/result.txt
@@ -0,0 +1,6 @@
+0-0
+0-1
+0-2
+1-0
+1-1
+1-2
diff --git a/test/data/nested-whitespace/template.txt b/test/data/nested-whitespace/template.txt
new file mode 100755
index 0000000..254dce2
--- /dev/null
+++ b/test/data/nested-whitespace/template.txt
@@ -0,0 +1,3 @@
+{% for x in xarray %}{% for y in yarray %}
+{{x}}-{{y}}
+{% endfor %}{% endfor %}
\ No newline at end of file
diff --git a/test/unit-files.cpp b/test/unit-files.cpp
index afd7614..0c6747d 100644
--- a/test/unit-files.cpp
+++ b/test/unit-files.cpp
@@ -41,6 +41,18 @@
 	}
 }
 
+TEST_CASE("complete-files-whitespace-control") {
+	inja::Environment env {test_file_directory};
+	env.set_trim_blocks(true);
+	env.set_lstrip_blocks(true);
+	
+	for (std::string test_name : {"nested-whitespace"}) {
+		SECTION(test_name) {
+			CHECK( env.render_file_with_json_file(test_name + "/template.txt", test_name + "/data.json") == env.load_file(test_name + "/result.txt") );
+		}
+	}
+}
+
 TEST_CASE("global-path") {
 	inja::Environment env {test_file_directory, "./"};
 	inja::Environment env_result {"./"};
diff --git a/test/unit-renderer.cpp b/test/unit-renderer.cpp
index 310eb41..1243470 100644
--- a/test/unit-renderer.cpp
+++ b/test/unit-renderer.cpp
@@ -7,8 +7,9 @@
 
 TEST_CASE("dot-to-pointer") {
 	std::string buffer;
-	CHECK( inja::convert_dot_to_json_pointer("person.names.surname", buffer) == "/person/names/surname" );
+	CHECK( inja::convert_dot_to_json_pointer("test", buffer) == "/test" );
 	CHECK( inja::convert_dot_to_json_pointer("guests.2", buffer) == "/guests/2" );
+	CHECK( inja::convert_dot_to_json_pointer("person.names.surname", buffer) == "/person/names/surname" );
 }
 
 TEST_CASE("types") {
@@ -172,7 +173,6 @@
 	SECTION("length") {
 		CHECK( env.render("{{ length(names) }}", data) == "4" ); // Length of array
 		CHECK( env.render("{{ length(name) }}", data) == "5" ); // Length of string
-
 		// CHECK_THROWS_WITH( env.render("{{ length(5) }}", data), "[inja.exception.json_error] [json.exception.type_error.302] type must be array, but is number" );
 	}