From 0bd14188165769565d53f2bfb5c2e41a2b3a6c9c Mon Sep 17 00:00:00 2001 From: Flex Ferrum Date: Wed, 22 Aug 2018 01:55:21 +0300 Subject: [PATCH] Implement 'macro' and 'call' statement (#35) * Begin implementation of macros * Implement 'macro' statement * Fix build * Implement 'call' statement * Add extra tests * Fix crash in tests --- CMakeLists.txt | 12 +- include/jinja2cpp/parse_result.h | 75 --------- src/expression_evaluator.cpp | 14 +- src/expression_evaluator.h | 21 +-- src/expression_parser.h | 2 +- src/helpers.h | 24 +++ src/lexer.h | 4 +- src/render_context.h | 5 +- src/statements.cpp | 109 ++++++++++++ src/statements.h | 58 ++++++- src/template_parser.cpp | 183 +++++++++++++++++++- src/template_parser.h | 13 +- test/errors_test.cpp | 42 ++++- test/macro_test.cpp | 275 +++++++++++++++++++++++++++++++ thirdparty/nonstd/expected-light | 2 +- 15 files changed, 718 insertions(+), 121 deletions(-) delete mode 100644 include/jinja2cpp/parse_result.h create mode 100644 test/macro_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ad62a1b..2d136d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,9 +44,14 @@ else () set (MSVC_RUNTIME_TYPE "/MD") endif () if (CMAKE_BUILD_TYPE MATCHES "Debug") + message("#######>>>>>>>>>>!!!!!!!!!!!!!! AAAAAAAAAAAAAAAAAAAAAAAAAAAA") set (CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${MSVC_RUNTIME_TYPE}d") + set (Boost_USE_DEBUG_RUNTIME ON) else () set (CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} ${MSVC_RUNTIME_TYPE}") + set (CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} ${MSVC_RUNTIME_TYPE}") + set (CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "/PROFILE") + set (Boost_USE_DEBUG_RUNTIME OFF) endif () endif() @@ -133,7 +138,7 @@ add_library(${LIB_TARGET_NAME} STATIC ${PublicHeaders} ) -target_link_libraries(${LIB_TARGET_NAME} PUBLIC ThirdParty::nonstd Boost::boost Boost::system Boost::filesystem) +target_link_libraries(${LIB_TARGET_NAME} PUBLIC ThirdParty::nonstd Boost::boost) # Boost::system Boost::filesystem) target_include_directories(${LIB_TARGET_NAME} INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) @@ -141,16 +146,13 @@ target_include_directories(${LIB_TARGET_NAME} if (WITH_TESTS) enable_testing() - message (STATUS "############# GTEST_INCLUDE_DIRS=${GTEST_INCLUDE_DIRS}") - message (STATUS "############# GTEST_BOTH_LIBRARIES=${GTEST_BOTH_LIBRARIES}") - include_directories( ${GTEST_INCLUDE_DIRS} ) CollectSources(TestSources TestHeaders ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/test) add_executable(jinja2cpp_tests ${TestSources} ${TestHeaders}) - target_link_libraries(jinja2cpp_tests ${GTEST_BOTH_LIBRARIES} ${LIB_TARGET_NAME} ${EXTRA_TEST_LIBS}) + target_link_libraries(jinja2cpp_tests ${GTEST_BOTH_LIBRARIES} ${LIB_TARGET_NAME} ${EXTRA_TEST_LIBS} ${Boost_LIBRARIES}) add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/test_data/simple_template1.j2tpl diff --git a/include/jinja2cpp/parse_result.h b/include/jinja2cpp/parse_result.h deleted file mode 100644 index 88bab0e..0000000 --- a/include/jinja2cpp/parse_result.h +++ /dev/null @@ -1,75 +0,0 @@ -#if 0 - -#ifndef JINJA2CPP_PARSE_RESULT_H -#define JINJA2CPP_PARSE_RESULT_H - -#include "error_info.h" - -#include - -#include - -namespace jinja2 -{ -template -class ParseResultTpl -{ -public: - using ThisType = ParseResultTpl; - using ErrorType = ErrorInfoTpl; - - ParseResultTpl() = default; - explicit ParseResultTpl(ErrorType&& error) - : m_error(std::move(error)) - { - } - - bool operator!() const - { - return !!m_error; - } - operator bool() const - { - return !m_error; - } - - bool HasError() const - { - return static_cast(m_error); - } - - auto GetError() const - { - return m_error.get_ptr(); - } - -private: - boost::optional m_error; - -}; - -using ParseResult = ParseResultTpl; -using ParseResultW = ParseResultTpl; - -inline std::ostream& operator << (std::ostream& os, const ParseResult& res) -{ - if (res) - os << "OK"; - else - os << *res.GetError(); - return os; -} - -inline std::wostream& operator << (std::wostream& os, const ParseResultW& res) -{ - if (res) - os << L"OK"; - else - os << *res.GetError(); - return os; -} - -} // jinja2 - -#endif // JINJA2CPP_PARSE_RESULT_H -#endif diff --git a/src/expression_evaluator.cpp b/src/expression_evaluator.cpp index 67da723..d5bb230 100644 --- a/src/expression_evaluator.cpp +++ b/src/expression_evaluator.cpp @@ -366,7 +366,8 @@ enum ParamState MappedKw, }; -ParsedArguments ParseCallParams(const std::initializer_list& args, const CallParams& params, bool& isSucceeded) +template +ParsedArguments ParseCallParamsImpl(const T& args, const CallParams& params, bool& isSucceeded) { struct ArgInfo { @@ -503,5 +504,16 @@ ParsedArguments ParseCallParams(const std::initializer_list& args, return result; } + +ParsedArguments ParseCallParams(const std::initializer_list& args, const CallParams& params, bool& isSucceeded) +{ + return ParseCallParamsImpl(args, params, isSucceeded); +} + +ParsedArguments ParseCallParams(const std::vector& args, const CallParams& params, bool& isSucceeded) +{ + return ParseCallParamsImpl(args, params, isSucceeded); +} + } } diff --git a/src/expression_evaluator.h b/src/expression_evaluator.h index 67c2489..fab4432 100644 --- a/src/expression_evaluator.h +++ b/src/expression_evaluator.h @@ -328,26 +328,7 @@ class IfExpression namespace helpers { ParsedArguments ParseCallParams(const std::initializer_list& argsInfo, const CallParams& params, bool& isSucceeded); - -//constexpr size_t NoPosParam = std::numeric_limits::max(); - -//inline bool FindParam(const CallParams& params, size_t pos, std::string paramName, ExpressionEvaluatorPtr<>& value) -//{ -// auto p = params.kwParams.find(paramName); -// if (p != params.kwParams.end()) -// { -// value = p->second; -// return true; -// } - -// if (pos < params.posParams.size()) -// { -// value = params.posParams[pos]; -// return true; -// } - -// return false; -//} +ParsedArguments ParseCallParams(const std::vector& args, const CallParams& params, bool& isSucceeded); } } // jinja2 diff --git a/src/expression_parser.h b/src/expression_parser.h index 23bf115..9ba0d90 100644 --- a/src/expression_parser.h +++ b/src/expression_parser.h @@ -19,6 +19,7 @@ class ExpressionParser ExpressionParser(); ParseResult Parse(LexScanner& lexer); ParseResult> ParseFullExpression(LexScanner& lexer, bool includeIfPart = true); + ParseResult ParseCallParams(LexScanner& lexer); private: ParseResult> ParseLogicalNot(LexScanner& lexer); ParseResult> ParseLogicalOr(LexScanner& lexer); @@ -34,7 +35,6 @@ class ExpressionParser ParseResult> ParseDictionary(LexScanner& lexer); ParseResult> ParseTuple(LexScanner& lexer); ParseResult> ParseCall(LexScanner& lexer, ExpressionEvaluatorPtr valueRef); - ParseResult ParseCallParams(LexScanner& lexer); ParseResult> ParseSubscript(LexScanner& lexer, ExpressionEvaluatorPtr valueRef); ParseResult> ParseFilterExpression(LexScanner& lexer); ParseResult> ParseIfExpression(LexScanner& lexer); diff --git a/src/helpers.h b/src/helpers.h index 63c79e3..d75ae2e 100644 --- a/src/helpers.h +++ b/src/helpers.h @@ -65,11 +65,23 @@ struct StringConverter { std::mbstate_t state = std::mbstate_t(); auto src = from.data(); +#ifndef _MSC_VER std::size_t len = 1 + std::wcsrtombs(nullptr, &src, 0, &state); +#else + std::size_t len = 0; + auto err = wcsrtombs_s(&len, nullptr, 0, &src, 0, &state); + if (err != 0) + return std::string(); + ++ len; +#endif std::string result; result.resize(len); src = from.data(); +#ifndef _MSC_VER std::wcsrtombs(&result[0], &src, from.size(), &state); +#else + wcsrtombs_s(&len, &result[0], len, &src, from.size(), &state); +#endif return result; } }; @@ -81,11 +93,23 @@ struct StringConverter { std::mbstate_t state = std::mbstate_t(); auto src = from.data(); +#ifndef _MSC_VER std::size_t len = 1 + std::mbsrtowcs(NULL, &src, 0, &state); +#else + std::size_t len = 0; + auto err = mbsrtowcs_s(&len, NULL, 0, &src, 0, &state); + if (err != 0) + return std::wstring(); + ++len; +#endif std::wstring result; result.resize(len); src = from.data(); +#ifndef _MSC_VER std::mbsrtowcs(&result[0], &src, result.size(), &state); +#else + mbsrtowcs_s(&len, &result[0], len, &src, result.size(), &state); +#endif return result; } }; diff --git a/src/lexer.h b/src/lexer.h index 724c3e1..426910b 100644 --- a/src/lexer.h +++ b/src/lexer.h @@ -93,8 +93,8 @@ struct Token ExprEnd, }; - Type type; - CharRange range; + Type type = Unknown; + CharRange range = {0, 0}; InternalValue value; bool IsEof() const diff --git a/src/render_context.h b/src/render_context.h index 0268785..988fa9e 100644 --- a/src/render_context.h +++ b/src/render_context.h @@ -52,8 +52,9 @@ class RenderContext { for (auto p = m_scopes.rbegin(); p != m_scopes.rend(); ++ p) { - auto valP = p->find(val); - if (valP != p->end()) + auto& map = *p; + auto valP = map.find(val); + if (valP != map.end()) { found = true; return valP; diff --git a/src/statements.cpp b/src/statements.cpp index 0ee711f..04c1027 100644 --- a/src/statements.cpp +++ b/src/statements.cpp @@ -307,4 +307,113 @@ void ExtendsStatement::Render(OutStream& os, RenderContext& values) renderer->Render(os, values); } +void MacroStatement::PrepareMacroParams(RenderContext& values) +{ + for (auto& p : m_params) + { + ArgumentInfo info(p.paramName, !p.defaultValue); + if (p.defaultValue) + info.defaultVal = p.defaultValue->Evaluate(values); + m_preparedParams.push_back(std::move(info)); + } +} + +void MacroStatement::Render(OutStream& os, RenderContext& values) +{ + PrepareMacroParams(values); + + values.GetCurrentScope()[m_name] = Callable([this](const CallParams& callParams, OutStream& stream, RenderContext& context) { + InvokeMacroRenderer(callParams, stream, context); + }); +} + +void MacroStatement::InvokeMacroRenderer(const CallParams& callParams, OutStream& stream, RenderContext& context) +{ + InternalValueMap callArgs; + InternalValueMap kwArgs; + InternalValueList varArgs; + + SetupCallArgs(m_preparedParams, callParams, context, callArgs, kwArgs, varArgs); + InternalValueList arguments; + InternalValueList defaults; + for (auto& a : m_preparedParams) + { + arguments.emplace_back(a.name); + defaults.emplace_back(a.defaultVal); + } + + auto& scope = context.EnterScope(); + for (auto& a : callArgs) + scope[a.first] = std::move(a.second); + + scope["kwargs"] = MapAdapter::CreateAdapter(std::move(kwArgs)); + scope["varargs"] = ListAdapter::CreateAdapter(std::move(varArgs)); + + scope["name"] = m_name; + scope["arguments"] = ListAdapter::CreateAdapter(std::move(arguments)); + scope["defaults"] = ListAdapter::CreateAdapter(std::move(defaults)); + + m_mainBody->Render(stream, context); + + context.ExitScope(); +} + +void MacroStatement::SetupCallArgs(const std::vector& argsInfo, const CallParams& callParams, RenderContext& context, InternalValueMap& callArgs, InternalValueMap& kwArgs, InternalValueList& varArgs) +{ + bool isSucceeded = true; + ParsedArguments args = helpers::ParseCallParams(argsInfo, callParams, isSucceeded); + + for (auto& a : args.args) + callArgs[a.first] = a.second->Evaluate(context); + + for (auto& a : args.extraKwArgs) + kwArgs[a.first] = a.second->Evaluate(context); + + for (auto& a : args.extraPosArgs) + varArgs.push_back(a->Evaluate(context)); +} + +void MacroStatement::SetupMacroScope(InternalValueMap& scope) +{ + ; +} + +void MacroCallStatement::Render(OutStream& os, RenderContext& values) +{ + bool isMacroFound = false; + auto macroPtr = values.FindValue(m_macroName, isMacroFound); + if (!isMacroFound) + return; + + auto& fnVal = macroPtr->second; + const Callable* callable = boost::get(&fnVal); + if (callable == nullptr || callable->GetType() == Callable::Type::Expression) + return; + + PrepareMacroParams(values); + auto& curScope = values.GetCurrentScope(); + auto callerP = curScope.find("caller"); + bool hasCallerVal = callerP != curScope.end(); + InternalValue prevCaller; + if (hasCallerVal) + prevCaller = callerP->second; + + curScope["caller"] = Callable([this](const CallParams& callParams, OutStream& stream, RenderContext& context) { + InvokeMacroRenderer(callParams, stream, context); + }); + + callable->GetStatementCallable()(m_callParams, os, values); + + if (hasCallerVal) + curScope["caller"] = prevCaller; + else + values.GetCurrentScope().erase("caller"); +} + +void MacroCallStatement::SetupMacroScope(InternalValueMap& scope) +{ + +} + + } // jinja2 diff --git a/src/statements.h b/src/statements.h index abb469d..d344036 100644 --- a/src/statements.h +++ b/src/statements.h @@ -19,6 +19,14 @@ using StatementPtr = std::shared_ptr; template class TemplateImpl; +struct MacroParam +{ + std::string paramName; + ExpressionEvaluatorPtr<> defaultValue; +}; + +using MacroParams = std::vector; + class ForStatement : public Statement { public: @@ -41,7 +49,7 @@ class ForStatement : public Statement } void Render(OutStream& os, RenderContext& values) override; - + private: void RenderLoop(const InternalValue& val, OutStream& os, RenderContext& values); @@ -186,6 +194,54 @@ class ExtendsStatement : public Statement BlocksCollection m_blocks; void DoRender(OutStream &os, RenderContext &values); }; + +class MacroStatement : public Statement +{ +public: + MacroStatement(std::string name, MacroParams params) + : m_name(std::move(name)) + , m_params(std::move(params)) + { + } + + void SetMainBody(RendererPtr renderer) + { + m_mainBody = renderer; + } + void Render(OutStream &os, RenderContext &values) override; + +protected: + void InvokeMacroRenderer(const CallParams& callParams, OutStream& stream, RenderContext& context); + void SetupCallArgs(const std::vector& argsInfo, const CallParams& callParams, RenderContext& context, InternalValueMap& callArgs, InternalValueMap& kwArgs, InternalValueList& varArgs); + virtual void SetupMacroScope(InternalValueMap& scope); + +protected: + std::string m_name; + MacroParams m_params; + std::vector m_preparedParams; + RendererPtr m_mainBody; + void PrepareMacroParams(RenderContext& values); +}; + +class MacroCallStatement : public MacroStatement +{ +public: + MacroCallStatement(std::string macroName, CallParams callParams, MacroParams callbackParams) + : MacroStatement("$call$", std::move(callbackParams)) + , m_macroName(std::move(macroName)) + , m_callParams(std::move(callParams)) + { + } + + void Render(OutStream &os, RenderContext &values) override; + +protected: + void SetupMacroScope(InternalValueMap& scope) override; + +protected: + std::string m_macroName; + CallParams m_callParams; +}; } // jinja2 #endif // STATEMENTS_H diff --git a/src/template_parser.cpp b/src/template_parser.cpp index 1a5dfb7..4445568 100644 --- a/src/template_parser.cpp +++ b/src/template_parser.cpp @@ -42,9 +42,17 @@ StatementsParser::ParseResult StatementsParser::Parse(LexScanner& lexer, Stateme result = ParseExtends(lexer, statementsInfo, tok); break; case jinja2::Token::Macro: + result = ParseMacro(lexer, statementsInfo, tok); + break; case jinja2::Token::EndMacro: + result = ParseEndMacro(lexer, statementsInfo, tok); + break; case jinja2::Token::Call: + result = ParseCall(lexer, statementsInfo, tok); + break; case jinja2::Token::EndCall: + result = ParseEndCall(lexer, statementsInfo, tok); + break; case jinja2::Token::Filter: case jinja2::Token::EndFilter: case jinja2::Token::EndSet: @@ -314,9 +322,6 @@ StatementsParser::ParseResult StatementsParser::ParseBlock(LexScanner& lexer, St if (nextTok != Token::Identifier) return MakeParseError(ErrorCode::ExpectedIdentifier, nextTok); - if (nextTok == Token::Eof) - lexer.ReturnToken(); - std::string blockName = AsString(nextTok.value); auto& info = statementsInfo.back(); @@ -360,10 +365,9 @@ StatementsParser::ParseResult StatementsParser::ParseEndBlock(LexScanner& lexer, Token nextTok = lexer.PeekNextToken(); if (nextTok != Token::Identifier && nextTok != Token::Eof) { - auto tok2 = nextTok; + Token tok2; tok2.type = Token::Identifier; - tok2.range.startOffset = tok2.range.endOffset; - auto tok3 = nextTok; + Token tok3; tok3.type = Token::Eof; return MakeParseError(ErrorCode::ExpectedToken, nextTok, {tok2, tok3}); } @@ -419,4 +423,171 @@ StatementsParser::ParseResult StatementsParser::ParseExtends(LexScanner& lexer, return ParseResult(); } +StatementsParser::ParseResult StatementsParser::ParseMacro(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok) +{ + if (statementsInfo.empty()) + return MakeParseError(ErrorCode::UnexpectedStatement, stmtTok); + + Token nextTok = lexer.NextToken(); + if (nextTok != Token::Identifier) + return MakeParseError(ErrorCode::ExpectedIdentifier, nextTok); + + std::string macroName = AsString(nextTok.value); + MacroParams macroParams; + + if (lexer.EatIfEqual('(')) + { + auto result = ParseMacroParams(lexer); + if (!result) + return result.get_unexpected(); + + macroParams = std::move(result.value()); + } + else if (lexer.PeekNextToken() != Token::Eof) + { + Token tok = lexer.PeekNextToken(); + Token tok1; + tok1.type = Token::RBracket; + Token tok2; + tok2.type = Token::Eof; + + return MakeParseError(ErrorCode::UnexpectedToken, tok, {tok1, tok2}); + } + + auto renderer = std::make_shared(std::move(macroName), std::move(macroParams)); + StatementInfo statementInfo = StatementInfo::Create(StatementInfo::MacroStatement, stmtTok); + statementInfo.renderer = renderer; + statementsInfo.push_back(statementInfo); + + return ParseResult(); +} + +nonstd::expected StatementsParser::ParseMacroParams(LexScanner& lexer) +{ + MacroParams items; + + if (lexer.EatIfEqual(')')) + return std::move(items); + + ExpressionParser exprParser; + + do + { + Token name = lexer.NextToken(); + if (name != Token::Identifier) + return MakeParseError(ErrorCode::ExpectedIdentifier, name); + + ExpressionEvaluatorPtr<> defVal; + if (lexer.EatIfEqual('=')) + { + auto result = exprParser.ParseFullExpression(lexer, false); + if (!result) + return result.get_unexpected(); + + defVal = *result; + } + + MacroParam p; + p.paramName = AsString(name.value); + p.defaultValue = defVal; + items.push_back(std::move(p)); + + } while (lexer.EatIfEqual(',')); + + auto tok = lexer.NextToken(); + if (tok != ')') + return MakeParseError(ErrorCode::ExpectedRoundBracket, tok); + + return std::move(items); +} + +StatementsParser::ParseResult StatementsParser::ParseEndMacro(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok) +{ + if (statementsInfo.size() <= 1) + return MakeParseError(ErrorCode::UnexpectedStatement, stmtTok); + + StatementInfo info = statementsInfo.back(); + + if (info.type != StatementInfo::MacroStatement) + { + 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(); +} + +StatementsParser::ParseResult StatementsParser::ParseCall(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok) +{ + if (statementsInfo.empty()) + return MakeParseError(ErrorCode::UnexpectedStatement, stmtTok); + + MacroParams callbackParams; + + if (lexer.EatIfEqual('(')) + { + auto result = ParseMacroParams(lexer); + if (!result) + return result.get_unexpected(); + + callbackParams = std::move(result.value()); + } + + Token nextTok = lexer.NextToken(); + if (nextTok != Token::Identifier) + { + Token tok = nextTok; + Token tok1; + tok1.type = Token::Identifier; + + return MakeParseError(ErrorCode::UnexpectedToken, tok, {tok1}); + } + + std::string macroName = AsString(nextTok.value); + + CallParams callParams; + if (lexer.EatIfEqual('(')) + { + ExpressionParser exprParser; + auto result = exprParser.ParseCallParams(lexer); + if (!result) + return result.get_unexpected(); + + callParams = std::move(result.value()); + } + + auto renderer = std::make_shared(std::move(macroName), std::move(callParams), std::move(callbackParams)); + StatementInfo statementInfo = StatementInfo::Create(StatementInfo::MacroCallStatement, stmtTok); + statementInfo.renderer = renderer; + statementsInfo.push_back(statementInfo); + + return ParseResult(); +} + +StatementsParser::ParseResult StatementsParser::ParseEndCall(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok) +{ + if (statementsInfo.size() <= 1) + return MakeParseError(ErrorCode::UnexpectedStatement, stmtTok); + + StatementInfo info = statementsInfo.back(); + + if (info.type != StatementInfo::MacroCallStatement) + { + 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 47cfe38..e2268ed 100644 --- a/src/template_parser.h +++ b/src/template_parser.h @@ -163,7 +163,9 @@ struct StatementInfo SetStatement, ExtendsStatement, BlockStatement, - ParentBlockStatement + ParentBlockStatement, + MacroStatement, + MacroCallStatement }; using ComposedPtr = std::shared_ptr; @@ -205,6 +207,11 @@ class StatementsParser ParseResult ParseBlock(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); ParseResult ParseEndBlock(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); ParseResult ParseExtends(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); + ParseResult ParseMacro(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); + nonstd::expected ParseMacroParams(LexScanner& lexer); + ParseResult ParseEndMacro(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); + ParseResult ParseCall(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); + ParseResult ParseEndCall(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); }; template @@ -290,7 +297,7 @@ class TemplateParser : public LexerHelper { CharRange range{0ULL, m_template->size()}; m_lines.push_back(LineInfo{range, 0}); - m_textBlocks.push_back(TextBlockInfo{range, m_template->front() == '#' ? TextBlockType::LineStatement : TextBlockType::RawText}); + m_textBlocks.push_back(TextBlockInfo{range, (!m_template->empty() && m_template->front() == '#') ? TextBlockType::LineStatement : TextBlockType::RawText}); return nonstd::expected>(); } @@ -408,7 +415,7 @@ class TemplateParser : public LexerHelper std::vector errors; TextBlockInfo* prevBlock = nullptr; StatementInfoList statementsStack; - StatementInfo root = StatementInfo::Create(StatementInfo::TemplateRoot, Token{Token::Unknown, {0, 0}, {}}, renderers); + StatementInfo root = StatementInfo::Create(StatementInfo::TemplateRoot, Token(), renderers); statementsStack.push_back(root); for (auto& origBlock : m_textBlocks) { diff --git a/test/errors_test.cpp b/test/errors_test.cpp index 54d2a02..e4ba756 100644 --- a/test/errors_test.cpp +++ b/test/errors_test.cpp @@ -174,10 +174,44 @@ INSTANTIATE_TEST_CASE_P(StatementsTest, ErrorsGenericTest, ::testing::Values( "noname.j2tpl:1:10: error: Identifier expected\n{% block 10 %}\n ---^-------"}, InputOutputPair{"{% block a scp %}", "noname.j2tpl:1:12: error: Unexpected token 'scp'. Expected: 'scoped'\n{% block a scp %}\n ---^-------"}, - InputOutputPair{"{% macro %}", - "noname.j2tpl:1:4: error: This feature has not been supported yet\n{% macro %}\n---^-------"}, - InputOutputPair{"{% call %}", - "noname.j2tpl:1:4: error: This feature has not been supported yet\n{% call %}\n---^-------"}, + InputOutputPair{"{% block somename %}{% endblock 10 %}", + "noname.j2tpl:1:33: error: Unexpected token '10'. Expected: '<>', '<>'\n{% block somename %}{% endblock 10 %}\n ---^-------"}, + InputOutputPair{"{% endblock %}{% endblock %}", + "noname.j2tpl:1:4: error: Unexpected statement: 'endblock'\n{% endblock %}{% endblock %}\n---^-------"}, + InputOutputPair{"{% macro 10 %}{% endmacro %}", + "noname.j2tpl:1:10: error: Identifier expected\n{% macro 10 %}{% endmacro %}\n ---^-------"}, + InputOutputPair{"{% macro name(10) %}{% endmacro %}", + "noname.j2tpl:1:15: error: Identifier expected\n{% macro name(10) %}{% endmacro %}\n ---^-------"}, + InputOutputPair{"{% macro name(=10) %}{% endmacro %}", + "noname.j2tpl:1:15: error: Identifier expected\n{% macro name(=10) %}{% endmacro %}\n ---^-------"}, + InputOutputPair{"{% macro name name1 %}{% endmacro %}", + "noname.j2tpl:1:15: error: Unexpected token: 'name1'\n{% macro name name1 %}{% endmacro %}\n ---^-------"}, + InputOutputPair{"{% macro name() name1 %}{% endmacro %}", + "noname.j2tpl:1:17: error: Expected end of statement, got: 'name1'\n{% macro name() name1 %}{% endmacro %}\n ---^-------"}, + InputOutputPair{"{% macro name(param) name1 %}{% endmacro %}", + "noname.j2tpl:1:22: error: Expected end of statement, got: 'name1'\n{% macro name(param) name1 %}{% endmacro %}\n ---^-------"}, + InputOutputPair{"{% macro name(param=*) %}{% endmacro %}", + "noname.j2tpl:1:21: error: Unexpected token: '*'\n{% macro name(param=*) %}{% endmacro %}\n ---^-------"}, + InputOutputPair{"{% block b %}{% endmacro %}", + "noname.j2tpl:1:17: error: Unexpected statement: 'endmacro'\n{% block b %}{% endmacro %}\n ---^-------"}, + InputOutputPair{"{% call 10 %}{% endcall %}", + "noname.j2tpl:1:9: error: Unexpected token: '10'\n{% call 10 %}{% endcall %}\n ---^-------"}, + InputOutputPair{"{% call name(=10) %}{% endcall %}", + "noname.j2tpl:1:14: error: Unexpected token: '='\n{% call name(=10) %}{% endcall %}\n ---^-------"}, + InputOutputPair{"{% call(10) %}{% endcall %}", + "noname.j2tpl:1:9: error: Identifier expected\n{% call(10) %}{% endcall %}\n ---^-------"}, + InputOutputPair{"{% call (=10) name %}{% endcall %}", + "noname.j2tpl:1:10: error: Identifier expected\n{% call (=10) name %}{% endcall %}\n ---^-------"}, + InputOutputPair{"{% call name name1 %}{% endcall %}", + "noname.j2tpl:1:14: error: Expected end of statement, got: 'name1'\n{% call name name1 %}{% endcall %}\n ---^-------"}, + InputOutputPair{"{% call name() name1 %}{% endcall %}", + "noname.j2tpl:1:16: error: Expected end of statement, got: 'name1'\n{% call name() name1 %}{% endcall %}\n ---^-------"}, + InputOutputPair{"{% call name(param) name1 %}{% endcall %}", + "noname.j2tpl:1:21: error: Expected end of statement, got: 'name1'\n{% call name(param) name1 %}{% endcall %}\n ---^-------"}, + InputOutputPair{"{% call name(param=*) %}{% endcall %}", + "noname.j2tpl:1:20: error: Unexpected token: '*'\n{% call name(param=*) %}{% endcall %}\n ---^-------"}, + InputOutputPair{"{% block b %}{% endcall %}", + "noname.j2tpl:1:17: error: Unexpected statement: 'endcall'\n{% block b %}{% endcall %}\n ---^-------"}, InputOutputPair{"{{}}", "noname.j2tpl:1:3: error: Unexpected token: '<>'\n{{}}\n--^-------"} )); diff --git a/test/macro_test.cpp b/test/macro_test.cpp new file mode 100644 index 0000000..123b243 --- /dev/null +++ b/test/macro_test.cpp @@ -0,0 +1,275 @@ +#include +#include + +#include "gtest/gtest.h" + +#include "jinja2cpp/template.h" +#include "test_tools.h" + +using namespace jinja2; + +TEST(MacroTest, SimpleMacro) +{ + std::string source = R"( +{% macro test %} +Hello World! +{% endmacro %} +{{ test() }}{{ test() }} +)"; + + Template tpl; + auto parseRes = tpl.Load(source); + EXPECT_TRUE(parseRes.has_value()); + if (!parseRes) + { + std::cout << parseRes.error() << std::endl; + return; + } + + std::string result = tpl.RenderAsString(PrepareTestData()); + std::cout << result << std::endl; + std::string expectedResult = R"( +Hello World! +Hello World! + +)"; + EXPECT_EQ(expectedResult, result); +} + +TEST(MacroTest, OneParamMacro) +{ + std::string source = R"( +{% macro test(param) %} +-->{{ param }}<-- +{% endmacro %} +{{ test('Hello') }}{{ test(param='World!') }} +)"; + + Template tpl; + auto parseRes = tpl.Load(source); + EXPECT_TRUE(parseRes.has_value()); + if (!parseRes) + { + std::cout << parseRes.error() << std::endl; + return; + } + + std::string result = tpl.RenderAsString(PrepareTestData()); + std::cout << result << std::endl; + std::string expectedResult = R"( +-->Hello<-- +-->World!<-- + +)"; + EXPECT_EQ(expectedResult, result); +} + +TEST(MacroTest, OneDefaultParamMacro) +{ + std::string source = R"( +{% macro test(param='Hello') %} +-->{{ param }}<-- +{% endmacro %} +{{ test() }}{{ test('World!') }} +)"; + + Template tpl; + auto parseRes = tpl.Load(source); + EXPECT_TRUE(parseRes.has_value()); + if (!parseRes) + { + std::cout << parseRes.error() << std::endl; + return; + } + + std::string result = tpl.RenderAsString(PrepareTestData()); + std::cout << result << std::endl; + std::string expectedResult = R"( +-->Hello<-- +-->World!<-- + +)"; + EXPECT_EQ(expectedResult, result); +} + +TEST(MacroTest, ClosureMacro) +{ + std::string source = R"( +{% macro test1(param) %}-->{{ param('Hello World') }}<--{% endmacro %} +{% macro test(param1) %} +{% set var='Some Value' %} +{% macro inner1(msg) %}{{var ~ param1}} -> {{msg}}{% endmacro %} +{% macro inner2(msg) %}{{msg | upper}}{% endmacro %} +-->{{ test1(inner1) }}<-- +-->{{ test1(inner2) }}<-- +{% endmacro %} +{{ test() }}{{ test('World!') }} +)"; + + Template tpl; + auto parseRes = tpl.Load(source); + EXPECT_TRUE(parseRes.has_value()); + if (!parseRes) + { + std::cout << parseRes.error() << std::endl; + return; + } + + std::string result = tpl.RenderAsString(PrepareTestData()); + std::cout << result << std::endl; + std::string expectedResult = R"( +-->-->Some Value -> Hello World<--<-- +-->-->HELLO WORLD<--<-- +-->-->Some ValueWorld! -> Hello World<--<-- +-->-->HELLO WORLD<--<-- + +)"; + EXPECT_EQ(expectedResult, result); +} + +TEST(MacroTest, MacroVariables) +{ + std::string source = R"( +{% macro test(param1='Hello', param2, param3='World') %} +name: {{ name }} +arguments: {{ arguments | pprint }} +defaults: {{ defaults | pprint }} +varargs: {{ varargs | pprint }} +kwargs: {{ kwargs | pprint }} +{% endmacro %} +{{ test(1, 2, param3=3, 4, extraValue=5, 6) }} +)"; + + Template tpl; + auto parseRes = tpl.Load(source); + EXPECT_TRUE(parseRes.has_value()); + if (!parseRes) + { + std::cout << parseRes.error() << std::endl; + return; + } + + std::string result = tpl.RenderAsString(PrepareTestData()); + std::cout << result << std::endl; + std::string expectedResult = R"( +name: test +arguments: ['param1', 'param2', 'param3'] +defaults: ['Hello', none, 'World'] +varargs: [4, 6] +kwargs: {'extraValue': 5} + +)"; + EXPECT_EQ(expectedResult, result); +} + +TEST(MacroTest, SimpleCallMacro) +{ + std::string source = R"( +{% macro test %} +Hello World! -> {{ caller() }} <- +{% endmacro %} +{% call test %}Message from caller{% endcall %} +)"; + + Template tpl; + auto parseRes = tpl.Load(source); + EXPECT_TRUE(parseRes.has_value()); + if (!parseRes) + { + std::cout << parseRes.error() << std::endl; + return; + } + + std::string result = tpl.RenderAsString(PrepareTestData()); + std::cout << result << std::endl; + std::string expectedResult = R"( +Hello World! -> Message from caller <- +)"; + EXPECT_EQ(expectedResult, result); +} + +TEST(MacroTest, CallWithParamsAndSimpleMacro) +{ + std::string source = R"( +{% macro test %} +-> {{ caller('Hello World' | upper) }} <- +{% endmacro %} +{% call(message) test %}{{ message }}{% endcall %} +)"; + + Template tpl; + auto parseRes = tpl.Load(source); + EXPECT_TRUE(parseRes.has_value()); + if (!parseRes) + { + std::cout << parseRes.error() << std::endl; + return; + } + + std::string result = tpl.RenderAsString(PrepareTestData()); + std::cout << result << std::endl; + std::string expectedResult = R"( +-> HELLO WORLD <- +)"; + EXPECT_EQ(expectedResult, result); +} + +TEST(MacroTest, CallWithParamsAndMacro) +{ + std::string source = R"( +{% macro test(msg) %} +{{ msg }} >>> -> {{ caller([msg]) }} <--> {{ caller([msg], 'upper') }} <- +{% endmacro %} +{% call(message, fName='lower') test('Hello World') %}{{ message | map(fName) | first }}{% endcall %} +)"; + + Template tpl; + auto parseRes = tpl.Load(source); + EXPECT_TRUE(parseRes.has_value()); + if (!parseRes) + { + std::cout << parseRes.error() << std::endl; + return; + } + + std::string result = tpl.RenderAsString(PrepareTestData()); + std::cout << result << std::endl; + std::string expectedResult = R"( +Hello World >>> -> hello world <--> HELLO WORLD <- +)"; + EXPECT_EQ(expectedResult, result); +} + +TEST(MacroTest, MacroCallVariables) +{ + std::string source = R"( +{% macro invoke() %}{{ caller(1, 2, param3=3, 4, extraValue=5, 6) }}{% endmacro %} +{% call (param1='Hello', param2, param3='World') invoke %} +name: {{ name }} +arguments: {{ arguments | pprint }} +defaults: {{ defaults | pprint }} +varargs: {{ varargs | pprint }} +kwargs: {{ kwargs | pprint }} +{% endcall %} +)"; + + Template tpl; + auto parseRes = tpl.Load(source); + EXPECT_TRUE(parseRes.has_value()); + if (!parseRes) + { + std::cout << parseRes.error() << std::endl; + return; + } + + std::string result = tpl.RenderAsString(PrepareTestData()); + std::cout << result << std::endl; + std::string expectedResult = R"( +name: $call$ +arguments: ['param1', 'param2', 'param3'] +defaults: ['Hello', none, 'World'] +varargs: [4, 6] +kwargs: {'extraValue': 5} +)"; + EXPECT_EQ(expectedResult, result); +} diff --git a/thirdparty/nonstd/expected-light b/thirdparty/nonstd/expected-light index fd516e2..a2359cf 160000 --- a/thirdparty/nonstd/expected-light +++ b/thirdparty/nonstd/expected-light @@ -1 +1 @@ -Subproject commit fd516e26146736ba573c06433cc1bc92fb27c157 +Subproject commit a2359cf61478d735a308e952b1c9840714f61386