diff --git a/include/inja/config.hpp b/include/inja/config.hpp index 0d571b0..d7f16b9 100644 --- a/include/inja/config.hpp +++ b/include/inja/config.hpp @@ -24,6 +24,9 @@ struct LexerConfig { 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) { diff --git a/include/inja/environment.hpp b/include/inja/environment.hpp index a590c5f..b794e36 100644 --- a/include/inja/environment.hpp +++ b/include/inja/environment.hpp @@ -77,6 +77,16 @@ class Environment { 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/include/inja/lexer.hpp b/include/inja/lexer.hpp index 2ef6db0..ebad0f7 100644 --- a/include/inja/lexer.hpp +++ b/include/inja/lexer.hpp @@ -59,12 +59,15 @@ class Lexer { // 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; @@ -72,8 +75,13 @@ class Lexer { 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; @@ -100,7 +108,7 @@ class Lexer { 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); @@ -111,7 +119,10 @@ class Lexer { // 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; } } } @@ -119,7 +130,7 @@ class Lexer { 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); @@ -133,7 +144,10 @@ class Lexer { 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 @@ -254,6 +268,34 @@ class Lexer { 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; + } }; }