diff --git a/src/expression_parser.h b/src/expression_parser.h index 43efd971..a8c98a59 100644 --- a/src/expression_parser.h +++ b/src/expression_parser.h @@ -41,7 +41,7 @@ class ExpressionParser ParseResult> ParseIfExpression(LexScanner& lexer); private: - ComposedRenderer* m_topLevelRenderer; + ComposedRenderer* m_topLevelRenderer = nullptr; }; } // jinja2 diff --git a/src/lexer.h b/src/lexer.h index f16f4c63..78ea48b4 100644 --- a/src/lexer.h +++ b/src/lexer.h @@ -86,6 +86,7 @@ struct Token Recursive, Scoped, With, + EndWith, Without, Ignore, Missing, @@ -164,6 +165,7 @@ enum class Keyword Recursive, Scoped, With, + EndWith, Without, Ignore, Missing, diff --git a/src/statements.cpp b/src/statements.cpp index b5a081c2..2f4095f0 100644 --- a/src/statements.cpp +++ b/src/statements.cpp @@ -639,4 +639,17 @@ void DoStatement::Render(OutStream& os, RenderContext& values) { m_expr->Evaluate(values); } + +void WithStatement::Render(OutStream& os, RenderContext& values) +{ + auto innerValues = values.Clone(true); + auto& scope = innerValues.EnterScope(); + + for (auto& var : m_scopeVars) + scope[var.first] = var.second->Evaluate(values); + + m_mainBody->Render(os, innerValues); + + innerValues.ExitScope(); +} } // jinja2 diff --git a/src/statements.h b/src/statements.h index bb7a3c85..2461b436 100644 --- a/src/statements.h +++ b/src/statements.h @@ -328,13 +328,34 @@ class DoStatement : public Statement public: VISITABLE_STATEMENT(); - DoStatement(ExpressionEvaluatorPtr <> expr) : m_expr(expr) {} + DoStatement(ExpressionEvaluatorPtr<> expr) : m_expr(expr) {} void Render(OutStream &os, RenderContext &values) override; private: ExpressionEvaluatorPtr<> m_expr; }; + +class WithStatement : public Statement +{ +public: + VISITABLE_STATEMENT(); + + void SetScopeVars(std::vector>> vars) + { + m_scopeVars = std::move(vars); + } + void SetMainBody(RendererPtr renderer) + { + m_mainBody = renderer; + } + + void Render(OutStream &os, RenderContext &values) override; + +private: + std::vector>> m_scopeVars; + RendererPtr m_mainBody; +}; } // jinja2 diff --git a/src/template_parser.cpp b/src/template_parser.cpp index 6dc95bed..bddd6f7b 100644 --- a/src/template_parser.cpp +++ b/src/template_parser.cpp @@ -67,6 +67,12 @@ StatementsParser::ParseResult StatementsParser::Parse(LexScanner& lexer, Stateme return MakeParseError(ErrorCode::ExtensionDisabled, tok); result = ParseDo(lexer, statementsInfo, tok); break; + case Keyword::With: + result = ParseWith(lexer, statementsInfo, tok); + break; + case Keyword::EndWith: + result = ParseEndWith(lexer, statementsInfo, tok); + break; case Keyword::Filter: case Keyword::EndFilter: case Keyword::EndSet: @@ -815,4 +821,63 @@ StatementsParser::ParseResult StatementsParser::ParseDo(LexScanner& lexer, State return jinja2::StatementsParser::ParseResult(); } +StatementsParser::ParseResult StatementsParser::ParseWith(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok) +{ + std::vector>> vars; + + ExpressionParser exprParser(m_settings); + while (lexer.PeekNextToken() == Token::Identifier) + { + auto nameTok = lexer.NextToken(); + if (!lexer.EatIfEqual('=')) + return MakeParseErrorTL(ErrorCode::ExpectedToken, lexer.PeekNextToken(), '='); + + auto expr = exprParser.ParseFullExpression(lexer); + if (!expr) + return expr.get_unexpected(); + auto valueExpr = *expr; + + vars.emplace_back(AsString(nameTok.value), valueExpr); + + if (!lexer.EatIfEqual(',')) + break; + } + + auto nextTok = lexer.PeekNextToken(); + if (vars.empty()) + return MakeParseError(ErrorCode::ExpectedIdentifier, nextTok); + + if (nextTok != Token::Eof) + return MakeParseErrorTL(ErrorCode::ExpectedToken, nextTok, Token::Eof, ','); + + auto renderer = std::make_shared(); + renderer->SetScopeVars(std::move(vars)); + StatementInfo statementInfo = StatementInfo::Create(StatementInfo::WithStatement, stmtTok); + statementInfo.renderer = renderer; + statementsInfo.push_back(statementInfo); + + return ParseResult(); +} + +StatementsParser::ParseResult StatementsParser::ParseEndWith(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok) +{ + if (statementsInfo.size() <= 1) + return MakeParseError(ErrorCode::UnexpectedStatement, stmtTok); + + StatementInfo info = statementsInfo.back(); + + if (info.type != StatementInfo::WithStatement) + { + return MakeParseError(ErrorCode::UnexpectedStatement, stmtTok); + } + + statementsInfo.pop_back(); + auto renderer = static_cast(info.renderer.get()); + renderer->SetMainBody(info.compositions[0]); + + statementsInfo.back().currentComposition->AddRenderer(info.renderer); + + return ParseResult(); +} + } diff --git a/src/template_parser.h b/src/template_parser.h index 9a5e3552..3351b240 100644 --- a/src/template_parser.h +++ b/src/template_parser.h @@ -49,7 +49,7 @@ template struct ParserTraitsBase { static Token::Type s_keywords[]; - static KeywordsInfo s_keywordsInfo[40]; + static KeywordsInfo s_keywordsInfo[41]; static std::unordered_map s_tokens; }; @@ -166,7 +166,8 @@ struct StatementInfo BlockStatement, ParentBlockStatement, MacroStatement, - MacroCallStatement + MacroCallStatement, + WithStatement }; using ComposedPtr = std::shared_ptr; @@ -224,6 +225,10 @@ class StatementsParser private: Settings m_settings; + + ParseResult ParseWith(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& token); + + ParseResult ParseEndWith(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); }; template @@ -781,7 +786,7 @@ class TemplateParser : public LexerHelper }; template -KeywordsInfo ParserTraitsBase::s_keywordsInfo[40] = { +KeywordsInfo ParserTraitsBase::s_keywordsInfo[41] = { {UNIVERSAL_STR("for"), Keyword::For}, {UNIVERSAL_STR("endfor"), Keyword::Endfor}, {UNIVERSAL_STR("in"), Keyword::In}, @@ -815,6 +820,7 @@ KeywordsInfo ParserTraitsBase::s_keywordsInfo[40] = { {UNIVERSAL_STR("recursive"), Keyword::Recursive}, {UNIVERSAL_STR("scoped"), Keyword::Scoped}, {UNIVERSAL_STR("with"), Keyword::With}, + {UNIVERSAL_STR("endwith"), Keyword::EndWith}, {UNIVERSAL_STR("without"), Keyword::Without}, {UNIVERSAL_STR("ignore"), Keyword::Ignore}, {UNIVERSAL_STR("missing"), Keyword::Missing}, @@ -881,6 +887,7 @@ std::unordered_map ParserTraitsBase::s_tokens = { {Token::Recursive, UNIVERSAL_STR("recursive")}, {Token::Scoped, UNIVERSAL_STR("scoped")}, {Token::With, UNIVERSAL_STR("with")}, + {Token::EndWith, UNIVERSAL_STR("endwith")}, {Token::Without, UNIVERSAL_STR("without")}, {Token::Ignore, UNIVERSAL_STR("ignore")}, {Token::Missing, UNIVERSAL_STR("missing")}, diff --git a/test/errors_test.cpp b/test/errors_test.cpp index c073beb9..b635a424 100644 --- a/test/errors_test.cpp +++ b/test/errors_test.cpp @@ -19,7 +19,7 @@ TEST_P(ErrorsGenericTest, Test) Template tpl; auto parseResult = tpl.Load(source); - EXPECT_FALSE(parseResult.has_value()); + ASSERT_FALSE(parseResult.has_value()); std::ostringstream errorDescr; errorDescr << parseResult.error(); @@ -39,7 +39,7 @@ TEST_P(ErrorsGenericExtensionsTest, Test) Template tpl(&env); auto parseResult = tpl.Load(source); - EXPECT_FALSE(parseResult.has_value()); + ASSERT_FALSE(parseResult.has_value()); std::ostringstream errorDescr; errorDescr << parseResult.error(); @@ -213,6 +213,7 @@ INSTANTIATE_TEST_CASE_P(StatementsTest_1, ErrorsGenericTest, ::testing::Values( InputOutputPair{"{% from 'foo' import bar with context, %}", "noname.j2tpl:1:38: error: Expected end of statement, got: ','\n{% from 'foo' import bar with context, %}\n ---^-------"} )); + INSTANTIATE_TEST_CASE_P(StatementsTest_2, ErrorsGenericTest, ::testing::Values( InputOutputPair{"{% block %}", "noname.j2tpl:1:10: error: Identifier expected\n{% block %}\n ---^-------"}, @@ -260,6 +261,24 @@ INSTANTIATE_TEST_CASE_P(StatementsTest_2, ErrorsGenericTest, ::testing::Values( "noname.j2tpl:1:17: error: Unexpected statement: 'endcall'\n{% block b %}{% endcall %}\n ---^-------"}, InputOutputPair{"{% do 'Hello World' %}", "noname.j2tpl:1:4: error: Extension disabled\n{% do 'Hello World' %}\n---^-------"}, + InputOutputPair{"{% with %}{% endif }", + "noname.j2tpl:1:9: error: Identifier expected\n{% with %}{% endif }\n ---^-------"}, + InputOutputPair{"{% with a %}{% endif }", + "noname.j2tpl:1:11: error: Unexpected token '<>'. Expected: '='\n{% with a %}{% endif }\n ---^-------"}, + InputOutputPair{"{% with a 42 %}{% endif }", + "noname.j2tpl:1:11: error: Unexpected token '42'. Expected: '='\n{% with a 42 %}{% endif }\n ---^-------"}, + InputOutputPair{"{% with a = %}{% endif }", + "noname.j2tpl:1:13: error: Unexpected token: '<>'\n{% with a = %}{% endif }\n ---^-------"}, + InputOutputPair{"{% with a = 42 b = 30 %}{% endif }", + "noname.j2tpl:1:16: error: Unexpected token 'b'. Expected: '<>', ','\n{% with a = 42 b = 30 %}{% endif }\n ---^-------"}, + InputOutputPair{"{% with a = 42, %}{% endif }", + "noname.j2tpl:1:22: error: Unexpected statement: 'endif'\n{% with a = 42, %}{% endif }\n ---^-------"}, +// FIXME: InputOutputPair{"{% with a = 42 %}", +// "noname.j2tpl:1:4: error: Extension disabled\n{% do 'Hello World' %}\n---^-------"}, + InputOutputPair{"{% with a = 42 %}{% endfor %}", + "noname.j2tpl:1:21: error: Unexpected statement: 'endfor'\n{% with a = 42 %}{% endfor %}\n ---^-------"}, + InputOutputPair{"{% set a = 42 %}{% endwith %}", + "noname.j2tpl:1:20: error: Unexpected statement: 'endwith'\n{% set a = 42 %}{% endwith %}\n ---^-------"}, InputOutputPair{"{{}}", "noname.j2tpl:1:3: error: Unexpected token: '<>'\n{{}}\n--^-------"} )); diff --git a/test/set_test.cpp b/test/set_test.cpp index 1384cd61..eeadebaf 100644 --- a/test/set_test.cpp +++ b/test/set_test.cpp @@ -5,6 +5,8 @@ #include "jinja2cpp/template.h" +#include "test_tools.h" + using namespace jinja2; TEST(SetTest, SimpleSetTest) @@ -131,3 +133,80 @@ world: World )"; EXPECT_EQ(expectedResult, result); } + +using WithTest = TemplateEnvFixture; + +TEST_F(WithTest, SimpleTest) +{ + auto result = Render(R"( +{% with inner = 42 %} +{{ inner }} +{%- endwith %} +)", {}); + EXPECT_EQ("\n42", result); +} + +TEST_F(WithTest, MultiVarsTest) +{ + auto result = Render(R"( +{% with inner1 = 42, inner2 = 'Hello World' %} +{{ inner1 }} +{{ inner2 }} +{%- endwith %} +)", {}); + EXPECT_EQ("\n42\nHello World", result); +} + +TEST_F(WithTest, ScopeTest1) +{ + auto result = Render(R"( +{{ outer }} +{% with inner = 42, outer = 'Hello World' %} +{{ inner }} +{{ outer }} +{%- endwith %} +{{ outer }} +)", {{"outer", "World Hello"}}); + EXPECT_EQ("\nWorld Hello\n42\nHello WorldWorld Hello\n", result); +} + +TEST_F(WithTest, ScopeTest2) +{ + auto result = Render(R"( +{{ outer }} +{% with outer = 'Hello World', inner = outer %} +{{ inner }} +{{ outer }} +{%- endwith %} +{{ outer }} +)", {{"outer", "World Hello"}}); + EXPECT_EQ("\nWorld Hello\nWorld Hello\nHello WorldWorld Hello\n", result); +} + +TEST_F(WithTest, ScopeTest3) +{ + auto result = Render(R"( +{{ outer }} +{% with outer = 'Hello World' %} +{% set inner = outer %} +{{ inner }} +{{ outer }} +{%- endwith %} +{{ outer }} +)", {{"outer", "World Hello"}}); + EXPECT_EQ("\nWorld Hello\nHello World\nHello WorldWorld Hello\n", result); +} + +TEST_F(WithTest, ScopeTest4) +{ + auto result = Render(R"( +{% with inner1 = 42 %} +{% set inner2 = outer %} +{{ inner1 }} +{{ inner2 }} +{%- endwith %} +>> {{ inner1 }} << +>> {{ inner2 }} << +)", {{"outer", "World Hello"}}); + EXPECT_EQ("\n42\nWorld Hello>> <<\n>> <<\n", result); +}