diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..36b586fb --- /dev/null +++ b/.clang-format @@ -0,0 +1,38 @@ +BasedOnStyle: Mozilla +IndentWidth: 4 +TabWidth: 8 +ColumnLimit: 160 +BreakBeforeBraces: Custom +BraceWrapping: + AfterControlStatement: true + AfterClass: true + AfterNamespace: true + AfterFunction: true + BeforeCatch: true + BeforeElse: true + AfterStruct: true +AccessModifierOffset: -4 +BinPackParameters: false +AlignAfterOpenBracket: AlwaysBreak +AlwaysBreakAfterReturnType: None +AlwaysBreakAfterDefinitionReturnType: None +AllowAllParametersOfDeclarationOnNextLine: false +ConstructorInitializerIndentWidth: 4 +NamespaceIndentation: None +PointerAlignment: Left +Standard: Cpp11 +UseTab: Never +AlignAfterOpenBracket: Align +PenaltyReturnTypeOnItsOwnLine: 0 +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^((<|")(shared)\/)' + Priority: 4 + - Regex: '^((<|")(components)\/)' + Priority: 3 + - Regex: '^(<.*\.(h|hpp|hxx)>)' + Priority: 2 + - Regex: '^".*' + Priority: 1 + - Regex: '^(<[\w]*>)' + Priority: 5 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..04e8819e --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,86 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '23 15 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'cpp' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + # - name: Autobuild + # uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Build + run: | + #!/bin/bash + set -ex + export BUILD_TARGET=all + export CMAKE_OPTS=-DCMAKE_VERBOSE_MAKEFILE=ON + export WORKSPACE=$GITHUB_WORKSPACE + if [ "${INPUT_COMPILER}" == "clang-12" ] ; then + export INPUT_BASE_FLAGS="-DJINJA2CPP_CXX_STANDARD=20" ; + fi + export EXTRA_FLAGS="${INPUT_BASE_FLAGS} ${INPUT_EXTRA_FLAGS}" + mkdir $BUILD_DIRECTORY && cd $BUILD_DIRECTORY + sudo chmod gou+rw -R $WORKSPACE + cmake $CMAKE_OPTS -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS=$CMAKE_CXX_FLAGS -DJINJA2CPP_DEPS_MODE=internal -DJINJA2CPP_BUILD_SHARED=ON $EXTRA_FLAGS $WORKSPACE && cmake --build . --config Release --target all -- -j4 + + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml new file mode 100644 index 00000000..769ad56e --- /dev/null +++ b/.github/workflows/linux-build.yml @@ -0,0 +1,92 @@ +name: CI-linux-build + + +on: + push: + branches: + - master + - main + paths-ignore: + - 'docs/**' + - '**.md' + pull_request: + branches: + - master + - main + paths-ignore: + - 'docs/**' + - '**.md' + +jobs: + linux-build: + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + max-parallel: 8 + matrix: + compiler: [g++-9, g++-10, g++-11, clang++-12, clang++-13, clang++-14] + base-flags: ["", -DJINJA2CPP_CXX_STANDARD=17] + build-config: [Release, Debug] + build-shared: [TRUE, FALSE] + + include: + - compiler: g++-9 + extra-flags: -DJINJA2CPP_STRICT_WARNINGS=OFF + - compiler: g++-10 + extra-flags: -DJINJA2CPP_STRICT_WARNINGS=OFF + - compiler: g++-11 + extra-flags: -DJINJA2CPP_STRICT_WARNINGS=OFF + + steps: + - uses: actions/checkout@v3 + - name: Setup environment + env: + INPUT_COMPILER: ${{ matrix.compiler }} + INPUT_BASE_FLAGS: ${{ matrix.base-flags }} + INPUT_BASE_CONFIG: ${{ matrix.build-config }} + INPUT_EXTRA_FLAGS: ${{ matrix.extra-flags }} + run: | + sudo apt-get update + sudo apt-get install -y cmake build-essential ${INPUT_COMPILER} + - name: Prepare build + env: + INPUT_COMPILER: ${{ matrix.compiler }} + INPUT_BASE_FLAGS: ${{ matrix.base-flags }} + INPUT_BASE_CONFIG: ${{ matrix.build-config }} + INPUT_EXTRA_FLAGS: ${{ matrix.extra-flags }} + run: | + set -ex + export BUILD_TARGET=all + export CMAKE_OPTS=-DCMAKE_VERBOSE_MAKEFILE=OFF + if [[ "${INPUT_COMPILER}" != "" ]]; then export CXX=${INPUT_COMPILER}; fi + export BUILD_CONFIG=${INPUT_BASE_CONFIG} + $CXX --version + export EXTRA_FLAGS="${INPUT_BASE_FLAGS} ${INPUT_EXTRA_FLAGS}" + + - name: Build + env: + INPUT_BASE_CONFIG: ${{ matrix.build-config }} + INPUT_BASE_FLAGS: ${{ matrix.base-flags }} + INPUT_BUILD_SHARED: ${{ matrix.build-shared }} + INPUT_COMPILER: ${{ matrix.compiler }} + INPUT_EXTRA_FLAGS: ${{ matrix.extra-flags }} + run: | + set -ex + export BUILD_TARGET=all + export CMAKE_OPTS=-DCMAKE_VERBOSE_MAKEFILE=ON + if [[ "${INPUT_COMPILER}" != "" ]]; then export CXX=${INPUT_COMPILER}; fi + export BUILD_CONFIG=${INPUT_BASE_CONFIG} + $CXX --version + cmake --version + export EXTRA_FLAGS="${INPUT_BASE_FLAGS} ${INPUT_EXTRA_FLAGS}" + mkdir -p .build && cd .build + cmake $CMAKE_OPTS -DCMAKE_BUILD_TYPE=$BUILD_CONFIG -DCMAKE_CXX_FLAGS=$CMAKE_CXX_FLAGS -DJINJA2CPP_DEPS_MODE=internal -DJINJA2CPP_STRICT_WARNINGS=OFF -DJINJA2CPP_BUILD_SHARED=$INPUT_BUILD_SHARED $EXTRA_FLAGS .. && cmake --build . --config $BUILD_CONFIG --target all -- -j4 + + - name: Test + env: + BUILD_CONFIG: ${{ matrix.build-config }} + run: | + cd .build && ctest -C $BUILD_CONFIG -V + diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml new file mode 100644 index 00000000..b7a4635f --- /dev/null +++ b/.github/workflows/windows-build.yml @@ -0,0 +1,80 @@ +name: CI-windows-build + +on: + push: + branches: + - master + - main + paths-ignore: + - 'docs/**' + - '**.md' + pull_request: + branches: + - master + - main + paths-ignore: + - 'docs/**' + - '**.md' + +jobs: + windows-msvc-build: + + runs-on: ${{matrix.run-machine}} + + strategy: + fail-fast: false + max-parallel: 20 + matrix: + compiler: [msvc-2019] + base-flags: ["", -DJINJA2CPP_CXX_STANDARD=17] + build-config: [Release, Debug] + build-platform: [x86, x64] + build-runtime: ["", /MT, /MD] + build-shared: [FALSE, TRUE] + + include: + - compiler: msvc-2019 + build-platform: x86 + run-machine: windows-2019 + generator: Visual Studio 16 2019 + vc_vars: C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars32.bat + - compiler: msvc-2019 + build-platform: x64 + run-machine: windows-2019 + generator: Visual Studio 16 2019 + vc_vars: C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat + + + steps: + - uses: actions/checkout@v3 + + - name: Build + shell: cmd + env: + INPUT_COMPILER: ${{ matrix.compiler }} + INPUT_BASE_FLAGS: ${{ matrix.base-flags }} + INPUT_BUILD_CONFIG: ${{ matrix.build-config }} + INPUT_BUILD_SHARED: ${{ matrix.build-shared }} + INPUT_EXTRA_FLAGS: ${{ matrix.extra_flags }} + INPUT_BUILD_PLATFORM: ${{ matrix.build-platform }} + INPUT_BUILD_RUNTIME: ${{ matrix.build-runtime }} + INPUT_GENERATOR: ${{ matrix.generator }} + VC_VARS: "${{ matrix.vc_vars }}" + run: | + call "%VC_VARS%" + mkdir -p .build + cd .build + cmake --version + cmake .. -G "%INPUT_GENERATOR%" -DCMAKE_BUILD_TYPE=%INPUT_BUILD_CONFIG% -DJINJA2CPP_MSVC_RUNTIME_TYPE="%INPUT_BUILD_RUNTIME%" -DJINJA2CPP_DEPS_MODE=internal -DJINJA2CPP_BUILD_SHARED=%INPUT_BUILD_SHARED% %INPUT_BASE_FLAGS% %INPUT_EXTRA_FLAGS% + cmake --build . --config %INPUT_BUILD_CONFIG% --verbose + + - name: Test + shell: cmd + env: + INPUT_BUILD_CONFIG: ${{ matrix.build-config }} + VC_VARS: "${{ matrix.vc_vars }}" + run: | + cd .build + call "%VC_VARS%" + set path=%BOOST_ROOT%\lib;%PATH% + ctest -C %INPUT_BUILD_CONFIG% -V diff --git a/.gitignore b/.gitignore index 5a434454..8d1483f7 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,9 @@ *.out *.app +.* + build/ dist/ +compile_commands.json diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e4355fec..00000000 --- a/.gitmodules +++ /dev/null @@ -1,9 +0,0 @@ -[submodule "thirdparty/gtest"] - path = thirdparty/gtest - url = https://github.com/google/googletest.git -[submodule "thirdparty/nonstd/variant-light"] - path = thirdparty/nonstd/variant-light - url = https://github.com/flexferrum/variant-lite.git -[submodule "thirdparty/nonstd/expected-light"] - path = thirdparty/nonstd/expected-light - url = https://github.com/martinmoene/expected-lite.git diff --git a/.travis.yml b/.travis.yml index f10fbc5e..ce1080b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,89 +1,57 @@ +--- +dist: xenial language: cpp - -dist: trusty sudo: required matrix: include: - - os: linux - compiler: gcc - env: COMPILER=g++-5 - addons: - apt: - sources: ['ubuntu-toolchain-r-test'] - packages: ['cmake', 'g++-5'] - - - os: linux - compiler: gcc - env: - COMPILER=g++-6 - addons: - apt: - sources: ['ubuntu-toolchain-r-test'] - packages: ['cmake', 'g++-6'] - - - os: linux - compiler: gcc - env: - COMPILER=g++-6 - CMAKE_OPTS="-DCOVERAGE_ENABLED=TRUE" - BUILD_CONFIG=Debug + - + compiler: clang + env: COMPILER='clang++' + os: osx + osx_image: xcode9 + - + compiler: clang + env: COMPILER='clang++' + os: osx + osx_image: xcode10 + - + compiler: clang + env: COMPILER='clang++' + os: osx + osx_image: xcode11 + - addons: apt: - sources: ['ubuntu-toolchain-r-test'] - packages: ['cmake', 'g++-6', 'lcov'] - - - os: linux + packages: + - cmake + - g++-6 + - lcov + sources: + - ubuntu-toolchain-r-test compiler: gcc - env: COMPILER=g++-7 + env: "COMPILER=g++-6 COLLECT_COVERAGE=1" + os: linux + - addons: apt: - sources: ['ubuntu-toolchain-r-test'] - packages: ['cmake', 'g++-7'] - - - os: linux + packages: + - cmake + - clang-8 + - g++-8 + sources: + - ubuntu-toolchain-r-test + - llvm-toolchain-xenial-8 compiler: clang - env: COMPILER=clang++-5.0 - addons: - apt: - sources: ['ubuntu-toolchain-r-test', 'llvm-toolchain-trusty-5.0'] - packages: ['cmake', 'clang-5.0', 'g++-6'] - - - os: linux - compiler: clang - env: COMPILER=clang++-6.0 - addons: - apt: - sources: ['ubuntu-toolchain-r-test', 'llvm-toolchain-trusty-6.0'] - packages: ['cmake', 'clang-6.0', 'g++-6'] - + env: "COMPILER=clang++-8 EXTRA_FLAGS=-DJINJA2CPP_CXX_STANDARD=17 SANITIZE_BUILD=address+undefined" + os: linux before_install: - - date -u - - uname -a - - sudo add-apt-repository -y ppa:samuel-bachmann/boost - - sudo apt-get update -qq - -install: - - sudo apt-get install libboost1.60-all-dev - -script: - - if [[ "${COMPILER}" != "" ]]; then export CXX=${COMPILER}; fi - - if [[ "${BUILD_CONFIG}" == "" ]]; then export BUILD_CONFIG="Release"; fi - - uname -a - - $CXX --version - - - mkdir -p build && cd build - - cmake $CMAKE_OPTS -DCMAKE_BUILD_TYPE=$BUILD_CONFIG .. && cmake --build . --config $BUILD_CONFIG -- -j4 - - ctest -C $BUILD_CONFIG -V - - + - "date -u" + - "uname -a" +script: ./scripts/build.sh after_success: - # Creating report - - echo "Uploading code coverate report" - - cd build - - lcov --directory . --capture --output-file coverage.info # capture coverage info - - lcov --remove coverage.info '/usr/*' --output-file coverage.info # filter out system - - lcov --list coverage.info #debug info - # Uploading report to CodeCov - - bash <(curl -s https://codecov.io/bash) -t "225d6d7a-2b71-4dbe-bf87-fbf75eb7c119" || echo "Codecov did not collect coverage reports" - - fi + - "if [[ \"${COLLECT_COVERAGE}\" != \"\" ]]; then echo \"Uploading code coverate report\" ; fi" + - "if [[ \"${COLLECT_COVERAGE}\" != \"\" ]]; then lcov --directory . --capture --output-file coverage.info ; fi" + - "if [[ \"${COLLECT_COVERAGE}\" != \"\" ]]; then lcov --remove coverage.info '/usr/*' --output-file coverage.info ; fi" + - "if [[ \"${COLLECT_COVERAGE}\" != \"\" ]]; then lcov --list coverage.info ; fi" + - "if [[ \"${COLLECT_COVERAGE}\" != \"\" ]]; then bash <(curl -s https://codecov.io/bash) -t \"225d6d7a-2b71-4dbe-bf87-fbf75eb7c119\" || echo \"Codecov did not collect coverage reports\"; fi" diff --git a/CMakeLists.txt b/CMakeLists.txt index 96df6f62..4aabcff5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,178 +1,356 @@ -cmake_minimum_required(VERSION 3.0) -project(Jinja2Cpp VERSION 0.5.0) +cmake_minimum_required(VERSION 3.23.0) -list (APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) +if(POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) +endif() + +project(Jinja2Cpp VERSION 1.3.2) -if(CMAKE_COMPILER_IS_GNUCXX AND COVERAGE_ENABLED) - message (STATUS "This is DEBUG build with enabled Code Coverage") - set (CMAKE_BUILD_TYPE Debug) - include(code_coverage) - setup_target_for_coverage(${PROJECT_NAME}_coverage jinja2cpp_tests coverage) +if("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}") + set(JINJA2CPP_IS_MAIN_PROJECT TRUE) +else() + set(JINJA2CPP_IS_MAIN_PROJECT FALSE) endif() -set(GTEST_ROOT $ENV{GTEST_DIR} CACHE PATH "Path to GTest/GMock library root") -if (NOT UNIX) - set(BOOST_ROOT $ENV{BOOST_DIR} CACHE PATH "Path to boost library root") - set(LIBRARY_TYPE STATIC CACHE PATH "Library link type") -else () - set(BOOST_ROOT "/usr" CACHE PATH "Path to boost library root") - set(LIBRARY_TYPE SHARED CACHE PATH "Library link type") -endif () +# Options +set(JINJA2CPP_SANITIZERS address+undefined memory) +set(JINJA2CPP_WITH_SANITIZERS none CACHE STRING "Build with sanitizer") +set_property(CACHE JINJA2CPP_WITH_SANITIZERS PROPERTY STRINGS ${JINJA2CPP_SANITIZERS}) +set(JINJA2CPP_SUPPORTED_REGEX std boost) +set(JINJA2CPP_USE_REGEX boost CACHE STRING "Use regex parser in lexer, boost works faster on most platforms") +set_property(CACHE JINJA2CPP_USE_REGEX PROPERTY STRINGS ${JINJA2CPP_SUPPORTED_REGEX}) +set(JINJA2CPP_WITH_JSON_BINDINGS boost nlohmann rapid all none) +set(JINJA2CPP_WITH_JSON_BINDINGS boost CACHE STRING "Build with json support(boost|rapid)") +set_property(CACHE JINJA2CPP_WITH_JSON_BINDINGS PROPERTY STRINGS ${JINJA2CPP_WITH_JSON_BINDINGS}) +set (JINJA2CPP_DEPS_MODE "internal" CACHE STRING "Jinja2Cpp dependency management mode (internal | external | external-boost | conan-build). See documentation for details. 'interal' is default.") +option(JINJA2CPP_BUILD_TESTS "Build Jinja2Cpp unit tests" ${JINJA2CPP_IS_MAIN_PROJECT}) +option(JINJA2CPP_STRICT_WARNINGS "Enable additional warnings and treat them as errors" ON) +option(JINJA2CPP_BUILD_SHARED "Build shared linkage version of Jinja2Cpp" OFF) +option(JINJA2CPP_PIC "Control -fPIC option for library build" OFF) +option(JINJA2CPP_VERBOSE "Add extra debug output to the build scripts" OFF) +option(JINJA2CPP_INSTALL "Add installation rules for JinjaCpp targets" ${JINJA2CPP_IS_MAIN_PROJECT}) -if (NOT DEFINED WITH_TESTS) - set (WITH_TESTS TRUE) +if (DEFINED BUILD_SHARED_LIBS) + set(JINJA2CPP_BUILD_SHARED ${BUILD_SHARED_LIBS}) endif () -set (EXTRA_TEST_LIBS "") -set (CMAKE_CXX_STANDARD 14) -set (CMAKE_CXX_STANDARD_REQUIRED ON) -if (${CMAKE_CXX_COMPILER_ID} MATCHES "Clang" OR ${CMAKE_CXX_COMPILER_ID} MATCHES "GNU") - if (NOT UNIX) - set (EXTRA_TEST_LIBS "stdc++") - set (CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} "-Wa,-mbig-obj") - else () - include(CMakeFindDependencyMacro) - find_dependency(Threads) - set (EXTRA_TEST_LIBS Threads::Threads) - endif () -else () - # MSVC - if (NOT DEFINED Boost_USE_STATIC_LIBS) - set (Boost_USE_STATIC_LIBS ON) - endif () - if (NOT DEFINED MSVC_RUNTIME_TYPE) - 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() - -if("${GTEST_ROOT}" STREQUAL "" AND WITH_TESTS) - set (THIRDPARTY_TARGETS ${THIRDPARTY_TARGETS} gtest) -endif() +if (JINJA2CPP_BUILD_SHARED) + set(JINJA2CPP_PIC ON) + set(JINJA2CPP_MSVC_RUNTIME_TYPE "/MD") +endif () -if(NOT "${BOOST_ROOT}" STREQUAL "") - list (APPEND CMAKE_PREFIX_PATH ${BOOST_ROOT}) - set (Boost_DIR ${BOOST_ROOT}) -endif() +if (NOT JINJA2CPP_DEPS_MODE) + set(JINJA2CPP_DEPS_MODE "internal") +endif () -make_directory (${CMAKE_CURRENT_BINARY_DIR}/thirdparty) -execute_process ( - COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER} -DCMAKE_C_COMPILER=${CMAKE_C_COMPILER} -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS} ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/thirdparty - RESULT_VARIABLE THIRDPARTY_BUILD_RESULT - ) +if (JINJA2CPP_IS_MAIN_PROJECT OR NOT CMAKE_CXX_STANDARD) + set(JINJA2CPP_CXX_STANDARD 14 CACHE STRING "Jinja2Cpp C++ standard to build with. C++14 is default") + set(CMAKE_CXX_STANDARD ${JINJA2CPP_CXX_STANDARD}) +endif () -if (THIRDPARTY_BUILD_RESULT EQUAL 0 AND THIRDPARTY_TARGETS) - execute_process ( - COMMAND ${CMAKE_COMMAND} --build . --target ${THIRDPARTY_TARGETS} - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/thirdparty - RESULT_VARIABLE THIRDPARTY_BUILD_RESULT - ) +if (NOT JINJA2CPP_CXX_STANDARD) + set (JINJA2CPP_CXX_STANDARD ${CMAKE_CXX_STANDARD}) endif () -if (NOT THIRDPARTY_BUILD_RESULT EQUAL 0) - message (FATAL_ERROR "Can't build thirdparty libraries") +if (JINJA2CPP_CXX_STANDARD LESS 14) + message(FATAL_ERROR "Jinja2Cpp is required C++14 or greater standard set. Currently selected standard: ${JINJA2CPP_CXX_STANDARD}") +else () + message(STATUS "Jinja2Cpp C++ standard: ${JINJA2CPP_CXX_STANDARD}") endif () -if("${GTEST_ROOT}" STREQUAL "" AND WITH_TESTS) - set (GTEST_ROOT ${CMAKE_CURRENT_BINARY_DIR}/thirdparty/gtest/install) - set (GTEST_SELF_BUILD ON) +include(CMakePackageConfigHelpers) +include(GNUInstallDirs) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +set(CLANG_CXX_FLAGS) +set(GCC_CXX_FLAGS) +set(MSVC_CXX_FLAGS) + +if (JINJA2CPP_WITH_COVERAGE) + message(STATUS "This is DEBUG build with enabled Code Coverage") + set(CMAKE_BUILD_TYPE Debug) + set(JINJA2CPP_COVERAGE_TARGET "jinja2cpp_build_coverage") + include(coverage) + add_coverage_target("${JINJA2CPP_COVERAGE_TARGET}") endif () -if(NOT "${GTEST_ROOT}" STREQUAL "" AND WITH_TESTS) - list (APPEND CMAKE_PREFIX_PATH ${GTEST_ROOT}) - set (Gtest_DIR ${GTEST_ROOT}) - message(STATUS "GTest library search path: ${Gtest_DIR}") - if (NOT GTEST_SELF_BUILD) - find_package(GTest) - else () - set (Gtest_DIR ${GTEST_ROOT}) - find_package(GTest) +if(NOT ${JINJA2CPP_WITH_SANITIZERS} STREQUAL "none") + message (STATUS "Build with sanitizers enabled: ${JINJA2CPP_WITH_SANITIZERS}") + set(_chosen_san) + list(FIND JINJA2CPP_SANITIZERS ${JINJA2CPP_WITH_SANITIZERS} _chosen_san) + if (${_chosen_san} EQUAL -1) + message(FATAL_ERROR "Wrong sanitizer type has been chosen, must be one of ${JINJA2CPP_SANITIZERS}") + endif() + + include("sanitizer.${JINJA2CPP_WITH_SANITIZERS}") + set (JINJA2CPP_SANITIZE_TARGET "jinja2cpp_build_sanitizers") + add_sanitizer_target(${JINJA2CPP_SANITIZE_TARGET}) +endif() + +if (UNIX) + if (JINJA2CPP_PIC OR CONAN_CMAKE_POSITION_INDEPENDENT_CODE) + add_compile_options(-fPIC) + endif () + + if (DEFINED CONAN_SHARED_LINKER_FLAGS) + set(GCC_CXX_FLAGS ${GCC_CXX_FLAGS} -Wl,${CONAN_SHARED_LINKER_FLAGS}) + set(CLANG_CXX_FLAGS ${CLANG_CXX_FLAGS} -Wl,${CONAN_SHARED_LINKER_FLAGS}) + endif () +else () + set(GCC_CXX_FLAGS ${GCC_CXX_FLAGS} "-Wa,-mbig-obj" -O1) + if (CMAKE_CXX_COMPILER_ID MATCHES "GNU") + add_definitions(-D_CRT_SECURE_NO_WARNINGS) + endif () + add_definitions(-DBOOST_ALL_NO_LIB) + set(MSVC_CXX_FLAGS ${MSVC_CXX_FLAGS} /wd4503 /bigobj) + + if (CMAKE_BUILD_TYPE MATCHES "Debug" AND JINJA2CPP_MSVC_RUNTIME_TYPE) + set(JINJA2CPP_MSVC_RUNTIME_TYPE "${JINJA2CPP_MSVC_RUNTIME_TYPE}d") endif () - if (NOT GTEST_INCLUDE_DIRS) - if (MSVC AND NOT GTEST_INCLUDE_DIRS) - set (GTEST_MSVC_SEARCH "MT") - find_package(GTest) + if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") + if (JINJA2CPP_DEPS_MODE MATCHES "conan-build" OR CMAKE_BUILD_TYPE STREQUAL "") + if (NOT JINJA2CPP_MSVC_RUNTIME_TYPE STREQUAL "") + set(MSVC_RUNTIME_DEBUG ${JINJA2CPP_MSVC_RUNTIME_TYPE}) + set(MSVC_RUNTIME_RELEASE ${JINJA2CPP_MSVC_RUNTIME_TYPE}) + endif () + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${MSVC_RUNTIME_DEBUG}") + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} ${MSVC_RUNTIME_RELEASE}") + set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} ${MSVC_RUNTIME_RELEASE}") + set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "/PROFILE") + set(Boost_USE_DEBUG_RUNTIME OFF) + elseif (CMAKE_BUILD_TYPE MATCHES "Debug") + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${JINJA2CPP_MSVC_RUNTIME_TYPE}") + set(Boost_USE_DEBUG_RUNTIME ON) else () - set (GTEST_BOTH_LIBRARIES "optimized;${GTEST_ROOT}/lib/libgtest.a;debug;${GTEST_ROOT}/lib/libgtestd.a;optimized;${GTEST_ROOT}/lib/libgtest_main.a;debug;${GTEST_ROOT}/lib/libgtest_maind.a") - set (GTEST_INCLUDE_DIRS ${GTEST_ROOT}/include) + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} ${JINJA2CPP_MSVC_RUNTIME_TYPE}") + set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} ${JINJA2CPP_MSVC_RUNTIME_TYPE}") + set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "/PROFILE") + set(Boost_USE_DEBUG_RUNTIME OFF) endif () + message(STATUS "Selected MSVC runtime type for Jinja2C++ library: '${JINJA2CPP_MSVC_RUNTIME_TYPE}'") endif () endif() -if (WITH_TESTS) - find_package(Boost COMPONENTS system filesystem REQUIRED) -else () - find_package(Boost) -endif () -add_subdirectory (thirdparty/nonstd) +if (JINJA2CPP_BUILD_SHARED) + set(LIB_LINK_TYPE SHARED) + message(STATUS "Jinja2C++ library type: SHARED") +else() + set(LIB_LINK_TYPE STATIC) + message(STATUS "Jinja2C++ library type: STATIC") +endif() include(collect_sources) -include_directories( - ${CMAKE_CURRENT_SOURCE_DIR}/include - ) - set (LIB_TARGET_NAME jinja2cpp) CollectSources(Sources Headers ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/src) CollectSources(PublicSources PublicHeaders ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/include) -if(CMAKE_COMPILER_IS_GNUCXX AND COVERAGE_ENABLED) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage") -endif() - -add_library(${LIB_TARGET_NAME} STATIC +add_library(${LIB_TARGET_NAME} ${LIB_LINK_TYPE} ${Sources} ${Headers} ${PublicHeaders} +) + +target_sources(${LIB_TARGET_NAME} + PUBLIC FILE_SET HEADERS + FILES ${PublicHeaders} + BASE_DIRS include +) + +string(TOUPPER "${CMAKE_BUILD_TYPE}" BUILD_CFG_NAME) +set(CURRENT_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${CMAKE_CXX_FLAGS_${BUILD_CFG_NAME}}") + +set(JINJA2CPP_EXTRA_LIBS "" CACHE STRING "You can pass some libs that could used during link stage") +set(JINJA2CPP_PUBLIC_LIBS "${JINJA2CPP_EXTRA_LIBS}") +separate_arguments(JINJA2CPP_PUBLIC_LIBS) +if (JINJA2CPP_WITH_COVERAGE) + target_compile_options( + ${JINJA2CPP_COVERAGE_TARGET} + INTERFACE + -g -O0 ) + list(APPEND JINJA2CPP_PUBLIC_LIBS ${JINJA2CPP_COVERAGE_TARGET}) +endif() +if (NOT JINJA2CPP_WITH_SANITIZERS STREQUAL "none") + target_compile_options( + ${JINJA2CPP_SANITIZE_TARGET} + INTERFACE + -g -O2 + ) + list(APPEND JINJA2CPP_PUBLIC_LIBS ${JINJA2CPP_SANITIZE_TARGET}) +endif() -target_link_libraries(${LIB_TARGET_NAME} PUBLIC ThirdParty::nonstd Boost::boost) # Boost::system Boost::filesystem) +set(JINJA2CPP_PRIVATE_LIBS "${JINJA2CPP_PRIVATE_LIBS}") +include(thirdparty/CMakeLists.txt) + +target_link_libraries( + ${LIB_TARGET_NAME} + PUBLIC + ${JINJA2CPP_PUBLIC_LIBS} + PRIVATE + ${JINJA2CPP_PRIVATE_LIBS} +) target_include_directories(${LIB_TARGET_NAME} - INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include) + PUBLIC + $ + $ +) -if (WITH_TESTS) - enable_testing() +if (JINJA2CPP_STRICT_WARNINGS) + if (UNIX) + set(GCC_CXX_FLAGS ${GCC_CXX_FLAGS} -Wall -Werror) + set(CLANG_CXX_FLAGS ${CLANG_CXX_FLAGS} -Wall -Werror -Wno-unused-command-line-argument) + set(MSVC_CXX_FLAGS ${MSVC_CXX_FLAGS} /W4) + endif () +endif () + +if (CMAKE_CXX_COMPILER_ID MATCHES "GNU") + target_compile_options(${LIB_TARGET_NAME} PRIVATE ${GCC_CXX_FLAGS}) +elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang") + target_compile_options(${LIB_TARGET_NAME} PRIVATE ${CLANG_CXX_FLAGS}) +elseif (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") + target_compile_options(${LIB_TARGET_NAME} PRIVATE ${MSVC_CXX_FLAGS}) +endif () - include_directories( - ${GTEST_INCLUDE_DIRS} - ) +if ("${JINJA2CPP_USE_REGEX}" STREQUAL "boost") + set(_regex_define "-DJINJA2CPP_USE_REGEX_BOOST") +endif() +if ("${JINJA2CPP_WITH_JSON_BINDINGS}" STREQUAL "boost") + set(_bindings_define "-DJINJA2CPP_WITH_JSON_BINDINGS_BOOST") +elseif("${JINJA2CPP_WITH_JSON_BINDINGS}" STREQUAL "rapid") + set(_bindings_define "-DJINJA2CPP_WITH_JSON_BINDINGS_RAPID") +endif() +target_compile_definitions(${LIB_TARGET_NAME} + PUBLIC + -DBOOST_SYSTEM_NO_DEPRECATED + -DBOOST_ERROR_CODE_HEADER_ONLY + ${_regex_define} + ${_bindings_define} +) + +if (JINJA2CPP_BUILD_SHARED) + target_compile_definitions(${LIB_TARGET_NAME} PRIVATE -DJINJA2CPP_BUILD_AS_SHARED PUBLIC -DJINJA2CPP_LINK_AS_SHARED) +endif () + +set_target_properties(${LIB_TARGET_NAME} PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION 1 +) + +set_target_properties(${LIB_TARGET_NAME} PROPERTIES + CXX_STANDARD ${JINJA2CPP_CXX_STANDARD} + CXX_STANDARD_REQUIRED ON +) + +configure_file(jinja2cpp.pc.in ${CMAKE_BINARY_DIR}/jinja2cpp.pc @ONLY) + +if (JINJA2CPP_BUILD_TESTS) + enable_testing() 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} ${Boost_LIBRARIES}) + target_link_libraries(jinja2cpp_tests gtest gtest_main + nlohmann_json::nlohmann_json ${LIB_TARGET_NAME} ${EXTRA_TEST_LIBS} ${JINJA2CPP_PRIVATE_LIBS}) + + set_target_properties(jinja2cpp_tests PROPERTIES + CXX_STANDARD ${JINJA2CPP_CXX_STANDARD} + CXX_STANDARD_REQUIRED ON) + + if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") + target_compile_options(jinja2cpp_tests PRIVATE /bigobj) + endif () add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/test_data/simple_template1.j2tpl COMMAND ${CMAKE_COMMAND} ARGS -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/test/test_data ${CMAKE_CURRENT_BINARY_DIR}/test_data MAIN_DEPENDENCY ${CMAKE_CURRENT_SOURCE_DIR}/test/test_data/simple_template1.j2tpl COMMENT "Copy test data to the destination dir" - ) + ) add_custom_target(CopyTestData ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/test_data/simple_template1.j2tpl - ) + ) + + add_dependencies(jinja2cpp_tests CopyTestData) + + add_test(NAME jinja2cpp_tests COMMAND jinja2cpp_tests) endif () -install(TARGETS ${LIB_TARGET_NAME} - RUNTIME DESTINATION bin - LIBRARY DESTINATION lib - ARCHIVE DESTINATION lib/static) +set (JINJA2CPP_INSTALL_CONFIG_DIR "${CMAKE_INSTALL_LIBDIR}/${LIB_TARGET_NAME}") +set (JINJA2CPP_TMP_CONFIG_PATH "cmake/config") + -install (DIRECTORY include/ DESTINATION include) -install (DIRECTORY thirdparty/nonstd/expected-light/include/ DESTINATION include) -install (FILES cmake/public/FindJinja2Cpp.cmake DESTINATION cmake) +macro (Jinja2CppGetTargetIncludeDir infix target) + message (STATUS "infix: ${infix} target: ${target}") -add_test(NAME jinja2cpp_tests COMMAND jinja2cpp_tests) + if (TARGET ${target}) + set (_J2CPP_VAR_NAME JINJA2CPP_${infix}_INCLUDE_DIRECTORIES) + get_target_property(${_J2CPP_VAR_NAME} ${target} INTERFACE_INCLUDE_DIRECTORIES) + else () + message(WARNING "No target: ${target}") + endif() + +endmacro () + +Jinja2CppGetTargetIncludeDir(EXPECTED-LITE nonstd::expected-lite) +Jinja2CppGetTargetIncludeDir(VARIANT-LITE nonstd::variant-lite) +Jinja2CppGetTargetIncludeDir(OPTIONAL-LITE nonstd::optional-lite) +Jinja2CppGetTargetIncludeDir(STRING-VIEW-LITE nonstd::string-view-lite) + +# Workaround for #14444 bug of CMake (https://gitlab.kitware.com/cmake/cmake/issues/14444) +# We can't use EXPORT feature of 'install' as is due to limitation of subproject's targets installation +# So jinja2cpp-config.cmake should be written manually + +if(JINJA2CPP_INSTALL) + install( + TARGETS + ${LIB_TARGET_NAME} + EXPORT + InstallTargets + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}/static + FILE_SET HEADERS + ) + + install( + FILES + ${CMAKE_BINARY_DIR}/jinja2cpp.pc + DESTINATION + ${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig + ) + + install( + EXPORT + InstallTargets + FILE + jinja2cpp-cfg.cmake + DESTINATION + ${JINJA2CPP_INSTALL_CONFIG_DIR} + ) + + configure_package_config_file( + cmake/public/jinja2cpp-config.cmake.in + ${JINJA2CPP_TMP_CONFIG_PATH}/jinja2cpp-config.cmake + INSTALL_DESTINATION ${JINJA2CPP_TMP_CONFIG_PATH} + NO_CHECK_REQUIRED_COMPONENTS_MACRO + ) + + configure_package_config_file( + cmake/public/jinja2cpp-config-deps-${JINJA2CPP_DEPS_MODE}.cmake.in + ${JINJA2CPP_TMP_CONFIG_PATH}/jinja2cpp-config-deps.cmake + INSTALL_DESTINATION ${JINJA2CPP_TMP_CONFIG_PATH} + NO_CHECK_REQUIRED_COMPONENTS_MACRO + ) + + install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/${JINJA2CPP_TMP_CONFIG_PATH}/${LIB_TARGET_NAME}-config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/${JINJA2CPP_TMP_CONFIG_PATH}/${LIB_TARGET_NAME}-config-deps.cmake + DESTINATION + ${JINJA2CPP_INSTALL_CONFIG_DIR} + ) +endif() \ No newline at end of file diff --git a/README.md b/README.md index 0368c165..30147310 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,39 @@ -# Jinja2Cpp +
+ +# Jinja2С++ [![Language](https://img.shields.io/badge/language-C++-blue.svg)](https://isocpp.org/) [![Standard](https://img.shields.io/badge/c%2B%2B-14-blue.svg)](https://en.wikipedia.org/wiki/C%2B%2B#Standardization) -[![Build Status](https://travis-ci.org/flexferrum/Jinja2Cpp.svg?branch=master)](https://travis-ci.org/flexferrum/Jinja2Cpp) -[![Build status](https://ci.appveyor.com/api/projects/status/19v2k3bl63jxl42f/branch/master?svg=true)](https://ci.appveyor.com/project/flexferrum/Jinja2Cpp) -[![Coverage Status](https://codecov.io/gh/flexferrum/Jinja2Cpp/branch/master/graph/badge.svg)](https://codecov.io/gh/flexferrum/Jinja2Cpp) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/d932d23e9288404ba44a1f500ab42778)](https://www.codacy.com/app/flexferrum/Jinja2Cpp?utm_source=github.com&utm_medium=referral&utm_content=flexferrum/Jinja2Cpp&utm_campaign=Badge_Grade) -[![Github Releases](https://img.shields.io/github/release/flexferrum/Jinja2Cpp.svg)](https://github.com/flexferrum/Jinja2Cpp/releases) -[![Github Issues](https://img.shields.io/github/issues/flexferrum/Jinja2Cpp.svg)](http://github.com/flexferrum/Jinja2Cpp/issues) -[![GitHub License](https://img.shields.io/badge/license-Mozilla-blue.svg)](https://raw.githubusercontent.com/flexferrum/Jinja2Cpp/master/LICENSE) - -C++ implementation of big subset of Jinja2 template engine features. This library was inspired by [Jinja2CppLight](https://github.com/hughperkins/Jinja2CppLight) project and brings support of mostly all Jinja2 templates features into C++ world. Unlike [inja](https://github.com/pantor/inja) lib, you have to build Jinja2Cpp, but it has only one dependence: boost. - -# Table of contents - - - - - -- [Introduction](#introduction) -- [Getting started](#getting-started) - - [More complex example](#more-complex-example) - - [The simplest case](#the-simplest-case) - - [Reflection](#reflection) - - ['set' statement](#set-statement) - - ['extends' statement](#extends-statement) - - [Macros](#macros) - - [Error reporting](#error-reporting) - - [Other features](#other-features) -- [Current Jinja2 support](#current-jinja2-support) -- [Supported compilers](#supported-compilers) -- [Build and install](#build-and-install) - - [Additional CMake build flags](#additional-cmake-build-flags) -- [Link with you projects](#link-with-you-projects) -- [Changelog](#changelog) - - [Version 0.6](#version-06) - - - -# Introduction - -Main features of Jinja2Cpp: -- Easy-to-use public interface. Just load templates and render them. -- Conformance to [Jinja2 specification](http://jinja.pocoo.org/docs/2.10/) -- Partial support for both narrow- and wide-character strings both for templates and parameters. -- Built-in reflection for C++ types. -- Powerful full-featured Jinja2 expressions with filtering (via '|' operator) and 'if'-expressions. -- Control statements (set, for, if). -- Templates extention. -- Macros -- Rich error reporting. +[![Standard](https://img.shields.io/badge/c%2B%2B-17-blue.svg)](https://en.wikipedia.org/wiki/C%2B%2B#Standardization) +[![Standard](https://img.shields.io/badge/c%2B%2B-20-blue.svg)](https://en.wikipedia.org/wiki/C%2B%2B#Standardization) +[![Coverage Status](https://codecov.io/gh/jinja2cpp/Jinja2Cpp/branch/master/graph/badge.svg)](https://codecov.io/gh/jinja2cpp/Jinja2Cpp) +[![Github Releases](https://img.shields.io/github/release/jinja2cpp/Jinja2Cpp/all.svg)](https://github.com/jinja2cpp/Jinja2Cpp/releases) +[![Github Issues](https://img.shields.io/github/issues/jinja2cpp/Jinja2Cpp.svg)](http://github.com/jinja2cpp/Jinja2Cpp/issues) +[![GitHub License](https://img.shields.io/badge/license-Mozilla-blue.svg)](https://raw.githubusercontent.com/jinja2cpp/Jinja2Cpp/master/LICENSE) +[![conan.io](https://api.bintray.com/packages/conan/conan-center/jinja2cpp%3A_/images/download.svg?version=1.2.1%3A_) ](https://conan.io/center/jinja2cpp) +[![Gitter Chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Jinja2Cpp/Lobby) + +C++ implementation of the Jinja2 Python template engine. This library brings support of powerful Jinja2 template features into the C++ world, reports dynamic HTML pages and source code generation. + +## Introduction + +Main features of Jinja2C++: +- Easy-to-use public interface. Just load templates and render them. +- Conformance to [Jinja2 specification](http://jinja.pocoo.org/docs/2.10/) +- Full support of narrow- and wide-character strings both for templates and parameters. +- Built-in reflection for the common C++ types, nlohmann and rapid JSON libraries. +- Powerful full-featured Jinja2 expressions with filtering (via '|' operator) and 'if'-expressions. +- Control statements (`set`, `for`, `if`, `filter`, `do`, `with`). +- Templates extension, including and importing +- Macros +- Rich error reporting. +- Shared template environment with templates cache support For instance, this simple code: ```c++ +#include + std::string source = R"( {{ ("Hello", 'world') | join }}!!! {{ ("Hello", 'world') | join(', ') }}!!! @@ -62,8 +42,9 @@ std::string source = R"( )"; Template tpl; +tpl.Load(source); -std::string result = tpl.RenderAsString(ValuesMap()); +std::string result = tpl.RenderAsString({}).value(); ``` produces the result string: @@ -75,30 +56,30 @@ Hello; world!!! hello; world!!! ``` -# Getting started +## Getting started -In order to use Jinja2Cpp in your project you have to: -* Clone the Jinja2Cpp repository -* Build it according with the instructions +To use Jinja2C++ in your project you have to: +* Clone the Jinja2C++ repository +* Build it according to the [instructions](https://jinja2cpp.github.io/docs/build_and_install.html) * Link to your project. -Usage of Jinja2Cpp in the code is pretty simple: -1. Declare the jinja2::Template object: +Usage of Jinja2C++ in the code is pretty simple: +1. Declare the jinja2::Template object: ```c++ jinja2::Template tpl; ``` -2. Populate it with template: +2. Populate it with template: ```c++ -tpl.Load("{{'Hello World' }}!!!"); +tpl.Load("{{ 'Hello World' }}!!!"); ``` -3. Render the template: +3. Render the template: ```c++ -std::cout << tpl.RenderAsString(jinja2::ValuesMap{}) << std::endl; +std::cout << tpl.RenderAsString({}).value() << std::endl; ``` and get: @@ -109,452 +90,86 @@ Hello World!!! That's all! -## More complex example -Let's say you have the following enum: - -```c++ -enum Animals -{ - Dog, - Cat, - Monkey, - Elephant -}; -``` - -And you want to automatically produce string-to-enum and enum-to-string convertor. Like this: - -```c++ -inline const char* AnimalsToString(Animals e) -{ - switch (e) - { - case Dog: - return "Dog"; - case Cat: - return "Cat"; - case Monkey: - return "Monkey"; - case Elephant: - return "Elephant"; - } - return "Unknown Item"; -} -``` - -Of course, you can write this producer in the way like [this](https://github.com/flexferrum/autoprogrammer/blob/87a9dc8ff61c7bdd30fede249757b71984e4b954/src/generators/enum2string_generator.cpp#L140). It's too complicated for writing 'from scratch'. Actually, there is a better and simpler way. - -### The simplest case - -Firstly, you should define the simple jinja2 template (in the C++ manner): -```c++ -std::string enum2StringConvertor = R"( -inline const char* {{enumName}}ToString({{enumName}} e) -{ - switch (e) - { -{% for item in items %} - case {{item}}: - return "{{item}}"; -{% endfor %} - } - return "Unknown Item"; -})"; -``` -As you can see, this template is similar to the C++ sample code above, but some parts replaced by placeholders ("parameters"). These placeholders will be replaced with the actual text during template rendering process. In order to this happen, you should fill up the rendering parameters. This is a simple dictionary which maps the parameter name to the corresponding value: - -```c++ -jinja2::ValuesMap params { - {"enumName", "Animals"}, - {"items", {"Dog", "Cat", "Monkey", "Elephant"}}, -}; -``` -An finally, you can render this template with Jinja2Cpp library: - -```c++ -jinja2::Template tpl; -tpl.Load(enum2StringConvertor); -std::cout << tpl.RenderAsString(params); -``` -And you will get on the console the conversion function mentioned above! - -You can call 'Render' method many times, with different parameters set, from several threads. Everything will be fine: every time you call 'Render' you will get the result depended only on provided params. Also you can render some part of template many times (for different parameters) with help of 'for' loop and 'extend' statement (described below). It allows you to iterate through the list of items from the first to the last one and render the loop body for each item respectively. In this particular sample it allows to put as many 'case' blocks in conversion function as many items in the 'reflected' enum. - -### Reflection -Let's imagine you don't want to fill the enum descriptor by hand, but want to fill it with help of some code parsing tool ([autoprogrammer](https://github.com/flexferrum/autoprogrammer) or [cppast](https://github.com/foonathan/cppast)). In this case you can define structure like this: - -```c++ -// Enum declaration description -struct EnumDescriptor -{ -// Enumeration name -std::string enumName; -// Namespace scope prefix -std::string nsScope; -// Collection of enum items -std::vector enumItems; -}; -``` -This structure holds the enum name, enum namespace scope prefix, and list of enum items (we need just names). Then, you can populate instances of this descriptor automatically using chosen tool (ex. here: [clang-based enum2string converter generator](https://github.com/flexferrum/flex_lib/blob/accu2017/tools/codegen/src/main.cpp) ). For our sample we can create the instance manually: -```c++ -EnumDescriptor descr; -descr.enumName = "Animals"; -descr.nsScope = ""; -descr.enumItems = {"Dog", "Cat", "Monkey", "Elephant"}; -``` - -And now you need to transfer data from this internal enum descriptor to Jinja2 value params map. Of course it's possible to do it by hands: -```c++ -jinja2::ValuesMap params { - {"enumName", descr.enumName}, - {"nsScope", descr.nsScope}, - {"items", {descr.enumItems[0], descr.enumItems[1], descr.enumItems[2], descr.enumItems[3]}}, -}; -``` - -But actually, with Jinja2Cpp you don't have to do it manually. Library can do it for you. You just need to define reflection rules. Something like this: - -```c++ -namespace jinja2 -{ -template<> -struct TypeReflection : TypeReflected -{ - static auto& GetAccessors() - { - static std::unordered_map accessors = { - {"name", [](const EnumDescriptor& obj) {return obj.name;}}, - {"nsScope", [](const EnumDescriptor& obj) { return obj.nsScope;}}, - {"items", [](const EnumDescriptor& obj) {return Reflect(obj.items);}}, - }; - - return accessors; - } -}; -``` -And in this case you need to correspondingly change the template itself and it's invocation: -```c++ -std::string enum2StringConvertor = R"( -inline const char* {{enum.enumName}}ToString({{enum.enumName}} e) -{ - switch (e) - { -{% for item in enum.items %} - case {{item}}: - return "{{item}}"; -{% endfor %} - } - return "Unknown Item"; -})"; - -// ... - jinja2::ValuesMap params = { - {"enum", jinja2::Reflect(descr)}, - }; -// ... -``` -Every specified field will be reflected into Jinja2Cpp internal data structures and can be accessed from the template without additional efforts. Quite simply! As you can see, you can use 'dot' notation to access named members of some parameter as well, as index notation like this: `enum['enumName']`. With index notation you can access to the particular item of a list: `enum.items[3]` or `enum.items[itemIndex]` or `enum['items'][itemIndex]`. - -### 'set' statement -But what if enum `Animals` will be in the namespace? - -```c++ -namespace world -{ -enum Animals -{ - Dog, - Cat, - Monkey, - Elephant -}; -} -``` -In this case you need to prefix both enum name and it's items with namespace prefix in the generated code. Like this: -```c++ -std::string enum2StringConvertor = R"( -inline const char* {{enum.enumName}}ToString({{enum.nsScope}}::{{enum.enumName}} e) -{ - switch (e) - { -{% for item in enum.items %} - case {{enum.nsScope}}::{{item}}: - return "{{item}}"; -{% endfor %} - } - return "Unknown Item"; -})"; -``` -This template will produce 'world::' prefix for our new scoped enum (and enum itmes). And '::' for the ones in global scope. But you may want to eliminate the unnecessary global scope prefix. And you can do it this way: -```c++ -{% set prefix = enum.nsScope + '::' if enum.nsScope else '' %} -std::string enum2StringConvertor = R"(inline const char* {{enum.enumName}}ToString({{prefix}}::{{enum.enumName}} e) -{ - switch (e) - { -{% for item in enum.items %} - case {{prefix}}::{{item}}: - return "{{item}}"; -{% endfor %} - } - return "Unknown Item"; -})"; -``` -This template uses two significant jinja2 template features: -1. The 'set' statement. You can declare new variables in your template. And you can access them by the name. -2. if-expression. It works like a ternary '?:' operator in C/C++. In C++ the code from the sample could be written in this way: -```c++ -std::string prefix = !descr.nsScope.empty() ? descr.nsScope + "::" : ""; -``` -I.e. left part of this expression (before 'if') is a true-branch of the statement. Right part (after 'else') - false-branch, which can be omitted. As a condition you can use any expression convertible to bool. - -## 'extends' statement -In general, C++ header files look similar to each other. Almost every header file has got header guard, block of 'include' directives and then block of declarations wrapped into namespaces. So, if you have several different Jinja2 templates for header files production it can be a good idea to extract the common header structure into separate template. Like this: -```c++ -{% if headerGuard is defined %} - #ifndef {{headerGuard}} - #define {{headerGuard}} -{% else %} - #pragma once -{% endif %} - -{% for fileName in inputFiles | sort %} - #include "{{fileName}}" -{% endfor %} - -{% for fileName in extraHeaders | sort %} -{% if fileName is startsWith('<') %} - #include {{fileName}} -{% else %} - #include "{{fileName}}" -{% endif %} -{% endfor %} - -{% block generator_headers %}{% endblock %} - -{% block namespaced_decls %} -{% set ns = rootNamespace %} -{#ns | pprint} -{{rootNamespace | pprint} #} -{% block namespace_content scoped %}{%endblock%} -{% for ns in rootNamespace.innerNamespaces recursive %}namespace {{ns.name}} -{ -{{self.namespace_content()}} -{{ loop(ns.innerNamespaces) }} -} -{% endfor %} -{% endblock %} - -{% block global_decls %}{% endblock %} - -{% if headerGuard is defined %} - #endif // {{headerGuard}} -{% endif %} -``` - -In this sample you can see the '**block**' statements. They are placeholders. Each block is a part of generic template which can be replaced by more specific template which 'extends' generic: -```c++ -{% extends "header_skeleton.j2tpl" %} - -{% block namespaced_decls %}{{super()}}{% endblock %} - -{% block namespace_content %} -{% for class in ns.classes | sort(attribute="name") %} - -class {{ class.name }} -{ -public: - {% for method in class.methods | rejectattr('isImplicit') | selectattr('accessType', 'equalto', 'Public') %} - {{ method.fullPrototype }}; - {% endfor %} -protected: - {% for method in class.methods | rejectattr('isImplicit') | selectattr('accessType', 'equalto', 'Protected') %} - {{ method.fullPrototype }}; - {% endfor %} -private: - {% for method in class.methods | rejectattr('isImplicit') | selectattr('accessType', 'in', ['Private', 'Undefined']) %} - {{ method.fullPrototype }}; - {% endfor %} -}; - -{% endfor %} -{% endblock %} -``` - -'**extends**' statement here defines the template to extend. Set of '**block**' statements after defines actual filling of the corresponding blocks from the extended template. If block from the extended template contains something (like ```namespaced_decls``` from the example above), this content can be rendered with help of '**super()**' function. In other case the whole content of the block will be replaced. More detailed description of template inheritance feature can be found in [Jinja2 documentation](http://jinja.pocoo.org/docs/2.10/templates/#template-inheritance). - -## Macros -Ths sample above violates 'DRY' rule. It contains the code which could be generalized. And Jinja2 supports features for such kind generalization. This feature called '**macros**'. The sample can be rewritten the following way: -```c++ -{% macro MethodsDecl(class, access) %} -{% for method in class.methods | rejectattr('isImplicit') | selectattr('accessType', 'in', access) %} - {{ method.fullPrototype }}; -{% endfor %} -{% endmacro %} - -class {{ class.name }} -{ -public: - {{ MethodsDecl(class, ['Public']) }} -protected: - {{ MethodsDecl(class, ['Protected']) }} -private: - {{ MethodsDecl(class, ['Private', 'Undefined']) }} -}; - -{% endfor %} -``` - -`MethodsDecl` statement here is a **macro** which takes two arguments. First one is a class with method definitions. The second is a tuple of access specifiers. Macro takes non-implicit methods from the methods collection (`rejectattr('isImplicit')` filter) then select such methods which have right access specifier (`selectattr('accessType', 'in', access)`), then just prints the method full prototype. Finally, the macro is invoked as a regular function call: `MethodsDecl(class, ['Public'])` and replaced with rendered macro body. - -There is another way to invoke macro: the **call** statement. Simply put, this is a way to call macro with *callback*. Let's take another sample: - -```c++ -{% macro InlineMethod(resultType='void', methodName, methodParams=[]) %} -inline {{ resultType }} {{ methodName }}({{ methodParams | join(', ') }} ) -{ - {{ caller() }} -} -{% endmacro %} - -{% call InlineMethod('const char*', enum.enumName + 'ToString', [enum.nsScope ~ '::' enum.enumName ~ ' e']) %} - switch (e) - { -{% for item in enum.items %} - case {{enum.nsScope}}::{{item}}: - return "{{item}}"; -{% endfor %} - } - return "Unknown Item"; -{% endcall %} -``` - -Here is an `InlineMacro` which just describe the inline method definition skeleton. This macro doesn't contain the actual method body. Instead of this it calls `caller` special function. This function invokes the special **callback** macro which is a body of `call` statement. And this macro can have parameters as well. More detailed this mechanics described in the [Jinja2 documentation](http://jinja.pocoo.org/docs/2.10/templates/#macros). - -## Error reporting -It's difficult to write complex template completely without errors. Missed braces, wrong characters, incorrect names... Everything is possible. So, it's crucial to be able to get informative error report from the template engine. Jinja2Cpp provides such kind of report. ```Template::Load``` method (and TemplateEnv::LoadTemplate respectively) return instance of ```ErrorInfo``` class which contains details about the error. These details include: -- Error code -- Error description -- File name and position (1-based line, col) of the error -- Location description - -For example, this template: -``` -{{ {'key'=,} }} -``` -produces the following error message: -``` -noname.j2tpl:1:11: error: Expected expression, got: ',' -{{ {'key'=,} }} - ---^------- -``` - -## Other features -The render procedure is stateless, so you can perform several renderings simultaneously in different threads. Even if you pass parameters: - -```c++ - ValuesMap params = { - {"intValue", 3}, - {"doubleValue", 12.123f}, - {"stringValue", "rain"}, - {"boolFalseValue", false}, - {"boolTrueValue", true}, - }; - - std::string result = tpl.RenderAsString(params); - std::cout << result << std::endl; -``` - -Parameters could have the following types: -- std::string/std::wstring -- integer (int64_t) -- double -- boolean (bool) -- Tuples (also known as arrays) -- Dictionaries (also known as maps) - -Tuples and dictionaries can be mapped to the C++ types. So you can smoothly reflect your structures and collections into the template engine: - -```c++ -namespace jinja2 -{ -template<> -struct TypeReflection : TypeReflected -{ - static auto& GetAccessors() - { - static std::unordered_map accessors = { - {"name", [](const reflection::EnumInfo& obj) {return Reflect(obj.name);}}, - {"scopeSpecifier", [](const reflection::EnumInfo& obj) {return Reflect(obj.scopeSpecifier);}}, - {"namespaceQualifier", [](const reflection::EnumInfo& obj) { return obj.namespaceQualifier;}}, - {"isScoped", [](const reflection::EnumInfo& obj) {return obj.isScoped;}}, - {"items", [](const reflection::EnumInfo& obj) {return Reflect(obj.items);}}, - }; - - return accessors; - } -}; - -// ... - jinja2::ValuesMap params = { - {"enum", jinja2::Reflect(enumInfo)}, - }; -``` - -In this cases method 'jinja2::reflect' reflects regular C++ type into jinja2 template param. If type is a user-defined class or structure then handwritten mapper 'TypeReflection<>' should be provided. - -# Current Jinja2 support -Currently, Jinja2Cpp supports the limited number of Jinja2 features. By the way, Jinja2Cpp is planned to be full [jinja2 specification](http://jinja.pocoo.org/docs/2.10/templates/)-conformant. The current support is limited to: -- expressions. You can use almost every style of expressions: simple, filtered, conditional, and so on. -- big number of filters (**sort, default, first, last, length, max, min, reverse, unique, sum, attr, map, reject, rejectattr, select, selectattr, pprint, dictsort, abs, float, int, list, round, random, trim, title, upper, wordcount, replace, truncate, groupby, urlencode**) -- big number of testers (**eq, defined, ge, gt, iterable, le, lt, mapping, ne, number, sequence, string, undefined, in, even, odd, lower, upper**) -- limited number of functions (**range**, **loop.cycle**) -- 'if' statement (with 'elif' and 'else' branches) -- 'for' statement (with 'else' branch and 'if' part support) -- 'extends' statement -- 'set' statement -- 'extends'/'block' statements -- 'macro'/'call' statements -- recursive loops -- space control - -# Supported compilers -Compilation of Jinja2Cpp tested on the following compilers (with C++14 enabled feature): -- Linux gcc 5.0 -- Linux gcc 6.0 -- Linux gcc 7.0 -- Linux clang 5.0 -- Microsoft Visual Studio 2015 x86 -- Microsoft Visual Studio 2017 x86 - -# Build and install -Jinja2Cpp has got only two external dependency: boost library (at least version 1.55) and expected-lite. Because of types from boost are used inside library, you should compile both your projects and Jinja2Cpp library with similar compiler settings. Otherwise ABI could be broken. - -In order to compile Jinja2Cpp you need: - -1. Install CMake build system (at least version 3.0) -2. Clone jinja2cpp repository and update submodules: +More detailed examples and features description can be found in the documentation: [https://jinja2cpp.github.io/docs/usage](https://jinja2cpp.github.io/docs/usage) + +## Current Jinja2 support +Currently, Jinja2C++ supports the limited number of Jinja2 features. By the way, Jinja2C++ is planned to be a fully [jinja2 specification](http://jinja.pocoo.org/docs/2.10/templates/)-conformant. The current support is limited to: +- expressions. You can use almost every expression style: simple, filtered, conditional, and so on. +- the big number of filters (**sort, default, first, last, length, max, min, reverse, unique, sum, attr, map, reject, rejectattr, select, selectattr, pprint, dictsort, abs, float, int, list, round, random, trim, title, upper, wordcount, replace, truncate, groupby, urlencode, capitalize, escape, tojson, striptags, center, xmlattr**) +- the big number of testers (**eq, defined, ge, gt, iterable, le, lt, mapping, ne, number, sequence, string, undefined, in, even, odd, lower, upper**) +- the number of functions (**range**, **loop.cycle**) +- 'if' statement (with 'elif' and 'else' branches) +- 'for' statement (with 'else' branch and 'if' part support) +- 'include' statement +- 'import'/'from' statements +- 'set' statement (both line and block) +- 'filter' statement +- 'extends'/'block' statements +- 'macro'/'call' statements +- 'with' statement +- 'do' extension statement +- recursive loops +- space control and 'raw'/'endraw' blocks + +Full information about Jinja2 specification support and compatibility table can be found here: [https://jinja2cpp.github.io/docs/j2_compatibility.html](https://jinja2cpp.github.io/docs/j2_compatibility.html). + +## Supported compilers +Compilation of Jinja2C++ tested on the following compilers (with C++14 and C++17 enabled features): +- Linux gcc 5.5 - 9.0 +- Linux clang 5.0 - 9 +- MacOS X-Code 9 +- MacOS X-Code 10 +- MacOS X-Code 11 (C++14 in default build, C++17 with externally-provided boost) +- Microsoft Visual Studio 2015 - 2019 x86, x64 +- MinGW gcc compiler 7.3 +- MinGW gcc compiler 8.1 + +**Note:** Support of gcc version >= 9.x or clang version >= 8.0 depends on the version of the Boost library provided. + +### Build status + +| Compiler | Status | +|---------|---------:| +| **MSVC** 2015 (x86, x64), **MinGW** 7 (x64), **MinGW** 8 (x64) | [![Build status](https://ci.appveyor.com/api/projects/status/vu59lw4r67n8jdxl/branch/master?svg=true)](https://ci.appveyor.com/project/flexferrum/jinja2cpp-n5hjm/branch/master) | +| **X-Code** 9, 10, 11 | [![Build Status](https://travis-ci.org/jinja2cpp/Jinja2Cpp.svg?branch=master)](https://travis-ci.org/jinja2cpp/Jinja2Cpp) | +| **MSVC** 2017 (x86, x64), **MSVC** 2019 (x86, x64), C++14/C++17 | [![](https://github.com/jinja2cpp/Jinja2Cpp/workflows/CI-windows-build/badge.svg)](https://github.com/jinja2cpp/Jinja2Cpp/actions?query=workflow%3ACI-windows-build) | +| **g++** 5, 6, 7, 8, 9, 10, 11 **clang** 5, 6, 7, 8, 9, 10, 11, 12 C++14/C++17/C++20 | [![](https://github.com/jinja2cpp/Jinja2Cpp/workflows/CI-linux-build/badge.svg)](https://github.com/jinja2cpp/Jinja2Cpp/actions?query=workflow%3ACI-linux-build) | + +## Build and install +Jinja2C++ has several external dependencies: +- `boost` library (at least version 1.65) +- `nonstd::expected-lite` [https://github.com/martinmoene/expected-lite](https://github.com/martinmoene/expected-lite) +- `nonstd::variant-lite` [https://github.com/martinmoene/variant-lite](https://github.com/martinmoene/variant-lite) +- `nonstd::optional-lite` [https://github.com/martinmoene/optional-lite](https://github.com/martinmoene/optional-lite) +- `nonstd::string-view-lite` [https://github.com/martinmoene/string-view-lite](https://github.com/martinmoene/string-view-lite) +- `fmtlib::fmt` [https://github.com/fmtlib/fmt](https://github.com/fmtlib/fmt) + +Examples of build scripts and different build configurations could be found here: [https://github.com/jinja2cpp/examples-build](https://github.com/jinja2cpp/examples-build) + +In simplest case to compile Jinja2C++ you need: + +1. Install CMake build system (at least version 3.0) +2. Clone jinja2cpp repository: ``` > git clone https://github.com/flexferrum/Jinja2Cpp.git -> git submodule -q update --init ``` -3. Create build directory: +3. Create build directory: ``` > cd Jinja2Cpp > mkdir build ``` -4. Run CMake and build the library: +4. Run CMake and build the library: ``` > cd build > cmake .. -DCMAKE_INSTALL_PREFIX= > cmake --build . --target all ``` -"Path to install folder" here is a path to the folder where you want to install Jinja2Cpp lib. +"Path to install folder" here is a path to the folder where you want to install Jinja2C++ lib. 5. Install library: @@ -562,69 +177,221 @@ In order to compile Jinja2Cpp you need: > cmake --build . --target install ``` -6. Also you can run the tests: +In this case, Jinja2C++ will be built with internally-shipped dependencies and install them respectively. But Jinja2C++ supports builds with externally-provided deps. +### Usage with conan.io dependency manager +Jinja2C++ can be used as conan.io package. In this case, you should do the following steps: -``` -> ctest -C Release -``` +1. Install conan.io according to the documentation ( https://docs.conan.io/en/latest/installation.html ) +2. Add a reference to Jinja2C++ package (`jinja2cpp/1.2.1`) to your conanfile.txt, conanfile.py or CMakeLists.txt. For instance, with the usage of `conan-cmake` integration it could be written this way: -## Additional CMake build flags -You can define (via -D command line CMake option) the following build flags: +```cmake -* **WITH_TESTS** (default TRUE) - build or not Jinja2Cpp tests. -* **MSVC_RUNTIME_TYPE** (default /MD) - MSVC runtime type to link with (if you use Microsoft Visual Studio compiler). -* **LIBRARY_TYPE** Could be STATIC (default for Windows platform) or SHARED (default for Linux). Specify the type of Jinja2Cpp library to build. +cmake_minimum_required(VERSION 3.24) +project(Jinja2CppSampleConan CXX) -# Link with you projects -Jinja2Cpp is shipped with cmake finder script. So you can: +list(APPEND CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR}) +list(APPEND CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR}) -1. Include Jinja2Cpp cmake scripts to the project: -```cmake -list (APPEND CMAKE_MODULE_PATH ${JINJA2CPP_INSTALL_DIR}/cmake) -``` +add_definitions("-std=c++14") -2. Use regular 'find' script: -```cmake -find_package(Jinja2Cpp) -``` +if(NOT EXISTS "${CMAKE_BINARY_DIR}/conan.cmake") + message(STATUS "Downloading conan.cmake from https://github.com/conan-io/cmake-conan") + file(DOWNLOAD "https://raw.githubusercontent.com/conan-io/cmake-conan/0.18.1/conan.cmake" + "${CMAKE_BINARY_DIR}/conan.cmake" + TLS_VERIFY ON) +endif() +include(${CMAKE_BINARY_DIR}/conan.cmake) -3. Add found paths to the project settings: -```cmake -#... -include_directories( - #... - ${JINJA2CPP_INCLUDE_DIR} - ) - -target_link_libraries(YourTarget - #... - ${JINJA2CPP_LIBRARY} - ) -#... -``` +conan_cmake_autodetect(settings) +conan_cmake_run(REQUIRES + jinja2cpp/1.1.0 + gtest/1.14.0 + BASIC_SETUP + ${CONAN_SETTINGS} + OPTIONS + jinja2cpp/*:shared=False + gtest/*:shared=False + BUILD missing) + +set (TARGET_NAME jinja2cpp_build_test) + +add_executable (${TARGET_NAME} main.cpp) + +target_link_libraries (${TARGET_NAME} ${CONAN_LIBS}) +set_target_properties (${TARGET_NAME} PROPERTIES + CXX_STANDARD 14 + CXX_STANDARD_REQUIRED ON) -or just link with Jinja2Cpp target: -```cmake -#... -target_link_libraries(YourTarget - #... - Jinja2Cpp - ) -#... ``` -# Changelog -## Version 0.9 -* Support of 'extents'/'block' statements -* Support of 'macro'/'call' statements -* Rich error reporting -* Support for recursive loops -* Support for space control before and after control blocks -* Improve reflection - -## Version 0.6 -* A lot of filters has been implemented. Full set of supported filters listed here: https://github.com/flexferrum/Jinja2Cpp/issues/7 -* A lot of testers has been implemented. Full set of supported testers listed here: https://github.com/flexferrum/Jinja2Cpp/issues/8 -* 'Contatenate as string' operator ('~') has been implemented -* For-loop with 'if' condition has been implemented -* Fixed some bugs in parser + +### Additional CMake build flags +You can define (via -D command-line CMake option) the following build flags: + +- **JINJA2CPP_BUILD_TESTS** (default TRUE) - to build or not to Jinja2C++ tests. +- **JINJA2CPP_STRICT_WARNINGS** (default TRUE) - Enable strict mode compile-warnings(-Wall -Werror, etc). +- **JINJA2CPP_MSVC_RUNTIME_TYPE** (default /MD) - MSVC runtime type to link with (if you use Microsoft Visual Studio compiler). +- **JINJA2CPP_DEPS_MODE** (default "internal") - modes for dependency handling. Following values possible: + - `internal` In this mode Jinja2C++ build script uses dependencies (include `boost`) shipped as subprojects. Nothing needs to be provided externally. + - `external-boost` In this mode Jinja2C++ build script uses only `boost` as an externally-provided dependency. All other dependencies are taken from subprojects. + - `external` In this mode all dependencies should be provided externally. Paths to `boost`, `nonstd-*` libs, etc. should be specified via standard CMake variables (like `CMAKE_PREFIX_PATH` or libname_DIR) + - `conan-build` Special mode for building Jinja2C++ via conan recipe. + + +### Build with C++17 standard enabled +Jinja2C++ tries to use standard versions of `std::variant`, `std::string_view` and `std::optional` if possible. + +## Acknowledgments +Thanks to **@flexferrum** for creating this library, for being one of the brightest minds in software engineering community. Rest in peace, friend. + +Thanks to **@manu343726** for CMake scripts improvement, bug hunting, and fixing and conan.io packaging. + +Thanks to **@martinmoene** for the perfectly implemented xxx-lite libraries. + +Thanks to **@vitaut** for the amazing text formatting library. + +Thanks to **@martinus** for the fast hash maps implementation. + + +## Changelog + +### Version 1.3.2 + +#### What's Changed +- Fix empty dict literal parsing in https://github.com/jinja2cpp/Jinja2Cpp/pull/243 +- Fixing missing life rage protection for the reverse filter (and others). by @jferreyra-sc in https://github.com/jinja2cpp/Jinja2Cpp/pull/246 +- Ability to disable JinjaCpp installation rules by @ilya-lavrenov in https://github.com/jinja2cpp/Jinja2Cpp/pull/250 +- Cmake finds only downloaded rapidjson by @ilya-lavrenov in https://github.com/jinja2cpp/Jinja2Cpp/pull/254 +- Update boost dependencies by @Cheaterdev in https://github.com/jinja2cpp/Jinja2Cpp/pull/253 + +#### New Contributors +- @jferreyra-sc made their first contribution in https://github.com/jinja2cpp/Jinja2Cpp/pull/246 +- @ilya-lavrenov made their first contribution in https://github.com/jinja2cpp/Jinja2Cpp/pull/250 +- @Cheaterdev made their first contribution in https://github.com/jinja2cpp/Jinja2Cpp/pull/253 + +### Version 1.3.1 + +#### Changes and improvements +- bump deps versions +- add new json binding - boost::json +- speedup regex parsing by switching to boost::regex(std::regex extremely slow) + - templates are now loading faster + +#### Fixed bugs +- small fixes across code base + +#### Breaking changes +- internal deps now used through cmake fetch_content +- default json serializer/deserializer is switched to boost::json + +### Version 1.2.1 + +#### Changes and improvements +- bump deps versions +- support modern compilers(up to Clang 12) and standards(C++20) +- tiny code style cleanup + +#### Fixed bugs +- small fixes across code base + +#### Breaking changes +- internal deps point to make based boost build + +### Version 1.1.0 +#### Changes and improvements +- `batch` filter added +- `slice` filter added +- `format` filter added +- `tojson` filter added +- `striptags` filter added +- `center` filter added +- `xmlattr` filter added +- `raw`/`endraw` tags added +- repeat string operator added (e. g. `'a' * 5` will produce `'aaaaa'`) +- support for templates metadata (`meta`/`endmeta` tags) added +- `-fPIC` flag added to Linux build configuration + +#### Fixed bugs +- Fix behavior of lstripblock/trimblocks global settings. Now it fully corresponds to the origina jinja2 +- Fix bug with rendering parent `block` content if child doesn't override this block +- Fix compilation issues with user-defined callables with number of arguments more than 2 +- Fix access to global Jinja2 functions from included/extended templates +- Fix point of evaluation of macro params +- Fix looping over the strings +- Cleanup warnings + +#### Breaking changes +- From now with C++17 standard enabled Jinja2C++ uses standard versions of types `variant`, `string_view` and `optional` + +### Version 1.0.0 +#### Changes and improvements +- `default` attribute added to the `map` filter (#48) +- escape sequences support added to the string literals (#49) +- arbitrary ranges, generated sequences, input iterators, etc. now can be used with `GenericList` type (#66) +- nonstd::string_view is now one of the possible types for the `Value` +- `filter` tag support added to the template parser (#44) +- `escape` filter support added to the template parser (#140) +- `capitalize` filter support added to the template parser (#137) +- the multiline version of `set` tag added to the parser (#45) +- added built-in reflection for nlohmann JSON and RapidJSON libraries (#78) +- `loop.depth` and `loop.depth0` variables support added +- {fmt} is now used as a formatting library instead of iostreams +- robin hood hash map is now used for internal value storage +- rendering performance improvements +- template cache implemented in `TemplateEnv` +- user-defined callables now can accept global context via `*context` special param +- MinGW, clang >= 7.0, XCode >= 9, gcc >= 7.0 are now officially supported as a target compilers (#79) + +#### Fixed bugs +- Fixed pipe (`|`) operator precedence (#47) +- Fixed bug in internal char <-> wchar_t converter on Windows +- Fixed crash in parsing `endblock` tag +- Fixed scope control for `include` and `for` tags +- Fixed bug with macros call within expression context + +#### Breaking changes +- MSVC runtime type is now defined by `JINJA2CPP_MSVC_RUNTIME_TYPE` CMake variable + +### Version 0.9.2 +#### Major changes +- User-defined callables implemented. Now you can define your own callable objects, pass them as input parameters and use them inside templates as regular (global) functions, filters or testers. See details here: https://jinja2cpp.github.io/docs/usage/ud_callables.html +- Now you can define global (template environment-wide) parameters that are accessible for all templates bound to this environment. +- `include`, `import` and `from` statements implemented. Now it's possible to include other templates and use macros from other templates. +- `with` statement implemented +- `do` statement implemented +- Sample build projects for various Jinja2C++ usage variants created: https://github.com/jinja2cpp/examples-build](https://github.com/jinja2cpp/examples-build) +- Documentation site created for Jinja2C++: https://jinja2cpp.github.io + +#### Minor changes +- Render-time error handling added +- Dependency management mode added to the build script +- Fix bugs with error reporting during the parse time +- Upgraded versions of external dependencies + +#### Breaking changes +- `RenderAsString` method now returns `nonstd::expected` instead of regular `std::string` +- Templates with `import`, `extends` and `include` generate errors if parsed without `TemplateEnv` set +- Release bundles (archives) are configured with `external` dependency management mode by default + +### Version 0.9.1 +- `applymacro` filter added which allows applying arbitrary macro as a filter +- dependencies to boost removed from the public interface +- CMake scripts improved +- Various bugs fixed +- Improve reflection +- Warnings cleanup + +### Version 0.9 +- Support of 'extents'/'block' statements +- Support of 'macro'/'call' statements +- Rich error reporting +- Support for recursive loops +- Support for space control before and after control blocks +- Improve reflection + +### Version 0.6 +- A lot of filters have been implemented. Full set of supported filters listed here: [https://github.com/flexferrum/Jinja2Cpp/issues/7](https://github.com/flexferrum/Jinja2Cpp/issues/7) +- A lot of testers have been implemented. Full set of supported testers listed here: [https://github.com/flexferrum/Jinja2Cpp/issues/8](https://github.com/flexferrum/Jinja2Cpp/issues/8) +- 'Concatenate as string' operator ('~') has been implemented +- For-loop with 'if' condition has been implemented +- Fixed some bugs in parser diff --git a/appveyor.yml b/appveyor.yml index 89d7a5a9..874ea11f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,30 +1,95 @@ -version: 0.9.{build} +version: 1.0.0.{build} + +skip_commits: + message: /.*/ +skip_branch_with_pr: true +skip_tags: true +skip_non_tags: true os: - Visual Studio 2015 - Visual Studio 2017 +platform: + - Win32 + - x64 + +configuration: + - Debug + - Release + environment: + BOOST_ROOT: C:\Libraries\boost_1_65_1 + GENERATOR: "\"NMake Makefiles\"" + DEPS_MODE: internal + matrix: - - PLATFORM: x64 - - PLATFORM: x86 +# - BUILD_PLATFORM: clang +# MSVC_RUNTIME_TYPE: +# GENERATOR: "\"Unix Makefiles\"" +# DEPS_MODE: internal +# EXTRA_CMAKE_ARGS: -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_MAKE_PROGRAM=mingw32-make.exe -DCMAKE_VERBOSE_MAKEFILE=ON -DJINJA2CPP_STRICT_WARNINGS=OFF + - BUILD_PLATFORM: MinGW7 + MSVC_RUNTIME_TYPE: + GENERATOR: "\"Unix Makefiles\"" + DEPS_MODE: internal + EXTRA_CMAKE_ARGS: -DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc.exe -DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++.exe -DCMAKE_MAKE_PROGRAM=mingw32-make.exe -DCMAKE_VERBOSE_MAKEFILE=ON -DJINJA2CPP_STRICT_WARNINGS=OFF + - BUILD_PLATFORM: MinGW8 + MSVC_RUNTIME_TYPE: + GENERATOR: "\"Unix Makefiles\"" + DEPS_MODE: internal + EXTRA_CMAKE_ARGS: -DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc.exe -DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++.exe -DCMAKE_MAKE_PROGRAM=mingw32-make.exe -DCMAKE_VERBOSE_MAKEFILE=ON -DJINJA2CPP_STRICT_WARNINGS=OFF + - BUILD_PLATFORM: x64 + MSVC_RUNTIME_TYPE: /MD + - BUILD_PLATFORM: x64 + MSVC_RUNTIME_TYPE: /MT + - BUILD_PLATFORM: x64 + MSVC_RUNTIME_TYPE: + - BUILD_PLATFORM: x86 + MSVC_RUNTIME_TYPE: /MD + - BUILD_PLATFORM: x86 + MSVC_RUNTIME_TYPE: /MT + - BUILD_PLATFORM: x86 + MSVC_RUNTIME_TYPE: matrix: fast_finish: false + exclude: + - os: Visual Studio 2015 + BUILD_PLATFORM: MinGW7 + - os: Visual Studio 2015 + BUILD_PLATFORM: MinGW8 + - os: Visual Studio 2015 + BUILD_PLATFORM: clang + - os: Visual Studio 2017 + BUILD_PLATFORM: x86 + - os: Visual Studio 2017 + BUILD_PLATFORM: x64 + - platform: Win32 + BUILD_PLATFORM: x64 + - platform: Win32 + BUILD_PLATFORM: MinGW7 + - platform: Win32 + BUILD_PLATFORM: MinGW8 + - platform: Win32 + BUILD_PLATFORM: clang + - platform: x64 + BUILD_PLATFORM: x86 + init: - - if "%APPVEYOR_BUILD_WORKER_IMAGE%"=="Visual Studio 2017" if "%PLATFORM%"=="x86" call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars32.bat" - - if "%APPVEYOR_BUILD_WORKER_IMAGE%"=="Visual Studio 2017" if "%PLATFORM%"=="x64" call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars64.bat" - - if "%APPVEYOR_BUILD_WORKER_IMAGE%"=="Visual Studio 2015" call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 - -install: - - git submodule -q update --init + - if "%APPVEYOR_BUILD_WORKER_IMAGE%"=="Visual Studio 2017" if "%BUILD_PLATFORM%"=="MinGW7" set PATH=C:\mingw-w64\x86_64-7.2.0-posix-seh-rt_v5-rev1\mingw64\bin;%PATH% + - if "%APPVEYOR_BUILD_WORKER_IMAGE%"=="Visual Studio 2017" if "%BUILD_PLATFORM%"=="MinGW8" set PATH=C:\mingw-w64\x86_64-8.1.0-posix-seh-rt_v6-rev0\mingw64\bin;%PATH% + - if "%APPVEYOR_BUILD_WORKER_IMAGE%"=="Visual Studio 2017" if "%BUILD_PLATFORM%"=="clang" set PATH=%BOOST_ROOT%\lib64-msvc-14.1;C:\mingw-w64\x86_64-7.3.0-posix-seh-rt_v5-rev0\mingw64\bin;C:\Libraries\llvm-5.0.0\bin;%PATH% && call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars64.bat" + - if "%APPVEYOR_BUILD_WORKER_IMAGE%"=="Visual Studio 2015" if "%BUILD_PLATFORM%"=="x86" call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 + - if "%APPVEYOR_BUILD_WORKER_IMAGE%"=="Visual Studio 2015" if "%BUILD_PLATFORM%"=="x86" set PATH=%BOOST_ROOT%\lib32-msvc-14.0;%PATH% + - if "%APPVEYOR_BUILD_WORKER_IMAGE%"=="Visual Studio 2015" if "%BUILD_PLATFORM%"=="x64" call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" amd64 + - if "%APPVEYOR_BUILD_WORKER_IMAGE%"=="Visual Studio 2015" if "%BUILD_PLATFORM%"=="x64" set PATH=%BOOST_ROOT%\lib64-msvc-14.0;%PATH% build_script: - mkdir -p build && cd build - - set BOOST_DIR=C:\Libraries\boost_1_65_1 - - cmake .. -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release - - cmake --build . --target all + - cmake .. -G %GENERATOR% -DCMAKE_BUILD_TYPE=%configuration% -DJINJA2CPP_MSVC_RUNTIME_TYPE=%MSVC_RUNTIME_TYPE% -DJINJA2CPP_DEPS_MODE=%DEPS_MODE% %EXTRA_CMAKE_ARGS% + - cmake --build . --target all --config %configuration% test_script: - - ctest -C Release -V + - ctest -C %configuration% -V diff --git a/cmake/coverage.cmake b/cmake/coverage.cmake new file mode 100644 index 00000000..3e8720fa --- /dev/null +++ b/cmake/coverage.cmake @@ -0,0 +1,26 @@ +if ((NOT ${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") AND (NOT ${CMAKE_CXX_COMPILER_ID} MATCHES "GNU")) + message(WARNING "coverage build is not supported on such compiler ${CMAKE_CXX_COMPILER_ID}") + set(JINJA2CPP_WITH_COVERAGE OFF) + return() +endif() + +function(add_coverage_target _TARGET) + if (NOT TARGET ${_TARGET}) + add_library(${_TARGET} INTERFACE) + endif() + target_compile_options( + ${_TARGET} + INTERFACE + -fprofile-arcs -ftest-coverage + ) + target_link_libraries(${_TARGET} INTERFACE gcov) + + if(JINJA2CPP_INSTALL) + install( + TARGETS + ${_TARGET} + EXPORT + InstallTargets + ) + endif() +endfunction() diff --git a/cmake/public/jinja2cpp-config-deps-conan-build.cmake.in b/cmake/public/jinja2cpp-config-deps-conan-build.cmake.in new file mode 100644 index 00000000..e69de29b diff --git a/cmake/public/jinja2cpp-config-deps-conan.cmake.in b/cmake/public/jinja2cpp-config-deps-conan.cmake.in new file mode 100644 index 00000000..e69de29b diff --git a/cmake/public/jinja2cpp-config-deps-external-boost.cmake.in b/cmake/public/jinja2cpp-config-deps-external-boost.cmake.in new file mode 100644 index 00000000..89fcaf5f --- /dev/null +++ b/cmake/public/jinja2cpp-config-deps-external-boost.cmake.in @@ -0,0 +1,15 @@ +macro (Jinja2CppAddBoostDep name) + if (TARGET Boost::${name}) + list (APPEND JINJA2CPP_INTERFACE_LINK_LIBRARIES $) + elseif (TARGET boost_${name}) + list (APPEND JINJA2CPP_INTERFACE_LINK_LIBRARIES $) + endif () +endmacro () + +Jinja2CppAddBoostDep(variant) +Jinja2CppAddBoostDep(filesystem) +Jinja2CppAddBoostDep(algorithm) + +set_property(TARGET jinja2cpp PROPERTY + INTERFACE_LINK_LIBRARIES ${JINJA2CPP_INTERFACE_LINK_LIBRARIES} +) diff --git a/cmake/public/jinja2cpp-config-deps-external.cmake.in b/cmake/public/jinja2cpp-config-deps-external.cmake.in new file mode 100644 index 00000000..9a189890 --- /dev/null +++ b/cmake/public/jinja2cpp-config-deps-external.cmake.in @@ -0,0 +1,45 @@ +# Create imported target expected-lite +add_library(expected-lite INTERFACE IMPORTED) + +set_target_properties(expected-lite PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "@JINJA2CPP_EXPECTED-LITE_INCLUDE_DIRECTORIES@" +) + +# Create imported target variant-lite +add_library(variant-lite INTERFACE IMPORTED) + +set_target_properties(variant-lite PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "@JINJA2CPP_VARIANT-LITE_INCLUDE_DIRECTORIES@" +) + +# Create imported target optional-lite +add_library(optional-lite INTERFACE IMPORTED) + +set_target_properties(optional-lite PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "@JINJA2CPP_OPTIONAL-LITE_INCLUDE_DIRECTORIES@" +) + +# Create imported target string-view-lite +add_library(string-view-lite INTERFACE IMPORTED) + +set_target_properties(string-view-lite PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "@JINJA2CPP_STRING-VIEW-LITE_INCLUDE_DIRECTORIES@" +) + +set (JINJA2CPP_INTERFACE_LINK_LIBRARIES expected-lite variant-lite optional-lite string-view-lite) + +macro (Jinja2CppAddBoostDep name) + if (TARGET Boost::${name}) + list (APPEND JINJA2CPP_INTERFACE_LINK_LIBRARIES $) + elseif (TARGET boost_${name}) + list (APPEND JINJA2CPP_INTERFACE_LINK_LIBRARIES $) + endif () +endmacro () + +Jinja2CppAddBoostDep(variant) +Jinja2CppAddBoostDep(filesystem) +Jinja2CppAddBoostDep(algorithm) + +set_property(TARGET jinja2cpp PROPERTY + INTERFACE_LINK_LIBRARIES ${JINJA2CPP_INTERFACE_LINK_LIBRARIES} +) diff --git a/cmake/public/jinja2cpp-config-deps-internal.cmake.in b/cmake/public/jinja2cpp-config-deps-internal.cmake.in new file mode 100644 index 00000000..e69de29b diff --git a/cmake/public/jinja2cpp-config.cmake.in b/cmake/public/jinja2cpp-config.cmake.in new file mode 100644 index 00000000..37e6652e --- /dev/null +++ b/cmake/public/jinja2cpp-config.cmake.in @@ -0,0 +1,85 @@ +# Based on generated file by CMake + +if("${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}" LESS 2.5) + message(FATAL_ERROR "CMake >= 2.6.0 required") +endif() +cmake_policy(PUSH) +cmake_policy(VERSION 2.6) + +#---------------------------------------------------------------- +# Generated CMake target import file. +#---------------------------------------------------------------- + +# Commands may need to know the format version. +set(CMAKE_IMPORT_FILE_VERSION 1) + +# Protect against multiple inclusion, which would fail when already imported targets are added once more. +set(_targetsDefined) +set(_targetsNotDefined) +set(_expectedTargets) +foreach(_expectedTarget jinja2cpp) + list(APPEND _expectedTargets ${_expectedTarget}) + if(NOT TARGET ${_expectedTarget}) + list(APPEND _targetsNotDefined ${_expectedTarget}) + endif() + if(TARGET ${_expectedTarget}) + list(APPEND _targetsDefined ${_expectedTarget}) + endif() +endforeach() +if("${_targetsDefined}" STREQUAL "${_expectedTargets}") + unset(_targetsDefined) + unset(_targetsNotDefined) + unset(_expectedTargets) + set(CMAKE_IMPORT_FILE_VERSION) + cmake_policy(POP) + return() +endif() +if(NOT "${_targetsDefined}" STREQUAL "") + message(FATAL_ERROR "Some (but not all) targets in this export set were already defined.\nTargets Defined: ${_targetsDefined}\nTargets not yet defined: ${_targetsNotDefined}\n") +endif() +unset(_targetsDefined) +unset(_targetsNotDefined) +unset(_expectedTargets) + + +# Compute the installation prefix relative to this file. +get_filename_component(_IMPORT_PREFIX "${CMAKE_CURRENT_LIST_FILE}" PATH) +get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH) +get_filename_component(_IMPORT_PREFIX "${_IMPORT_PREFIX}" PATH) +if(_IMPORT_PREFIX STREQUAL "/") + set(_IMPORT_PREFIX "") +endif() + +# Create imported target jinja2cpp +add_library(jinja2cpp STATIC IMPORTED) + +set_target_properties(jinja2cpp PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include" +) + +if (JINJA2CPP_BUILD_SHARED) + target_compile_definitions(jinja2cpp PUBLIC -DJINJA2CPP_LINK_AS_SHARED) +endif() + + +# INTERFACE_LINK_LIBRARIES "nonstd::expected-lite;nonstd::variant-lite;nonstd::value_ptr-lite;nonstd::optional-lite;\$;\$;\$;\$" + +if(CMAKE_VERSION VERSION_LESS 2.8.12) + message(FATAL_ERROR "This file relies on consumers using CMake 2.8.12 or greater.") +endif() + +# Load information for each installed configuration. +get_filename_component(_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH) +file(GLOB CONFIG_FILES "${_DIR}/jinja2cpp-cfg-*.cmake") +foreach(f ${CONFIG_FILES}) + include(${f}) +endforeach() + +include(${_DIR}/jinja2cpp-config-deps.cmake) + +# Cleanup temporary variables. +set(_IMPORT_PREFIX) + +# Commands beyond this point should not need to know the version. +set(CMAKE_IMPORT_FILE_VERSION) +cmake_policy(POP) diff --git a/cmake/sanitizer.address+undefined.cmake b/cmake/sanitizer.address+undefined.cmake new file mode 100644 index 00000000..91e3a703 --- /dev/null +++ b/cmake/sanitizer.address+undefined.cmake @@ -0,0 +1 @@ +include(sanitizer) diff --git a/cmake/sanitizer.cmake b/cmake/sanitizer.cmake new file mode 100644 index 00000000..7b63759e --- /dev/null +++ b/cmake/sanitizer.cmake @@ -0,0 +1,43 @@ +#if ((NOT ${CMAKE_CXX_COMPILER_ID} MATCHES "Clang") AND (NOT ${CMAKE_CXX_COMPILER_ID} MATCHES "GNU")) +# message(WARNING "sanitized build is not supported using this compiler ${CMAKE_CXX_COMPILER_ID}") +# set(JINJA2CPP_WITH_SANITIZERS OFF) +# return() +#endif() + +set(_BASE_SANITIZER_FLAGS "-fno-omit-frame-pointer -fno-optimize-sibling-calls") +separate_arguments(_BASE_SANITIZER_FLAGS) +set(_BASE_ENABLE_SANITIZER_FLAGS) +if(JINJA2CPP_WITH_SANITIZERS STREQUAL address+undefined) + set(_BASE_ENABLE_SANITIZER_FLAGS "-fsanitize=address,undefined") +endif() + +if(JINJA2CPP_WITH_SANITIZERS STREQUAL memory) + set(_BASE_ENABLE_SANITIZER_FLAGS "-fsanitize=memory") +endif() + +function(add_sanitizer_target _TARGET) + if (NOT TARGET ${_TARGET}) + add_library(${_TARGET} INTERFACE) + endif() + target_compile_options( + ${_TARGET} + INTERFACE + ${_BASE_SANITIZER_FLAGS} ${_BASE_ENABLE_SANITIZER_FLAGS} + ) + target_link_libraries( + ${_TARGET} + INTERFACE + ${_BASE_ENABLE_SANITIZER_FLAGS} + ) + + if(JINJA2CPP_INSTALL) + install( + TARGETS + ${_TARGET} + EXPORT + InstallTargets + ) + endif() +endfunction() + + diff --git a/cmake/sanitizer.memory.cmake b/cmake/sanitizer.memory.cmake new file mode 100644 index 00000000..91e3a703 --- /dev/null +++ b/cmake/sanitizer.memory.cmake @@ -0,0 +1 @@ +include(sanitizer) diff --git a/conanfile.txt b/conanfile.txt new file mode 100644 index 00000000..13be3027 --- /dev/null +++ b/conanfile.txt @@ -0,0 +1,13 @@ +[requires] +boost/1.85.0 +expected-lite/0.6.3 +fmt/10.1.1 +nlohmann_json/3.11.2 +optional-lite/3.5.0 +rapidjson/cci.20220822 +string-view-lite/1.7.0 +variant-lite/2.0.0 + +[generators] +CMakeDeps +CMakeToolchain diff --git a/include/jinja2cpp/binding/boost_json.h b/include/jinja2cpp/binding/boost_json.h new file mode 100644 index 00000000..93564694 --- /dev/null +++ b/include/jinja2cpp/binding/boost_json.h @@ -0,0 +1,218 @@ +#ifndef JINJA2CPP_BINDING_BOOST_JSON_H +#define JINJA2CPP_BINDING_BOOST_JSON_H + +#include +#include +#include + +namespace jinja2 +{ +namespace detail +{ + +class BoostJsonObjectAccessor + : public IMapItemAccessor + , public ReflectedDataHolder +{ + struct SizeVisitor + { + size_t operator()(std::nullptr_t) { return {}; } + size_t operator()(bool) { return 1; } + size_t operator()(std::int64_t) { return 1; } + size_t operator()(std::uint64_t) { return 1; } + size_t operator()(double) { return 1; } + size_t operator()(const boost::json::string&) { return 1; } + size_t operator()(const boost::json::array& val) { return val.size(); } + size_t operator()(const boost::json::object& val) { return val.size(); } + size_t operator()(...) { return 0; } + }; + +public: + using ReflectedDataHolder::ReflectedDataHolder; + ~BoostJsonObjectAccessor() override = default; + + size_t GetSize() const override + { + auto j = this->GetValue(); + if (!j) + return {}; + // simulate nlohmann semantics + SizeVisitor sv; + return boost::json::visit(sv, *j); + } + + bool HasValue(const std::string& name) const override + { + auto j = this->GetValue(); + if (!j) + return false; + auto obj = j->if_object(); + return obj ? obj->contains(name) : false; + } + + Value GetValueByName(const std::string& name) const override + { + auto j = this->GetValue(); + if (!j) + return Value(); + auto obj = j->if_object(); + if (!obj) + return Value(); + auto val = obj->if_contains(name); + if (!val) + return Value(); + return Reflect(*val); + } + + std::vector GetKeys() const override + { + auto j = this->GetValue(); + if (!j) + return {}; + auto obj = j->if_object(); + if (!obj) + return {}; + std::vector result; + result.reserve(obj->size()); + for (auto& item : *obj) + { + result.emplace_back(item.key()); + } + return result; + } + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return this->GetValue() == val->GetValue(); + } +}; + +struct BoostJsonArrayAccessor + : IListItemAccessor + , IIndexBasedAccessor + , ReflectedDataHolder +{ + using ReflectedDataHolder::ReflectedDataHolder; + + nonstd::optional GetSize() const override + { + auto j = this->GetValue(); + return j ? j->size() : nonstd::optional(); + } + + const IIndexBasedAccessor* GetIndexer() const override { return this; } + + ListEnumeratorPtr CreateEnumerator() const override + { + using Enum = Enumerator; + auto j = this->GetValue(); + if (!j) + return jinja2::ListEnumeratorPtr(); + + return jinja2::ListEnumeratorPtr(new Enum(j->begin(), j->end())); + } + + Value GetItemByIndex(int64_t idx) const override + { + auto j = this->GetValue(); + if (!j) + return Value(); + + return Reflect((*j)[idx]); + } + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return GetValue() == val->GetValue(); + } +}; + +template<> +struct Reflector +{ + static Value Create(boost::json::value val) + { + Value result; + switch (val.kind()) + { + default: // unreachable()? + case boost::json::kind::null: + break; + case boost::json::kind::bool_: + result = val.get_bool(); + break; + case boost::json::kind::int64: + result = val.get_int64(); + break; + case boost::json::kind::uint64: + result = static_cast(val.get_uint64()); + break; + case boost::json::kind::double_: + result = val.get_double(); + break; + case boost::json::kind::string: + result = std::string(val.get_string().c_str()); + break; + case boost::json::kind::array: { + auto array = val.get_array(); + result = GenericList([accessor = BoostJsonArrayAccessor(std::move(array))]() { return &accessor; }); + break; + } + case boost::json::kind::object: { + auto obj = val.get_object(); + result = GenericMap([accessor = BoostJsonObjectAccessor(std::move(val))]() { return &accessor; }); + break; + } + } + return result; + } + + static Value CreateFromPtr(const boost::json::value* val) + { + Value result; + switch (val->kind()) + { + default: // unreachable()? + case boost::json::kind::null: + break; + case boost::json::kind::bool_: + result = val->get_bool(); + break; + case boost::json::kind::int64: + result = val->get_int64(); + break; + case boost::json::kind::uint64: + result = static_cast(val->get_uint64()); + break; + case boost::json::kind::double_: + result = val->get_double(); + break; + case boost::json::kind::string: + result = std::string(val->get_string().c_str()); + break; + case boost::json::kind::array: + { + auto array = val->get_array(); + result = GenericList([accessor = BoostJsonArrayAccessor(std::move(array))]() { return &accessor; }); + break; + } + case boost::json::kind::object: + { + auto obj = val->get_object(); + result = GenericMap([accessor = BoostJsonObjectAccessor(std::move(val))]() { return &accessor; }); + break; + } + } + return result; + } +}; + +} // namespace detail +} // namespace jinja2 + +#endif // JINJA2CPP_BINDING_BOOST_JSON_H diff --git a/include/jinja2cpp/binding/nlohmann_json.h b/include/jinja2cpp/binding/nlohmann_json.h new file mode 100644 index 00000000..88403780 --- /dev/null +++ b/include/jinja2cpp/binding/nlohmann_json.h @@ -0,0 +1,184 @@ +#ifndef JINJA2CPP_BINDING_NLOHMANN_JSON_H +#define JINJA2CPP_BINDING_NLOHMANN_JSON_H + +#include + +#include + +namespace jinja2 +{ +namespace detail +{ + +class NLohmannJsonObjectAccessor : public IMapItemAccessor, public ReflectedDataHolder +{ +public: + using ReflectedDataHolder::ReflectedDataHolder; + ~NLohmannJsonObjectAccessor() override = default; + + size_t GetSize() const override + { + auto j = this->GetValue(); + return j ? j->size() : 0ULL; + } + + bool HasValue(const std::string& name) const override + { + auto j = this->GetValue(); + return j ? j->contains(name) : false; + } + + Value GetValueByName(const std::string& name) const override + { + auto j = this->GetValue(); + if (!j || !j->contains(name)) + return Value(); + + return Reflect(&(*j)[name]); + } + + std::vector GetKeys() const override + { + auto j = this->GetValue(); + if (!j) + return {}; + + std::vector result; + result.reserve(j->size()); + for (auto& item : j->items()) + { + result.emplace_back(item.key()); + } + return result; + } + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return GetValue() == val->GetValue(); + } +}; + + +struct NLohmannJsonArrayAccessor : IListItemAccessor, IIndexBasedAccessor, ReflectedDataHolder +{ + using ReflectedDataHolder::ReflectedDataHolder; + + nonstd::optional GetSize() const override + { + auto j = this->GetValue(); + return j ? j->size() : nonstd::optional(); + } + + const IIndexBasedAccessor* GetIndexer() const override + { + return this; + } + + ListEnumeratorPtr CreateEnumerator() const override + { + using Enum = Enumerator; + auto j = this->GetValue(); + if (!j) + return jinja2::ListEnumeratorPtr(); + + return jinja2::ListEnumeratorPtr(new Enum(j->begin(), j->end())); + } + + Value GetItemByIndex(int64_t idx) const override + { + auto j = this->GetValue(); + if (!j) + return Value(); + + return Reflect((*j)[idx]); + } + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return GetValue() == val->GetValue(); + } +}; + +template<> +struct Reflector +{ + static Value Create(nlohmann::json val) + { + Value result; + switch (val.type()) + { + case nlohmann::detail::value_t::binary: + break; + case nlohmann::detail::value_t::null: + break; + case nlohmann::detail::value_t::object: + result = GenericMap([accessor = NLohmannJsonObjectAccessor(std::move(val))]() { return &accessor; }); + break; + case nlohmann::detail::value_t::array: + result = GenericList([accessor = NLohmannJsonArrayAccessor(std::move(val))]() { return &accessor; }); + break; + case nlohmann::detail::value_t::string: + result = val.get(); + break; + case nlohmann::detail::value_t::boolean: + result = val.get(); + break; + case nlohmann::detail::value_t::number_integer: + case nlohmann::detail::value_t::number_unsigned: + result = val.get(); + break; + case nlohmann::detail::value_t::number_float: + result = val.get(); + break; + case nlohmann::detail::value_t::discarded: + break; + } + return result; + } + + static Value CreateFromPtr(const nlohmann::json *val) + { + Value result; + switch (val->type()) + { + case nlohmann::detail::value_t::binary: + break; + case nlohmann::detail::value_t::null: + break; + case nlohmann::detail::value_t::object: + result = GenericMap([accessor = NLohmannJsonObjectAccessor(val)]() { return &accessor; }); + break; + case nlohmann::detail::value_t::array: + result = GenericList([accessor = NLohmannJsonArrayAccessor(val)]() {return &accessor;}); + break; + case nlohmann::detail::value_t::string: + result = val->get(); + break; + case nlohmann::detail::value_t::boolean: + result = val->get(); + break; + case nlohmann::detail::value_t::number_integer: + case nlohmann::detail::value_t::number_unsigned: + result = val->get(); + break; + case nlohmann::detail::value_t::number_float: + result = val->get(); + break; + case nlohmann::detail::value_t::discarded: + break; + } + return result; + } + +}; + +} // namespace detail +} // namespace jinja2 + +#endif // JINJA2CPP_BINDING_NLOHMANN_JSON_H diff --git a/include/jinja2cpp/binding/rapid_json.h b/include/jinja2cpp/binding/rapid_json.h new file mode 100644 index 00000000..f29a1413 --- /dev/null +++ b/include/jinja2cpp/binding/rapid_json.h @@ -0,0 +1,201 @@ +#ifndef JINJA2CPP_BINDING_RAPID_JSON_H +#define JINJA2CPP_BINDING_RAPID_JSON_H + +#include +#include + +#include +#include + +namespace jinja2 +{ +namespace detail +{ + +template +struct RapidJsonNameConverter; + +template<> +struct RapidJsonNameConverter +{ + static const std::string& GetName(const std::string& str) { return str; } +}; + +template<> +struct RapidJsonNameConverter +{ + static std::wstring GetName(const std::string& str) { return ConvertString(str); } +}; + +template +class RapidJsonObjectAccessor : public IMapItemAccessor, public ReflectedDataHolder +{ +public: + using ReflectedDataHolder::ReflectedDataHolder; + using NameCvt = RapidJsonNameConverter; + using ThisType = RapidJsonObjectAccessor; + ~RapidJsonObjectAccessor() override = default; + + size_t GetSize() const override + { + auto j = this->GetValue(); + return j ? j->MemberCount() : 0ULL; + } + + bool HasValue(const std::string& name) const override + { + auto j = this->GetValue(); + return j ? j->HasMember(NameCvt::GetName(name).c_str()) : false; + } + + Value GetValueByName(const std::string& nameOrig) const override + { + auto j = this->GetValue(); + const auto& name = NameCvt::GetName(nameOrig); + if (!j || !j->HasMember(name.c_str())) + return Value(); + + return Reflect(&(*j)[name.c_str()]); + } + + std::vector GetKeys() const override + { + auto j = this->GetValue(); + if (!j) + return {}; + + std::vector result; + result.reserve(j->MemberCount()); + for (auto it = j->MemberBegin(); it != j->MemberEnd(); ++ it) + { + result.emplace_back(ConvertString(nonstd::basic_string_view(it->name.GetString()))); + } + return result; + } + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + auto enumerator = this->GetValue(); + auto otherEnum = val->GetValue(); + if (enumerator && otherEnum && enumerator != otherEnum) + return false; + if ((enumerator && !otherEnum) || (!enumerator && otherEnum)) + return false; + return true; + } +}; + +template +struct RapidJsonArrayAccessor + : IListItemAccessor + , IIndexBasedAccessor + , ReflectedDataHolder, false> +{ + using ReflectedDataHolder, false>::ReflectedDataHolder; + using ThisType = RapidJsonArrayAccessor; + + nonstd::optional GetSize() const override + { + auto j = this->GetValue(); + return j ? j->Size() : nonstd::optional(); + } + + const IIndexBasedAccessor* GetIndexer() const override + { + return this; + } + + ListEnumeratorPtr CreateEnumerator() const override + { + using Enum = Enumerator::ConstValueIterator>; + auto j = this->GetValue(); + if (!j) + return jinja2::ListEnumeratorPtr(); + + return jinja2::ListEnumeratorPtr(new Enum(j->Begin(), j->End())); + } + + Value GetItemByIndex(int64_t idx) const override + { + auto j = this->GetValue(); + if (!j) + return Value(); + + return Reflect((*j)[static_cast(idx)]); + } + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + auto enumerator = this->GetValue(); + auto otherEnum = val->GetValue(); + if (enumerator && otherEnum && enumerator != otherEnum) + return false; + if ((enumerator && !otherEnum) || (!enumerator && otherEnum)) + return false; + return true; + } +}; + +template +struct Reflector> +{ + static Value CreateFromPtr(const rapidjson::GenericValue* val) + { + Value result; + switch (val->GetType()) + { + case rapidjson::kNullType: + break; + case rapidjson::kFalseType: + result = Value(false); + break; + case rapidjson::kTrueType: + result = Value(true); + break; + case rapidjson::kObjectType: + result = GenericMap([accessor = RapidJsonObjectAccessor>(val)]() { return &accessor; }); + break; + case rapidjson::kArrayType: + result = GenericList([accessor = RapidJsonArrayAccessor(val)]() { return &accessor; }); + break; + case rapidjson::kStringType: + result = std::basic_string(val->GetString(), val->GetStringLength()); + break; + case rapidjson::kNumberType: + if (val->IsInt64() || val->IsUint64()) + result = val->GetInt64(); + else if (val->IsInt() || val->IsUint()) + result = val->GetInt(); + else + result = val->GetDouble(); + break; + } + return result; + } + +}; + +template +struct Reflector> +{ + static Value Create(const rapidjson::GenericDocument& val) + { + return GenericMap([accessor = RapidJsonObjectAccessor>(&val)]() { return &accessor; }); + } + + static Value CreateFromPtr(const rapidjson::GenericDocument* val) + { + return GenericMap([accessor = RapidJsonObjectAccessor>(val)]() { return &accessor; }); + } + +}; +} // namespace detail +} // namespace jinja2 + +#endif // JINJA2CPP_BINDING_RAPID_JSON_H diff --git a/include/jinja2cpp/config.h b/include/jinja2cpp/config.h new file mode 100644 index 00000000..23d53ab3 --- /dev/null +++ b/include/jinja2cpp/config.h @@ -0,0 +1,27 @@ +#ifndef JINJA2CPP_CONFIG_H +#define JINJA2CPP_CONFIG_H + +// The Jinja2C++ library version in the form major * 10000 + minor * 100 + patch. +#define JINJA2CPP_VERSION 10100 + +#ifdef _WIN32 +#define JINJA2_DECLSPEC(S) __declspec(S) +#if _MSC_VER +#pragma warning(disable : 4251) +#endif +#else +#define JINJA2_DECLSPEC(S) +#endif + +#ifdef JINJA2CPP_BUILD_AS_SHARED +#define JINJA2CPP_EXPORT JINJA2_DECLSPEC(dllexport) +#define JINJA2CPP_SHARED_LIB +#elif JINJA2CPP_LINK_AS_SHARED +#define JINJA2CPP_EXPORT JINJA2_DECLSPEC(dllimport) +#define JINJA2CPP_SHARED_LIB +#else +#define JINJA2CPP_EXPORT +#define JINJA2CPP_STATIC_LIB +#endif + +#endif // JINJA2CPP_CONFIG_H diff --git a/include/jinja2cpp/error_info.h b/include/jinja2cpp/error_info.h index 5043605c..d4c2f88d 100644 --- a/include/jinja2cpp/error_info.h +++ b/include/jinja2cpp/error_info.h @@ -1,47 +1,84 @@ #ifndef JINJA2CPP_ERROR_INFO_H #define JINJA2CPP_ERROR_INFO_H +#include "config.h" #include "value.h" -#include - #include +#include #include namespace jinja2 { +/*! + * \brief Type of the error + */ enum class ErrorCode { - Unspecified = 0, - UnexpectedException = 1, - YetUnsupported, - FileNotFound, - ExpectedStringLiteral = 1001, - ExpectedIdentifier, - ExpectedSquareBracket, - ExpectedRoundBracket, - ExpectedCurlyBracket, - ExpectedToken, - ExpectedExpression, - ExpectedEndOfStatement, - UnexpectedToken, - UnexpectedStatement, - UnexpectedCommentBegin, - UnexpectedCommentEnd, - UnexpectedExprBegin, - UnexpectedExprEnd, - UnexpectedStmtBegin, - UnexpectedStmtEnd + Unspecified = 0, //!< Error is unspecified + UnexpectedException = 1, //!< Generic exception occurred during template parsing or execution. ExtraParams[0] contains `what()` string of the exception + YetUnsupported, //!< Feature of the jinja2 specification which yet not supported + FileNotFound, //!< Requested file was not found. ExtraParams[0] contains name of the file + ExtensionDisabled, //!< Particular jinja2 extension disabled in the settings + TemplateEnvAbsent, //!< Template uses `extend`, `import`, `from` or `include` features but it's loaded without the template environment set + TemplateNotFound, //!< Template with the specified name was not found. ExtraParams[0] contains name of the file + TemplateNotParsed, //!< Template was not parsed + InvalidValueType, //!< Invalid type of the value in the particular context + InvalidTemplateName, //!< Invalid name of the template. ExtraParams[0] contains the name + MetadataParseError, //!< Invalid name of the template. ExtraParams[0] contains the name + ExpectedStringLiteral = 1001, //!< String literal expected + ExpectedIdentifier, //!< Identifier expected + ExpectedSquareBracket, //!< ']' expected + ExpectedRoundBracket, //!< ')' expected + ExpectedCurlyBracket, //!< '}' expected + ExpectedToken, //!< Specific token(s) expected. ExtraParams[0] contains the actual token, rest of ExtraParams contain set of expected tokens + ExpectedExpression, //!< Expression expected + ExpectedEndOfStatement, //!< End of statement expected. ExtraParams[0] contains the expected end of statement tag + ExpectedRawEnd, //!< {% endraw %} expected + ExpectedMetaEnd, //!< {% endmeta %} expected + UnexpectedToken, //!< Unexpected token. ExtraParams[0] contains the invalid token + UnexpectedStatement, //!< Unexpected statement. ExtraParams[0] contains the invalid statement tag + UnexpectedCommentBegin, //!< Unexpected comment block begin (`{#`) + UnexpectedCommentEnd, //!< Unexpected comment block end (`#}`) + UnexpectedExprBegin, //!< Unexpected expression block begin (`{{`) + UnexpectedExprEnd, //!< Unexpected expression block end (`}}`) + UnexpectedStmtBegin, //!< Unexpected statement block begin (`{%`) + UnexpectedStmtEnd, //!< Unexpected statement block end (`%}`) + UnexpectedRawBegin, //!< Unexpected raw block begin {% raw %} + UnexpectedRawEnd, //!< Unexpected raw block end {% endraw %} + UnexpectedMetaBegin, //!< Unexpected meta block begin {% meta %} + UnexpectedMetaEnd, //!< Unexpected meta block end {% endmeta %} }; +/*! + * \brief Information about the source location of the error + */ struct SourceLocation { + //! Name of the file std::string fileName; + //! Line number (1-based) unsigned line = 0; + //! Column number (1-based) unsigned col = 0; }; template +/*! + * \brief Detailed information about the parse-time or render-time error + * + * If template parsing or rendering fails the detailed error information is provided. Exact specialization of ErrorInfoTpl is an object which contains + * this information. Type of specialization depends on type of the template object: \ref ErrorInfo for \ref Template and \ref ErrorInfoW for \ref TemplateW. + * + * Detailed information about an error contains: + * - Error code + * - Error location + * - Other locations related to the error + * - Description of the location + * - Extra parameters of the error + * + * @tparam CharT Character type which was used in template parser + */ class ErrorInfoTpl { public: @@ -54,36 +91,85 @@ class ErrorInfoTpl std::basic_string locationDescr; }; + //! Default constructor ErrorInfoTpl() = default; - ErrorInfoTpl(Data data) + //! Initializing constructor from error description + explicit ErrorInfoTpl(Data data) : m_errorData(std::move(data)) {} + //! Copy constructor + ErrorInfoTpl(const ErrorInfoTpl&) = default; + //! Move constructor + ErrorInfoTpl(ErrorInfoTpl&& val) noexcept + : m_errorData(std::move(val.m_errorData)) + { } + + //! Destructor + ~ErrorInfoTpl() noexcept = default; + + //! Copy-assignment operator + ErrorInfoTpl& operator =(const ErrorInfoTpl&) = default; + //! Move-assignment operator + ErrorInfoTpl& operator =(ErrorInfoTpl&& val) noexcept + { + if (this == &val) + return *this; + + std::swap(m_errorData.code, val.m_errorData.code); + std::swap(m_errorData.srcLoc, val.m_errorData.srcLoc); + std::swap(m_errorData.relatedLocs, val.m_errorData.relatedLocs); + std::swap(m_errorData.extraParams, val.m_errorData.extraParams); + std::swap(m_errorData.locationDescr, val.m_errorData.locationDescr); + return *this; + } + + //! Return code of the error ErrorCode GetCode() const { return m_errorData.code; } + //! Return error location in the template file auto& GetErrorLocation() const { return m_errorData.srcLoc; } + //! Return locations, related to the main error location auto& GetRelatedLocations() const { return m_errorData.relatedLocs; } + /*! + * \brief Return location description + * + * Return string of two lines. First line is contents of the line with error. Second highlight the exact position within line. For instance: + * ``` + * {% for i in range(10) endfor%} + * ---^------- + * ``` + * + * @return Location description + */ const std::basic_string& GetLocationDescr() const { return m_errorData.locationDescr; } - auto& GetExtraParams() const - { - return m_errorData.extraParams; - } + /*! + * \brief Return extra params of the error + * + * Extra params is a additional details assiciated with the error. For instance, name of the file which wasn't opened + * + * @return Vector with extra error params + */ + auto& GetExtraParams() const { return m_errorData.extraParams; } + + //! Convert error to the detailed string representation + JINJA2CPP_EXPORT std::basic_string ToString() const; private: Data m_errorData; @@ -92,8 +178,8 @@ class ErrorInfoTpl using ErrorInfo = ErrorInfoTpl; using ErrorInfoW = ErrorInfoTpl; -std::ostream& operator << (std::ostream& os, const ErrorInfo& res); -std::wostream& operator << (std::wostream& os, const ErrorInfoW& res); -} // jinja2 +JINJA2CPP_EXPORT std::ostream& operator<<(std::ostream& os, const ErrorInfo& res); +JINJA2CPP_EXPORT std::wostream& operator<<(std::wostream& os, const ErrorInfoW& res); +} // namespace jinja2 #endif // JINJA2CPP_ERROR_INFO_H diff --git a/include/jinja2cpp/filesystem_handler.h b/include/jinja2cpp/filesystem_handler.h index bd39b15d..fedbfa5a 100644 --- a/include/jinja2cpp/filesystem_handler.h +++ b/include/jinja2cpp/filesystem_handler.h @@ -1,8 +1,14 @@ #ifndef JINJA2CPP_FILESYSTEM_HANDLER_H #define JINJA2CPP_FILESYSTEM_HANDLER_H -#include +#include "config.h" +#include + +#include +#include + +#include #include #include #include @@ -16,47 +22,180 @@ using FileStreamPtr = std::unique_ptr, void (*)(std::b using CharFileStreamPtr = FileStreamPtr; using WCharFileStreamPtr = FileStreamPtr; -class IFilesystemHandler +/*! + * \brief Generic interface to filesystem handlers (loaders) + * + * This interface should be implemented in order to provide custom file system handler. Interface provides most-common methods which are called by + * the template environment to load the particular template. `OpenStream` methods return the unique pointer to the generic `istream` object implementation. + * So, the exact type (ex. `ifstream`, `istringstream` etc.) of input stream is unspecified. In order to delete stream object correctly returned pointer + * provide the custom deleter which should properly delete the stream object. + */ +class JINJA2CPP_EXPORT IFilesystemHandler : public IComparable { public: + //! Destructor virtual ~IFilesystemHandler() = default; + /*! + * \brief Method is called to open the file with the specified name in 'narrow-char' mode. + * + * Method should return unique pointer to the std::istream object with custom deleter (\ref CharFileStreamPtr) . Deleter should properly delete pointee + * stream object. + * + * @param name Name of the file to open + * @return Opened stream object or empty pointer in case of any error + */ virtual CharFileStreamPtr OpenStream(const std::string& name) const = 0; + /*! + * \brief Method is called to open the file with the specified name in 'wide-char' mode. + * + * Method should return unique pointer to the std::wistream object with custom deleter (\ref WCharFileStreamPtr) . Deleter should properly delete pointee + * stream object. + * + * @param name Name of the file to open + * @return Opened stream object or empty pointer in case of any error + */ virtual WCharFileStreamPtr OpenWStream(const std::string& name) const = 0; + /*! + * \brief Method is called to obtain the modification date of the specified file (if applicable) + * + * If the underlaying filesystem supports retrival of the last modification date of the file this method should return this date when called. In other + * case it should return the empty optional object. Main purpose of this method is to help templates loader to determine the necessity of cached template + * reload + * + * @param name Name of the file to get the last modification date + * @return Last modification date (if applicable) or empty optional object otherwise + */ + virtual nonstd::optional GetLastModificationDate(const std::string& name) const = 0; }; using FilesystemHandlerPtr = std::shared_ptr; -class MemoryFileSystem : public IFilesystemHandler +/*! + * \brief Filesystem handler for files stored in memory + * + * This filesystem handler implements the simple dictionary object which maps name of the file to it's content. New files can be added by \ref AddFile + * methods. Content of the files automatically converted to narrow/wide strings representation if necessary. + */ +class JINJA2CPP_EXPORT MemoryFileSystem : public IFilesystemHandler { public: + /*! + * \brief Add new narrow-char "file" to the filesystem handler + * + * Adds new file entry to the internal dictionary object or overwrite the existing one. New entry contains the specified content of the file + * + * @param fileName Name of the file to add + * @param fileContent Content of the file to add + */ void AddFile(std::string fileName, std::string fileContent); + /*! + * \brief Add new wide-char "file" to the filesystem handler + * + * Adds new file entry to the internal dictionary object or overwrite the existing one. New entry contains the specified content of the file + * + * @param fileName Name of the file to add + * @param fileContent Content of the file to add + */ void AddFile(std::string fileName, std::wstring fileContent); CharFileStreamPtr OpenStream(const std::string& name) const override; WCharFileStreamPtr OpenWStream(const std::string& name) const override; + nonstd::optional GetLastModificationDate(const std::string& name) const override; + /*! + * \brief Compares to an object of the same type + * + * return true if equal + */ + bool IsEqual(const IComparable& other) const override; private: - using FileContent = boost::variant; - std::unordered_map m_filesMap; + struct FileContent + { + nonstd::optional narrowContent; + nonstd::optional wideContent; + bool operator==(const FileContent& other) const + { + if (narrowContent != other.narrowContent) + return false; + return wideContent == other.wideContent; + } + bool operator!=(const FileContent& other) const + { + return !(*this == other); + } + }; + mutable std::unordered_map m_filesMap; }; -class RealFileSystem : public IFilesystemHandler +/*! + * \brief Filesystem handler for files stored on the filesystem + * + * This filesystem handler is an interface to the real file system. Root directory for file name mapping provided as a constructor argument. Each name (path) of + * the file to open is appended to the root directory path and then passed to the stream open methods. + */ +class JINJA2CPP_EXPORT RealFileSystem : public IFilesystemHandler { public: - RealFileSystem(std::string rootFolder = "."); + /*! + * \brief Initializing/default constructor + * + * @param rootFolder Path to the root folder. This path is used as a root for every opened file + */ + explicit RealFileSystem(std::string rootFolder = "."); + /*! + * \brief Reset path to the root folder to the new value + * + * @param newRoot New path to the root folder + */ void SetRootFolder(std::string newRoot) { - m_rootFolder = newRoot; + m_rootFolder = std::move(newRoot); } + /*! + * \brief Get path to the current root folder + * + * @return + */ + const std::string& GetRootFolder() const + { + return m_rootFolder; + } + /*! + * \brief Get full path to the specified file. + * + * Appends specified name of the file to the current root folder and returns it + * + * @param name Name of the file to get path to + * @return Full path to the file + */ + std::string GetFullFilePath(const std::string& name) const; + CharFileStreamPtr OpenStream(const std::string& name) const override; WCharFileStreamPtr OpenWStream(const std::string& name) const override; + /*! + * \brief Open the specified file as a binary stream + * + * Opens the specified file in the binary mode (instead of text). + * + * @param name Name of the file to get the last modification date + * @return Last modification date (if applicable) or empty optional object otherwise + */ + CharFileStreamPtr OpenByteStream(const std::string& name) const; + nonstd::optional GetLastModificationDate(const std::string& name) const override; + + /*! + * \brief Compares to an object of the same type + * + * return true if equal + */ + bool IsEqual(const IComparable& other) const override; private: std::string m_rootFolder; }; -} // jinja2 +} // namespace jinja2 -#endif // FILESYSTEM_HANDLER_H +#endif // JINJA2CPP_FILESYSTEM_HANDLER_H diff --git a/include/jinja2cpp/generic_list.h b/include/jinja2cpp/generic_list.h new file mode 100644 index 00000000..94360012 --- /dev/null +++ b/include/jinja2cpp/generic_list.h @@ -0,0 +1,287 @@ +#ifndef JINJA2CPP_GENERIC_LIST_H +#define JINJA2CPP_GENERIC_LIST_H + +#include +#include + +#include + +#include +#include +#include + +namespace jinja2 +{ +class Value; + +/*! + * \brief Interface for accessing items in list by the indexes + * + * This interface should provided by the particular list implementation in case of support index-based access to the items. + */ +struct IIndexBasedAccessor : virtual IComparable +{ + virtual ~IIndexBasedAccessor() = default; + /*! + * \brief This method is called to get the item by the specified index + * + * @param idx Index of item to get + * + * @return requested item + */ + virtual Value GetItemByIndex(int64_t idx) const = 0; +}; + +struct IListEnumerator; +using ListEnumeratorPtr = types::ValuePtr; + +inline auto MakeEmptyListEnumeratorPtr() +{ + return ListEnumeratorPtr(); +} + +/*! + * \brief Generic list enumerator interface + * + * This interface should be implemented by the lists of any type. Interface is used to enumerate the list contents item by item. + * + * Implementation notes: Initial state of the enumerator should be "before the first item". So, the first call of \ref MoveNext method either moves the + * enumerator to the first element end returns `true` or moves enumerator to the end and returns `false` in case of empty list. Each call of \ref GetCurrent + * method should return the current enumerable item. + */ +struct IListEnumerator : virtual IComparable +{ + //! Destructor + virtual ~IListEnumerator() = default; + + /*! + * \brief Method is called to reset enumerator to the initial state ('before the first element') if applicable. + * + * For the sequences which allow multi-pass iteration this method should reset enumerator to the initial state. For the single-pass sequences method + * can do nothing. + */ + virtual void Reset() = 0; + + /*! + * \brief Method is called to move the enumerator to the next item (if any) + * + * @return `true` if enumerator successfully moved and `false` if enumerator reaches the end of the sequence. + */ + virtual bool MoveNext() = 0; + + /*! + * \brief Method is called to get the current item value. Can be called multiply times + * + * @return Value of the item if the current item is valid item and empty value otherwise + */ + virtual Value GetCurrent() const = 0; + + /*! + * \brief Method is called to make a deep **copy** of the current enumerator state if possible + * + * @return New enumerator object with copy of the current enumerator state or empty pointer if copying is not applicable to the enumerator + */ + virtual ListEnumeratorPtr Clone() const = 0; + + /*! + * \brief Method is called to transfer current enumerator state to the new object + * + * State of the enumerator after successful creation of the new object is unspecified by there is a guarantee that there will no calls to the 'moved' + * enumerator. Destruction of the moved enumerator shouldn't affect the newly created object. + * + * @return New enumerator object which holds and owns the current enumerator state + */ + virtual ListEnumeratorPtr Move() = 0; +}; + +/*! + * \brief Generic list implementation interface + * + * Every list implementation should implement this interface for providing access to the items of the list. There are several types of lists and implementation + * notes for every of them: + * - **Single-pass sequences** . For instance, input-stream iterators, generator-based sequences. This type of generic lists should provide no size and no indexer. Enumerator of such sequence supports should be moveable but non-copyable and non-resetable. + * - **Forward sequences** . For instance, single-linked lists. This type of lists should provide no size and indexer. Enumerator should be moveable, copyable and resetable. + * - **Bidirectional sequences**. Have got the same implementation requirements as forward sequences. + * - **Random-access sequences**. Such as arrays or vectors. Should provide valid size (number of stored items), valid indexer implementation and moveable, + * copyable and resetable enumerator. + * + * It's assumed that indexer interface is a part of list implementation. + */ +struct IListItemAccessor : virtual IComparable +{ + virtual ~IListItemAccessor() = default; + + /*! + * \brief Called to get pointer to indexer interface implementation (if applicable) + * + * See implementation notes for the interface. This method should return pointer to the valid indexer interface if (and only if) list implementation + * supports random access to the items. + * + * Method can be called several times from the different threads. + * + * @return Pointer to the indexer interface implementation or null if indexing isn't supported for the list + */ + virtual const IIndexBasedAccessor* GetIndexer() const = 0; + + /*! + * \brief Called to get enumerator of the particular list + * + * See implementation notes for the interface. This method should return unique pointer (with custom deleter) to the ListEnumerator interface implementation. Enumerator implementation should follow the requirements for the particular list type implementation. + * + * Method can be called several times from the different threads. + * + * @return Pointer to the enumerator of the list + */ + virtual ListEnumeratorPtr CreateEnumerator() const = 0; + + /*! + * \brief Called to get size of the list if applicable. + * + * See implementation notes for the interface. This method should return valid (non-empty) size only for random-access sequences. In other cases this + * method should return empty optional. + * + * Method can be called several times from the different threads. + * + * @return Non-empty optional with the valid size of the list or empty optional in case of non-random sequence implementation + */ + virtual nonstd::optional GetSize() const = 0; + + /*! + * \brief Helper factory method of particular enumerator implementation + * + * @tparam T Type of enumerator to create + * @tparam Args Type of enumerator construct args + * @param args Actual enumerator constructor args + * @return Unique pointer to the enumerator + */ + template + static ListEnumeratorPtr MakeEnumerator(Args&&... args); +}; + + +namespace detail +{ +class GenericListIterator; +} // namespace detail + +/*! + * \brief Facade class for generic lists + * + * This class holds the implementation of particular generic list interface and provides friendly access to it's method. Also this class is used to hold + * the particular list. Pointer to the generic list interface implementation is held inside std::function object which provides access to the pointer to the interface. + * + * You can use \ref MakeGenericList method to create instances of the GenericList: + * ``` + * std::array sampleList{10, 20, 30, 40, 50, 60, 70, 80, 90}; + * + * ValuesMap params = { + * {"input", jinja2::MakeGenericList(begin(sampleList), end(sampleList)) } + * }; + * ``` + */ +class JINJA2CPP_EXPORT GenericList +{ +public: + //! Default constructor + GenericList() = default; + + /*! + * \brief Initializing constructor + * + * This constructor is only one way to create the valid GenericList object. `accessor` is a functional object which provides access to the \ref IListItemAccessor + * interface. The most common way of GenericList creation is to initialize it with lambda which simultaniously holds and and provide access to the + * generic list implementation: + * + * ``` + * auto MakeGeneratedList(ListGenerator&& fn) + * { + * return GenericList([accessor = GeneratedListAccessor(std::move(fn))]() {return &accessor;}); + * } + * ``` + * + * @param accessor Functional object which provides access to the particular generic list implementation + */ + explicit GenericList(std::function accessor) + : m_accessor(std::move(accessor)) + { + } + + /*! + * \brief Get size of the list + * + * @return Actual size of the generic list or empty optional object if not applicable + */ + nonstd::optional GetSize() const + { + return m_accessor ? m_accessor()->GetSize() : nonstd::optional(); + } + + /*! + * \brief Get pointer to the list accessor interface implementation + * + * @return Pointer to the list accessor interface or nullptr in case of non-initialized GenericList object + */ + auto GetAccessor() const + { + return m_accessor ? m_accessor() : nullptr; + } + + /*! + * \brief Check the GenericList object state + * + * @return true if GenericList object is valid (initialize) or false otherwize + */ + bool IsValid() const + { + return !(!m_accessor); + } + + /*! + * \brief Get interator to the first element of the list + * + * @return Iterator to the first element of the generic list or iterator equal to the `end()` if list is empty or not initialized + */ + detail::GenericListIterator begin() const; + /*! + * \brief Get the end iterator + * + * @return 'end' iterator of the generic list + */ + detail::GenericListIterator end() const; + + /*! + * \brief Get interator to the first element of the list + * + * @return Iterator to the first element of the generic list or iterator equal to the `end()` if list is empty or not initialized + */ + auto cbegin() const; + /*! + * \brief Get the end iterator + * + * @return 'end' iterator of the generic list + */ + auto cend() const; + + /*! + * \brief Compares with the objects of same type + * + * @return true if equal + */ + bool IsEqual(const GenericList& rhs) const; + +private: + std::function m_accessor; +}; + +bool operator==(const GenericList& lhs, const GenericList& rhs); +bool operator!=(const GenericList& lhs, const GenericList& rhs); + +template +inline ListEnumeratorPtr IListItemAccessor::MakeEnumerator(Args&& ...args) +{ + return ListEnumeratorPtr(types::MakeValuePtr(std::forward(args)...)); +} +} // namespace jinja2 + + +#endif // JINJA2CPP_GENERIC_LIST_H diff --git a/include/jinja2cpp/generic_list_impl.h b/include/jinja2cpp/generic_list_impl.h new file mode 100644 index 00000000..5ea5124a --- /dev/null +++ b/include/jinja2cpp/generic_list_impl.h @@ -0,0 +1,477 @@ +#ifndef JINJA2CPP_GENERIC_LIST_IMPL_H +#define JINJA2CPP_GENERIC_LIST_IMPL_H + +#include "generic_list.h" +#include "value.h" + +#include + +namespace jinja2 +{ +namespace lists_impl +{ +template +struct InputIteratorListAccessor : IListItemAccessor +{ + mutable It1 m_begin; + mutable It2 m_end; + + struct Enumerator : public IListEnumerator + { + It1* m_cur = nullptr; + It2* m_end = nullptr; + bool m_justInited = true; + + Enumerator(It1* begin, It2* end) + : m_cur(begin) + , m_end(end) + {} + + void Reset() override + { + } + + bool MoveNext() override + { + if (m_justInited) + m_justInited = false; + else + ++ *m_cur; + + return (*m_cur) != (*m_end); + } + + Value GetCurrent() const override + { + return Reflect(**m_cur); + } + + ListEnumeratorPtr Clone() const override + { + auto result = MakeEnumerator(m_cur, m_end); + auto ptr = static_cast(&(*result)); + ptr->m_cur = m_cur; + ptr->m_justInited = m_justInited; + return result; + } + + ListEnumeratorPtr Move() override + { + return MakeEnumerator(std::move(*this)); + } + bool IsEqual(const IComparable &other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_cur != val->m_cur) + return false; + if (m_end != val->m_end) + return false; + if (m_justInited != val->m_justInited) + return false; + return true; + } + }; + + explicit InputIteratorListAccessor(It1&& b, It2&& e) noexcept + : m_begin(std::move(b)) + , m_end(std::move(e)) + { + } + + nonstd::optional GetSize() const override + { + return nonstd::optional(); + } + + const IIndexBasedAccessor* GetIndexer() const override + { + return nullptr; + } + + ListEnumeratorPtr CreateEnumerator() const override + { + return MakeEnumerator(&m_begin, &m_end ); + } + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return m_begin == val->m_begin && m_end == val->m_end; + } + +}; + +template +struct ForwardIteratorListAccessor : IListItemAccessor +{ + It1 m_begin; + It2 m_end; + + struct Enumerator : public IListEnumerator + { + It1 m_begin; + It1 m_cur; + It2 m_end; + bool m_justInited = true; + + Enumerator(It1 begin, It2 end) + : m_begin(begin) + , m_cur(end) + , m_end(end) + {} + + void Reset() override + { + m_justInited = true; + } + + bool MoveNext() override + { + if (m_justInited) + { + m_cur = m_begin; + m_justInited = false; + } + else + { + ++m_cur; + } + + return m_cur != m_end; + } + + Value GetCurrent() const override + { + return Reflect(*m_cur); + } + + ListEnumeratorPtr Clone() const override + { + auto result = MakeEnumerator(m_cur, m_end); + auto ptr = static_cast(&(*result)); + ptr->m_begin = m_cur; + ptr->m_cur = m_cur; + ptr->m_justInited = m_justInited; + return result; + } + + ListEnumeratorPtr Move() override + { + return MakeEnumerator(std::move(*this)); + } + bool IsEqual(const IComparable &other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_begin != val->m_begin) + return false; + if (m_cur != val->m_cur) + return false; + if (m_end != val->m_end) + return false; + if (m_justInited != val->m_justInited) + return false; + return true; + } + }; + + explicit ForwardIteratorListAccessor(It1&& b, It2&& e) noexcept + : m_begin(std::move(b)) + , m_end(std::move(e)) + { + } + + nonstd::optional GetSize() const override + { + return nonstd::optional(); + } + + const IIndexBasedAccessor* GetIndexer() const override + { + return nullptr; + } + + ListEnumeratorPtr CreateEnumerator() const override + { + return MakeEnumerator(m_begin, m_end); + } + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return m_begin == val->m_begin && m_end == val->m_end; + } +}; + +template +struct RandomIteratorListAccessor : IListItemAccessor, IIndexBasedAccessor +{ + It1 m_begin; + It2 m_end; + + struct Enumerator : public IListEnumerator + { + It1 m_begin; + It1 m_cur; + It2 m_end; + bool m_justInited = true; + + Enumerator(It1 begin, It2 end) + : m_begin(begin) + , m_cur(end) + , m_end(end) + {} + + void Reset() override + { + m_justInited = true; + } + + bool MoveNext() override + { + if (m_justInited) + { + m_cur = m_begin; + m_justInited = false; + } + else + { + ++ m_cur; + } + + return m_cur != m_end; + } + + Value GetCurrent() const override + { + return Reflect(*m_cur); + } + + ListEnumeratorPtr Clone() const override + { + auto result = MakeEnumerator(m_cur, m_end); + auto ptr = static_cast(&(*result)); + ptr->m_begin = m_cur; + ptr->m_cur = m_cur; + ptr->m_justInited = m_justInited; + return result; + } + + ListEnumeratorPtr Move() override + { + return MakeEnumerator(std::move(*this)); + } + bool IsEqual(const IComparable &other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_begin != val->m_begin) + return false; + if (m_cur != val->m_cur) + return false; + if (m_end != val->m_end) + return false; + if (m_justInited != val->m_justInited) + return false; + return true; + } + }; + + explicit RandomIteratorListAccessor(It1 b, It2 e) noexcept + : m_begin(std::move(b)) + , m_end(std::move(e)) + { + } + + nonstd::optional GetSize() const override + { + return std::distance(m_begin, m_end); + } + + const IIndexBasedAccessor* GetIndexer() const override + { + return this; + } + + ListEnumeratorPtr CreateEnumerator() const override + { + return MakeEnumerator(m_begin, m_end); + } + + + Value GetItemByIndex(int64_t idx) const override + { + auto p = m_begin; + std::advance(p, static_cast(idx)); + return Reflect(*p); + } + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return m_begin == val->m_begin && m_end == val->m_end; + } + +}; + +using ListGenerator = std::function()>; + +class GeneratedListAccessor : public IListItemAccessor +{ +public: + class Enumerator : public IListEnumerator + { + public: + Enumerator(const ListGenerator* fn) + : m_fn(fn) + { } + + void Reset() override + { + } + + bool MoveNext() override + { + if (m_isFinished) + return false; + + auto res = (*m_fn)(); + if (!res) + return false; + + m_current = std::move(*res); + + return true; + } + + Value GetCurrent() const override { return m_current; } + + ListEnumeratorPtr Clone() const override + { + return MakeEnumerator(*this); + } + + ListEnumeratorPtr Move() override + { + return MakeEnumerator(std::move(*this)); + } + + bool IsEqual(const IComparable &other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return m_fn == val->m_fn && m_current == val->m_current && m_isFinished == val->m_isFinished; + } + protected: + const ListGenerator* m_fn; + Value m_current; + bool m_isFinished = false; + + + }; + + explicit GeneratedListAccessor(ListGenerator&& fn) : m_fn(std::move(fn)) {} + + nonstd::optional GetSize() const override + { + return nonstd::optional(); + } + const IIndexBasedAccessor* GetIndexer() const override + { + return nullptr; + } + + ListEnumeratorPtr CreateEnumerator() const override + { + return MakeEnumerator(&m_fn); + } + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return m_fn() == val->m_fn(); + } + +private: + ListGenerator m_fn; +}; + +template +auto MakeGenericList(It1&& it1, It2&& it2, std::input_iterator_tag) +{ + return GenericList([accessor = InputIteratorListAccessor(std::forward(it1), std::forward(it2))]() {return &accessor;}); +} + +template +auto MakeGenericList(It1&& it1, It2&& it2, std::random_access_iterator_tag) +{ + return GenericList([accessor = RandomIteratorListAccessor(std::forward(it1), std::forward(it2))]() {return &accessor;}); +} + +template +auto MakeGenericList(It1&& it1, It2&& it2, Category) +{ + return GenericList([accessor = ForwardIteratorListAccessor(std::forward(it1), std::forward(it2))]() {return &accessor;}); +} + +auto MakeGeneratedList(ListGenerator&& fn) +{ + return GenericList([accessor = GeneratedListAccessor(std::move(fn))]() {return &accessor;}); +} +} // namespace lists_impl + +/*! + * \brief Create instance of the GenericList from the pair of iterators + * + * @tparam It1 Type of the first (begin) iterator + * @tparam It2 Type of the second (end) iterator + * @param it1 First (begin) iterator + * @param it2 Second (end) iterator + * @return Instance of GenericList object for the provided pair of iterators + */ +template +auto MakeGenericList(It1&& it1, It2&& it2) +{ + return lists_impl::MakeGenericList(std::forward(it1), std::forward(it2), typename std::iterator_traits::iterator_category()); +} + +/*! + * \brief Create instance of the GenericList from the generator method (generator-based generic list) + * + * List generator method should follow the function signature: nonstd::optional() . Non-empty optional returned from the generator means that generated + * list isn't empty yet. The first returned empty optional object means the end of the generated sequence. For instance: + * ``` + * jinja2::MakeGenericList([cur = 10]() mutable -> nonstd::optional { + * if (cur > 90) + * return nonstd::optional(); + * + * auto tmp = cur; + * cur += 10; + * return Value(tmp); +}); + * ``` + * This generator produces the following list: `[10, 20, 30, 40, 50, 60, 70, 80, 90]` + * + * @param fn Generator of the items sequences + * @return Instance of GenericList object for the provided generation method + */ +auto MakeGenericList(lists_impl::ListGenerator fn) +{ + return lists_impl::MakeGeneratedList(std::move(fn)); +} + +} // namespace jinja2 + +#endif // JINJA2CPP_GENERIC_LIST_IMPL_H diff --git a/include/jinja2cpp/generic_list_iterator.h b/include/jinja2cpp/generic_list_iterator.h new file mode 100644 index 00000000..7af469f8 --- /dev/null +++ b/include/jinja2cpp/generic_list_iterator.h @@ -0,0 +1,104 @@ +#ifndef JINJA2CPP_GENERIC_LIST_ITERATOR_H +#define JINJA2CPP_GENERIC_LIST_ITERATOR_H + +#include "generic_list.h" +#include "value.h" +#include "value_ptr.h" + +namespace jinja2 +{ +namespace detail +{ +class JINJA2CPP_EXPORT GenericListIterator +{ +public: + using iterator_category = std::input_iterator_tag; + using value_type = const Value; + using difference_type = std::ptrdiff_t; + using reference = const Value&; + using pointer = const Value*; + using EnumeratorPtr = types::ValuePtr; + + GenericListIterator() = default; + + GenericListIterator(ListEnumeratorPtr enumerator) + : m_enumerator(types::ValuePtr(enumerator)) + { + if (m_enumerator) + m_hasValue = m_enumerator->MoveNext(); + + if (m_hasValue) + m_current = m_enumerator->GetCurrent(); + } + + bool operator == (const GenericListIterator& other) const + { + if (m_hasValue != other.m_hasValue) + return false; + if (!m_enumerator && !other.m_enumerator) + return true; + if (this->m_enumerator && other.m_enumerator && !m_enumerator->IsEqual(*other.m_enumerator)) + return false; + if ((m_enumerator && !other.m_enumerator) || (!m_enumerator && other.m_enumerator)) + return false; + if (m_current != other.m_current) + return false; + return true; + } + + bool operator != (const GenericListIterator& other) const + { + return !(*this == other); + } + + reference operator *() const + { + return m_current; + } + + GenericListIterator& operator ++() + { + if (!m_enumerator) + return *this; + m_hasValue = m_enumerator->MoveNext(); + if (m_hasValue) + { + m_current = m_enumerator->GetCurrent(); + } + else + { + EnumeratorPtr temp; + Value tempVal; + using std::swap; + swap(m_enumerator, temp); + swap(m_current, tempVal); + } + + return *this; + } + + GenericListIterator operator++(int) + { + GenericListIterator result(std::move(m_current)); + + this->operator++(); + return result; + } +private: + explicit GenericListIterator(Value&& val) + : m_hasValue(true) + , m_current(std::move(val)) + { + + } + +private: + EnumeratorPtr m_enumerator; + bool m_hasValue = false; + Value m_current; +}; + +} // namespace detail +} // namespace jinja2 + +#endif // JINJA2CPP_GENERIC_LIST_ITERATOR_H diff --git a/include/jinja2cpp/polymorphic_value.h b/include/jinja2cpp/polymorphic_value.h new file mode 100644 index 00000000..c5cb86ac --- /dev/null +++ b/include/jinja2cpp/polymorphic_value.h @@ -0,0 +1,449 @@ +/* + +Copyright (c) 2016 Jonathan B. Coe + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#ifndef ISOCPP_P0201_POLYMORPHIC_VALUE_H_INCLUDED +#define ISOCPP_P0201_POLYMORPHIC_VALUE_H_INCLUDED + +#include +#include +#include +#include +#include +#include + +// required variant_CPP17_OR_GREATER definition +#include + +// +// in_place: code duplicated in any-lite, expected-lite, optional-lite, value-ptr-lite, variant-lite: +// + +#ifndef nonstd_lite_HAVE_IN_PLACE_TYPES +#define nonstd_lite_HAVE_IN_PLACE_TYPES 1 + +// C++17 std::in_place in : + +#if variant_CPP17_OR_GREATER + +#include + +namespace nonstd { + +using std::in_place; +using std::in_place_type; +using std::in_place_index; +using std::in_place_t; +using std::in_place_type_t; +using std::in_place_index_t; + +#define nonstd_lite_in_place_t( T) std::in_place_t +#define nonstd_lite_in_place_type_t( T) std::in_place_type_t +#define nonstd_lite_in_place_index_t(K) std::in_place_index_t + +#define nonstd_lite_in_place( T) std::in_place_t{} +#define nonstd_lite_in_place_type( T) std::in_place_type_t{} +#define nonstd_lite_in_place_index(K) std::in_place_index_t{} + +} // namespace nonstd + +#else // variant_CPP17_OR_GREATER + +#include + +namespace nonstd { +namespace detail { + +template< class T > +struct in_place_type_tag {}; + +template< std::size_t K > +struct in_place_index_tag {}; + +} // namespace detail + +struct in_place_t {}; + +template< class T > +inline in_place_t in_place( detail::in_place_type_tag = detail::in_place_type_tag() ) +{ + return in_place_t(); +} + +template< std::size_t K > +inline in_place_t in_place( detail::in_place_index_tag = detail::in_place_index_tag() ) +{ + return in_place_t(); +} + +template< class T > +inline in_place_t in_place_type( detail::in_place_type_tag = detail::in_place_type_tag() ) +{ + return in_place_t(); +} + +template< std::size_t K > +inline in_place_t in_place_index( detail::in_place_index_tag = detail::in_place_index_tag() ) +{ + return in_place_t(); +} + +// mimic templated typedef: + +#define nonstd_lite_in_place_t( T) nonstd::in_place_t(&)( nonstd::detail::in_place_type_tag ) +#define nonstd_lite_in_place_type_t( T) nonstd::in_place_t(&)( nonstd::detail::in_place_type_tag ) +#define nonstd_lite_in_place_index_t(K) nonstd::in_place_t(&)( nonstd::detail::in_place_index_tag ) + +#define nonstd_lite_in_place( T) nonstd::in_place_type +#define nonstd_lite_in_place_type( T) nonstd::in_place_type +#define nonstd_lite_in_place_index(K) nonstd::in_place_index + +} // namespace nonstd + +#endif // variant_CPP17_OR_GREATER +#endif // nonstd_lite_HAVE_IN_PLACE_TYPES + +namespace isocpp_p0201 { + +namespace detail { + +//////////////////////////////////////////////////////////////////////////// +// Implementation detail classes +//////////////////////////////////////////////////////////////////////////// + +template +struct default_copy { + T* operator()(const T& t) const { return new T(t); } +}; + +template +struct default_delete { + void operator()(const T* t) const { delete t; } +}; + +template +struct control_block { + virtual ~control_block() = default; + + virtual std::unique_ptr clone() const = 0; + + virtual T* ptr() = 0; +}; + +template +class direct_control_block : public control_block { + static_assert(!std::is_reference::value, ""); + U u_; + + public: + template + explicit direct_control_block(Ts&&... ts) : u_(U(std::forward(ts)...)) {} + + std::unique_ptr> clone() const override { + return std::make_unique(*this); + } + + T* ptr() override { return std::addressof(u_); } +}; + +template , + class D = default_delete> +class pointer_control_block : public control_block, public C { + std::unique_ptr p_; + + public: + explicit pointer_control_block(U* u, C c = C{}, D d = D{}) + : C(std::move(c)), p_(u, std::move(d)) {} + + explicit pointer_control_block(std::unique_ptr p, C c = C{}) + : C(std::move(c)), p_(std::move(p)) {} + + std::unique_ptr> clone() const override { + assert(p_); + return std::make_unique( + C::operator()(*p_), static_cast(*this), p_.get_deleter()); + } + + T* ptr() override { return p_.get(); } +}; + +template +class delegating_control_block : public control_block { + std::unique_ptr> delegate_; + + public: + explicit delegating_control_block(std::unique_ptr> b) + : delegate_(std::move(b)) {} + + std::unique_ptr> clone() const override { + return std::make_unique(delegate_->clone()); + } + + T* ptr() override { return delegate_->ptr(); } +}; + +} // end namespace detail + +class bad_polymorphic_value_construction : public std::exception { + public: + bad_polymorphic_value_construction() noexcept = default; + + const char* what() const noexcept override { + return "Dynamic and static type mismatch in polymorphic_value " + "construction"; + } +}; + +template +class polymorphic_value; + +template +struct is_polymorphic_value : std::false_type {}; + +template +struct is_polymorphic_value> : std::true_type {}; + +//////////////////////////////////////////////////////////////////////////////// +// `polymorphic_value` class definition +//////////////////////////////////////////////////////////////////////////////// + +template +class polymorphic_value { + static_assert(!std::is_union::value, ""); + static_assert(std::is_class::value, ""); + + template + friend class polymorphic_value; + + template + friend polymorphic_value make_polymorphic_value(Ts&&... ts); + template + friend polymorphic_value make_polymorphic_value(Ts&&... ts); + + T* ptr_ = nullptr; + std::unique_ptr> cb_; + + public: + // + // Destructor + // + + ~polymorphic_value() = default; + + // + // Constructors + // + + polymorphic_value() {} + + template , + class D = detail::default_delete, + class V = std::enable_if_t::value>> + explicit polymorphic_value(U* u, C copier = C{}, D deleter = D{}) { + if (!u) { + return; + } + +#ifndef ISOCPP_P0201_POLYMORPHIC_VALUE_NO_RTTI + if (std::is_same>::value && + std::is_same>::value && + typeid(*u) != typeid(U)) + throw bad_polymorphic_value_construction(); +#endif + std::unique_ptr p(u, std::move(deleter)); + + cb_ = std::make_unique>( + std::move(p), std::move(copier)); + ptr_ = u; + } + + // + // Copy-constructors + // + + polymorphic_value(const polymorphic_value& p) { + if (!p) { + return; + } + auto tmp_cb = p.cb_->clone(); + ptr_ = tmp_cb->ptr(); + cb_ = std::move(tmp_cb); + } + + // + // Move-constructors + // + + polymorphic_value(polymorphic_value&& p) noexcept { + ptr_ = p.ptr_; + cb_ = std::move(p.cb_); + p.ptr_ = nullptr; + } + + // + // Converting constructors + // + + template ::value && + std::is_convertible::value>> + explicit polymorphic_value(const polymorphic_value& p) { + polymorphic_value tmp(p); + ptr_ = tmp.ptr_; + cb_ = std::make_unique>( + std::move(tmp.cb_)); + } + + template ::value && + std::is_convertible::value>> + explicit polymorphic_value(polymorphic_value&& p) { + ptr_ = p.ptr_; + cb_ = std::make_unique>( + std::move(p.cb_)); + p.ptr_ = nullptr; + } + +#if __cplusplus < 201703L + +#endif + // + // In-place constructor + // + + template *, T*>::value && + !is_polymorphic_value>::value>, + class... Ts> + explicit polymorphic_value(nonstd_lite_in_place_type_t(U), Ts&&... ts) +// explicit polymorphic_value(std::in_place_type_t, Ts&&... ts) + : cb_(std::make_unique>( + std::forward(ts)...)) { + ptr_ = cb_->ptr(); + } + + + // + // Assignment + // + + polymorphic_value& operator=(const polymorphic_value& p) { + if (std::addressof(p) == this) { + return *this; + } + + if (!p) { + cb_.reset(); + ptr_ = nullptr; + return *this; + } + + auto tmp_cb = p.cb_->clone(); + ptr_ = tmp_cb->ptr(); + cb_ = std::move(tmp_cb); + return *this; + } + + // + // Move-assignment + // + + polymorphic_value& operator=(polymorphic_value&& p) noexcept { + if (std::addressof(p) == this) { + return *this; + } + + cb_ = std::move(p.cb_); + ptr_ = p.ptr_; + p.ptr_ = nullptr; + return *this; + } + + // + // Modifiers + // + + void swap(polymorphic_value& p) noexcept { + using std::swap; + swap(ptr_, p.ptr_); + swap(cb_, p.cb_); + } + + // + // Observers + // + + explicit operator bool() const { return bool(cb_); } + + const T* operator->() const { + assert(ptr_); + return ptr_; + } + + const T& operator*() const { + assert(*this); + return *ptr_; + } + + T* operator->() { + assert(*this); + return ptr_; + } + + T& operator*() { + assert(*this); + return *ptr_; + } +}; + +// +// polymorphic_value creation +// +template +polymorphic_value make_polymorphic_value(Ts&&... ts) { + polymorphic_value p; + p.cb_ = std::make_unique>( + std::forward(ts)...); + p.ptr_ = p.cb_->ptr(); + return p; +} +template +polymorphic_value make_polymorphic_value(Ts&&... ts) { + polymorphic_value p; + p.cb_ = std::make_unique>( + std::forward(ts)...); + p.ptr_ = p.cb_->ptr(); + return p; +} + +// +// non-member swap +// +template +void swap(polymorphic_value& t, polymorphic_value& u) noexcept { + t.swap(u); +} + +} // namespace isocpp_p0201 + +#endif // ISOCPP_P0201_POLYMORPHIC_VALUE_H_INCLUDED diff --git a/include/jinja2cpp/reflected_value.h b/include/jinja2cpp/reflected_value.h index a2813c3e..b6ee12fb 100644 --- a/include/jinja2cpp/reflected_value.h +++ b/include/jinja2cpp/reflected_value.h @@ -1,11 +1,64 @@ -#ifndef JINJA2_REFLECTED_VALUE_H -#define JINJA2_REFLECTED_VALUE_H +#ifndef JINJA2CPP_REFLECTED_VALUE_H +#define JINJA2CPP_REFLECTED_VALUE_H #include "value.h" +#include + +#include +#include +#include +#include +#include +#include + namespace jinja2 { +/*! + * \brief Reflect the arbitrary C++ type to the Jinja2C++ engine + * + * Generic method which reflects arbitrary C++ type to the Jinja2C++ template engine. The way of reflection depends on the actual reflected type and could be + * - Reflect as an exact value if the type is basic type (such as `char`, `int`, `double`, `std::string` etc.) + * - Reflect as a GenericList/GenericMap for standard containers respectively + * - Reflect as a GenericMap for the user types + * Also pointers/shared pointers to the types could be reflected. + * + * Reflected value takes ownership on the object, passed to the `Reflect` method by r-value reference or value. Actually, such object is moved. For const + * references or pointers reflected value holds the pointer to the reflected object. So, it's necessary to be sure that life time of the reflected object is + * longer than it's usage within the template. + * + * In order to reflect custom (user) the \ref jinja2::TypeReflection template should be specialized in the following way: + * ```c++ + * struct jinja2::TypeReflection : jinja2::TypeReflected + * { + * using FieldAccessor = typename jinja2::TypeReflected::FieldAccessor; + * static auto& GetAccessors() + * { + * static std::unordered_map accessors = { + * {"intValue", [](const TestStruct& obj) {assert(obj.isAlive); return jinja2::Reflect(obj.intValue);}}, + * {"intEvenValue", [](const TestStruct& obj) -> Value + * { + * assert(obj.isAlive); + * if (obj.intValue % 2) + * return {}; + * return {obj.intValue}; + * }}, + * }; + * + * return accessors; + * } + * }; + * + * `TestStruct` here is a type which should be reflected. Specialization of the \ref TypeReflection template should derived from \ref TypeReflected template + * and define only one method: `GetAccessors`. This method returns the unordered map object which maps field name (as a string) to the corresponded field + * accessor. And field accessor here is a lambda object which takes the reflected object reference and returns the value of the field from it. + * + * @tparam T Type of value to reflect + * @param val Value to reflect + * + * @return jinja2::Value which contains the reflected value or the empty one + */ template Value Reflect(T&& val); @@ -14,21 +67,25 @@ struct TypeReflectedImpl : std::integral_constant { }; +template +using FieldAccessor = std::function; + template struct TypeReflected : TypeReflectedImpl { - using FieldAccessor = std::function; + using FieldAccessor = jinja2::FieldAccessor; }; -template +template struct TypeReflection : TypeReflectedImpl { }; +#ifndef JINJA2CPP_NO_DOXYGEN template -class ReflectedMapImplBase : public MapItemAccessor +class ReflectedMapImplBase : public IMapItemAccessor { public: bool HasValue(const std::string& name) const override @@ -57,37 +114,70 @@ class ReflectedMapImplBase : public MapItemAccessor { return Derived::GetAccessors().size(); } - virtual Value GetValueByIndex(int64_t idx) const override +}; + +template +class ReflectedDataHolder; + +template +class ReflectedDataHolder +{ +public: + explicit ReflectedDataHolder(T val) : m_value(std::move(val)) {} + explicit ReflectedDataHolder(const T* val) : m_valuePtr(val) {} + +protected: + const T* GetValue() const { - const auto& accessors = Derived::GetAccessors(); - auto p = accessors.begin(); - std::advance(p, idx); + return m_valuePtr ? m_valuePtr : (m_value ? &m_value.value() : nullptr); + } - ValuesMap result; - result["key"] = p->first; - result["value"] = static_cast(this)->GetField(p->second); +private: + nonstd::optional m_value; + const T* m_valuePtr = nullptr; +}; - return result; +template +class ReflectedDataHolder +{ +public: + explicit ReflectedDataHolder(const T* val) : m_valuePtr(val) {} + +protected: + const T* GetValue() const + { + return m_valuePtr; } + +private: + const T* m_valuePtr = nullptr; }; template -class ReflectedMapImpl : public ReflectedMapImplBase> +class ReflectedMapImpl : public ReflectedMapImplBase>, public ReflectedDataHolder { public: - ReflectedMapImpl(T val) : m_value(val) {} - ReflectedMapImpl(const T* val) : m_valuePtr(val) {} + using ReflectedDataHolder::ReflectedDataHolder; + using ThisType = ReflectedMapImpl; static auto GetAccessors() {return TypeReflection::GetAccessors();} template Value GetField(Fn&& accessor) const { - return accessor(m_valuePtr ? *m_valuePtr : m_value); + auto v = this->GetValue(); + if (!v) + return Value(); + return accessor(*v); } -private: - T m_value; - const T* m_valuePtr = nullptr; + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + + return this->GetValue() == val->GetValue(); + } }; namespace detail @@ -98,14 +188,100 @@ struct Reflector; template using IsReflectedType = std::enable_if_t::value>; -// using IsReflectedType = std::enable_if_t::GetAccessors())::key_type, std::string>::value>; -// using IsReflectedType = typename Type2Void::GetAccessors())>::key_type>::type; +template +struct Enumerator : public IListEnumerator +{ + using ThisType = Enumerator; + It m_begin; + It m_cur; + It m_end; + bool m_justInited = true; + + Enumerator(It begin, It end) + : m_begin(begin) + , m_cur(end) + , m_end(end) + {} + + void Reset() override + { + m_justInited = true; + } + + bool MoveNext() override + { + if (m_justInited) + { + m_cur = m_begin; + m_justInited = false; + } + else + { + ++ m_cur; + } + + return m_cur != m_end; + } + + Value GetCurrent() const override + { + return Reflect(*m_cur); + } + + ListEnumeratorPtr Clone() const override + { + auto result = std::make_unique>(m_begin, m_end); + result->m_cur = m_cur; + result->m_justInited = m_justInited; + return jinja2::ListEnumeratorPtr(result.release()); //, Deleter); + } + + ListEnumeratorPtr Move() override + { + auto result = std::make_unique>(m_begin, m_end); + result->m_cur = std::move(m_cur); + result->m_justInited = m_justInited; + this->m_justInited = true; + return jinja2::ListEnumeratorPtr(result.release()); //, Deleter); + } + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_begin != val->m_begin) + return false; + if (m_cur != val->m_cur) + return false; + if (m_end != val->m_end) + return false; + if (m_justInited != val->m_justInited) + return false; + return true; + } + + /* + It m_begin; + It m_cur; + It m_end; + bool m_justInited = true; +*/ +/* + static void Deleter(IListEnumerator* e) + { + delete static_cast*>(e); + } + */ +}; struct ContainerReflector { template - struct ValueItemAccessor : ListItemAccessor + struct ValueItemAccessor : IListItemAccessor, IIndexBasedAccessor { + using ThisType = ValueItemAccessor; + T m_value; explicit ValueItemAccessor(T&& cont) noexcept @@ -113,43 +289,92 @@ struct ContainerReflector { } - size_t GetSize() const override + nonstd::optional GetSize() const override { return m_value.size(); } - Value GetValueByIndex(int64_t idx) const override + + const IIndexBasedAccessor* GetIndexer() const override + { + return this; + } + + ListEnumeratorPtr CreateEnumerator() const override + { + using Enum = Enumerator; + return jinja2::ListEnumeratorPtr(new Enum(m_value.begin(), m_value.end()));//, Enum::Deleter); + } + + Value GetItemByIndex(int64_t idx) const override { auto p = m_value.begin(); std::advance(p, static_cast(idx)); return Reflect(*p); } + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + auto enumerator = CreateEnumerator(); + auto otherEnum = val->CreateEnumerator(); + if (enumerator && otherEnum && !enumerator->IsEqual(*otherEnum)) + return false; + return true; + } }; template - struct PtrItemAccessor : ListItemAccessor + struct PtrItemAccessor : IListItemAccessor, IIndexBasedAccessor { - const T* m_value; + using ThisType = PtrItemAccessor; + + const T* m_value{}; - PtrItemAccessor(const T* ptr) + explicit PtrItemAccessor(const T* ptr) : m_value(ptr) { } - size_t GetSize() const override + nonstd::optional GetSize() const override { return m_value->size(); } - Value GetValueByIndex(int64_t idx) const override + const IIndexBasedAccessor* GetIndexer() const override + { + return this; + } + + ListEnumeratorPtr CreateEnumerator() const override + { + using Enum = Enumerator; + return jinja2::ListEnumeratorPtr(new Enum(m_value->begin(), m_value->end()));//, Enum::Deleter); + } + + Value GetItemByIndex(int64_t idx) const override { auto p = m_value->begin(); - std::advance(p, idx); + std::advance(p, static_cast(idx)); return Reflect(*p); } + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + auto enumerator = CreateEnumerator(); + auto otherEnum = val->CreateEnumerator(); + if (enumerator && otherEnum && !enumerator->IsEqual(*otherEnum)) + return false; + return true; + } }; template static Value CreateFromValue(T&& cont) { - return GenericList([accessor = ValueItemAccessor(std::move(cont))]() {return &accessor;}); + return GenericList([accessor = ValueItemAccessor(std::forward(cont))]() {return &accessor;}); } template @@ -273,10 +498,6 @@ struct Reflector { return Reflector::CreateFromPtr(val); } -// static auto CreateFromPtr(const T*const val) -// { -// return Reflector::CreateFromPtr(val); -// } }; template @@ -297,19 +518,24 @@ struct Reflector> } }; -template<> -struct Reflector +template +struct Reflector> { - static auto Create(std::string str) - { + static auto Create(std::basic_string str) { return Value(std::move(str)); } - static auto CreateFromPtr(const std::string* str) - { + static auto CreateFromPtr(const std::basic_string* str) { return Value(*str); } }; +template +struct Reflector> +{ + static auto Create(nonstd::basic_string_view str) { return Value(std::move(str)); } + static auto CreateFromPtr(const nonstd::basic_string_view* str) { return Value(*str); } +}; + template<> struct Reflector { @@ -323,6 +549,20 @@ struct Reflector } }; +template<> +struct Reflector +{ + static auto Create(double val) { return Value(val); } + static auto CreateFromPtr(const float* val) { return Value(static_cast(*val)); } +}; + +template<> +struct Reflector +{ + static auto Create(double val) { return Value(val); } + static auto CreateFromPtr(const double* val) { return Value(*val); } +}; + #define JINJA2_INT_REFLECTOR(Type) \ template<> \ struct Reflector \ @@ -337,6 +577,8 @@ struct Reflector \ } \ } +JINJA2_INT_REFLECTOR(char); +JINJA2_INT_REFLECTOR(wchar_t); JINJA2_INT_REFLECTOR(int8_t); JINJA2_INT_REFLECTOR(uint8_t); JINJA2_INT_REFLECTOR(int16_t); @@ -345,15 +587,15 @@ JINJA2_INT_REFLECTOR(int32_t); JINJA2_INT_REFLECTOR(uint32_t); JINJA2_INT_REFLECTOR(int64_t); JINJA2_INT_REFLECTOR(uint64_t); -} // detail +} // namespace detail +#endif template Value Reflect(T&& val) { return detail::Reflector::Create(std::forward(val)); - // return Value(ReflectedMap([accessor = ReflectedMapImpl(std::forward(val))]() -> const ReflectedMap::ItemAccessor* {return &accessor;})); } -} // jinja2 +} // namespace jinja2 -#endif // JINJA2_REFLECTED_VALUE_H +#endif // JINJA2CPP_REFLECTED_VALUE_H diff --git a/include/jinja2cpp/string_helpers.h b/include/jinja2cpp/string_helpers.h new file mode 100644 index 00000000..e6ac08a4 --- /dev/null +++ b/include/jinja2cpp/string_helpers.h @@ -0,0 +1,296 @@ +#ifndef JINJA2CPP_STRING_HELPERS_H +#define JINJA2CPP_STRING_HELPERS_H + +#include "value.h" + +#include + +#include +#include +#include + +namespace jinja2 +{ +namespace detail +{ + template + struct StringConverter; + + template + struct StringConverter + { + static Src DoConvert(const nonstd::basic_string_view& from) + { + return Src(from.begin(), from.end()); + } + }; + + template<> + struct StringConverter + { + static std::string DoConvert(const nonstd::wstring_view& from) + { + std::mbstate_t state = std::mbstate_t(); + auto srcPtr = from.data(); + std::size_t srcSize = from.size(); + std::size_t destBytes = 0; + +#ifndef _MSC_VER + destBytes = std::wcsrtombs(nullptr, &srcPtr, srcSize, &state); + if (destBytes == static_cast(-1)) + return std::string(); +#else + auto err = wcsrtombs_s(&destBytes, nullptr, 0, &srcPtr, srcSize, &state); + if (err != 0) + return std::string(); +#endif + std::string result; +#ifndef _MSC_VER + result.resize(destBytes + 1); + auto converted = std::wcsrtombs(&result[0], &srcPtr, srcSize, &state); + if (converted == static_cast(-1)) + return std::string(); + result.resize(converted); +#else + result.resize(destBytes); + wcsrtombs_s(&destBytes, &result[0], destBytes, &srcPtr, srcSize, &state); + result.resize(destBytes - 1); +#endif + return result; + } + }; + + template<> + struct StringConverter + { + static std::wstring DoConvert(const nonstd::string_view& from) + { + std::mbstate_t state = std::mbstate_t(); + auto srcPtr = from.data(); + std::size_t srcSize = from.size(); + std::size_t destBytes = 0; + +#ifndef _MSC_VER + destBytes = std::mbsrtowcs(nullptr, &srcPtr, srcSize, &state); + if (destBytes == static_cast(-1)) + return std::wstring(); +#else + auto err = mbsrtowcs_s(&destBytes, nullptr, 0, &srcPtr, srcSize, &state); + if (err != 0) + return std::wstring(); +#endif + std::wstring result; +#ifndef _MSC_VER + result.resize(destBytes + 1); + srcPtr = from.data(); + auto converted = std::mbsrtowcs(&result[0], &srcPtr, srcSize, &state); + if (converted == static_cast(-1)) + return std::wstring(); + result.resize(converted); +#else + result.resize(destBytes); + mbsrtowcs_s(&destBytes, &result[0], destBytes, &srcPtr, srcSize, &state); + result.resize(destBytes - 1); +#endif + return result; + } + }; + + template + struct StringConverter, T> : public StringConverter, T> {}; + +} // namespace detail + +/*! + * \brief Convert string objects from one representation or another + * + * Converts string or string views to string objects with possible char conversion (char -> wchar_t or wchar_t -> char). + * This function should be used when exact type of string is needed. + * + * @tparam Dst Destination string type. Mandatory. Can be std::string or std::wstring + * @tparam Src Source string type. Auto detected. Can be either std::basic_string or nonstd::string_view + * + * @param from Source string object which should be converted + * @return Destination string object of the specified type + */ +template +Dst ConvertString(Src&& from) +{ + using src_t = std::decay_t; + return detail::StringConverter>::DoConvert(nonstd::basic_string_view(from)); +} + +/*! + * \brief Gets std::string from std::string + * + * Helper method for use in template context which gets std::string from the other possible string objects (std::string in this case) + * + * @param str Source string + * @return Copy of the source string + */ +inline const std::string AsString(const std::string& str) +{ + return str; +} +/*! + * \brief Gets std::string from std::wstring + * + * Helper method for use in template context which gets std::string from the other possible string objects (std::wstring in this case) + * Conversion wchar_t -> char is performing + * + * @param str Source string + * @return Converted source string + */ +inline std::string AsString(const std::wstring& str) +{ + return ConvertString(str); +} +/*! + * \brief Gets std::string from nonstd::string_view + * + * Helper method for use in template context which gets std::string from the other possible string objects (nonstd::string_view in this case) + * + * @param str Source string + * @return Copy of the source string + */ +inline std::string AsString(const nonstd::string_view& str) +{ + return std::string(str.begin(), str.end()); +} +/*! + * \brief Gets std::string from nonstd::wstring_view + * + * Helper method for use in template context which gets std::string from the other possible string objects (nonstd::wstring_view in this case) + * Conversion wchar_t -> char is performing + * + * @param str Source string + * @return Converted source string + */ +inline std::string AsString(const nonstd::wstring_view& str) +{ + return ConvertString(str); +} +/*! + * \brief Gets std::wstring from std::wstring + * + * Helper method for use in template context which gets std::wstring from the other possible string objects (std::wstring in this case) + * + * @param str Source string + * @return Copy of the source string + */ +inline const std::wstring AsWString(const std::wstring& str) +{ + return str; +} +/*! + * \brief Gets std::wstring from std::string + * + * Helper method for use in template context which gets std::wstring from the other possible string objects (std::string in this case) + * Conversion char -> wchar_t is performing + * + * @param str Source string + * @return Converted source string + */ +inline std::wstring AsWString(const std::string& str) +{ + return ConvertString(str); +} +/*! + * \brief Gets std::wstring from nonstd::wstring_view + * + * Helper method for use in template context which gets std::wstring from the other possible string objects (nonstd::wstring_view in this case) + * + * @param str Source string + * @return Copy of the source string + */ +inline std::wstring AsWString(const nonstd::wstring_view& str) +{ + return std::wstring(str.begin(), str.end()); +} +/*! + * \brief Gets std::wstring from nonstd::string_view + * + * Helper method for use in template context which gets std::wstring from the other possible string objects (nonstd::string_view in this case) + * Conversion char -> wchar_t is performing + * + * @param str Source string + * @return Converted source string + */ +inline std::wstring AsWString(const nonstd::string_view& str) +{ + return ConvertString(str); +} + +namespace detail +{ +struct StringGetter +{ + template + std::string operator()(const std::basic_string& str) const + { + return AsString(str); + } + template + std::string operator()(const nonstd::basic_string_view& str) const + { + return AsString(str); + } + + template + std::string operator()(T&&) const + { + return std::string(); + } +}; + +struct WStringGetter +{ + template + std::wstring operator()(const std::basic_string& str) const + { + return AsWString(str); + } + template + std::wstring operator()(const nonstd::basic_string_view& str) const + { + return AsWString(str); + } + + template + std::wstring operator()(T&&) const + { + return std::wstring(); + } +}; +} // namespace detail +/*! + * \brief Gets std::string from the arbitrary \ref Value + * + * Helper method for use in template context which gets std::string from the other possible string objects (Value in this case). + * Conversion wchar_t -> char is performing if needed. In case of non-string object actually stored in the \ref Value + * empty string is returned. + * + * @param val Source string + * @return Extracted or empty string + */ +inline std::string AsString(const Value& val) +{ + return nonstd::visit(detail::StringGetter(), val.data()); +} +/*! + * \brief Gets std::wstring from the arbitrary \ref Value + * + * Helper method for use in template context which gets std::wstring from the other possible string objects (Value in this case). + * Conversion char -> wchar_t is performing if needed. In case of non-string object actually stored in the \ref Value + * empty string is returned. + * + * @param val Source string + * @return Extracted or empty string + */ +inline std::wstring AsWString(const Value& val) +{ + return nonstd::visit(detail::WStringGetter(), val.data()); +} +} // namespace jinja2 + +#endif // JINJA2CPP_STRING_HELPERS_H diff --git a/include/jinja2cpp/template.h b/include/jinja2cpp/template.h index 8ce99791..687fc060 100644 --- a/include/jinja2cpp/template.h +++ b/include/jinja2cpp/template.h @@ -1,61 +1,305 @@ -#ifndef JINJA2_TEMPLATE_H -#define JINJA2_TEMPLATE_H +#ifndef JINJA2CPP_TEMPLATE_H +#define JINJA2CPP_TEMPLATE_H +#include "config.h" #include "error_info.h" #include "value.h" #include -#include #include #include +#include namespace jinja2 { -class ITemplateImpl; -class TemplateEnv; -template class TemplateImpl; -using ParseResult = nonstd::expected; -using ParseResultW = nonstd::expected; +class JINJA2CPP_EXPORT ITemplateImpl; +class JINJA2CPP_EXPORT TemplateEnv; +template +class TemplateImpl; +template +using Result = nonstd::expected; +template +using ResultW = nonstd::expected; + +template +struct MetadataInfo +{ + std::string metadataType; + nonstd::basic_string_view metadata; + SourceLocation location; +}; -class Template +/*! + * \brief Template object which is used to render narrow char templates + * + * This class is a main class for rendering narrow char templates. It can be used independently or together with + * \ref TemplateEnv. In the second case it's possible to use templates inheritance and extension. + * + * Basic usage of Template class: + * ```c++ + * std::string source = "Hello World from Parser!"; + * + * jinja2::Template tpl; + * tpl.Load(source); + * std::string result = tpl.RenderAsString(ValuesMap{}).value(); + * ``` + */ +class JINJA2CPP_EXPORT Template { public: - Template(TemplateEnv* env = nullptr); + /*! + * \brief Default constructor + */ + Template() + : Template(nullptr) + { + } + /*! + * \brief Initializing constructor + * + * Creates instance of the template with the specified template environment object + * + * @param env Template environment object which created template should refer to + */ + explicit Template(TemplateEnv* env); + /*! + * Destructor + */ ~Template(); - ParseResult Load(const char* tpl, std::string tplName = std::string()); - ParseResult Load(const std::string& str, std::string tplName = std::string()); - ParseResult Load(std::istream& stream, std::string tplName = std::string()); - ParseResult LoadFromFile(const std::string& fileName); + /*! + * \brief Load template from the zero-terminated narrow char string + * + * Takes specified narrow char string and parses it as a Jinja2 template. In case of error returns detailed + * diagnostic + * + * @param tpl Zero-terminated narrow char string with template description + * @param tplName Optional name of the template (for the error reporting purposes) + * + * @return Either noting or instance of \ref ErrorInfoTpl as an error + */ + Result Load(const char* tpl, std::string tplName = std::string()); + /*! + * \brief Load template from the std::string + * + * Takes specified std::string object and parses it as a Jinja2 template. In case of error returns detailed + * diagnostic + * + * @param str std::string object with template description + * @param tplName Optional name of the template (for the error reporting purposes) + * + * @return Either noting or instance of \ref ErrorInfoTpl as an error + */ + Result Load(const std::string& str, std::string tplName = std::string()); + /*! + * \brief Load template from the stream + * + * Takes specified stream object and parses it as a source of Jinja2 template. In case of error returns detailed + * diagnostic + * + * @param stream Stream object with template description + * @param tplName Optional name of the template (for the error reporting purposes) + * + * @return Either noting or instance of \ref ErrorInfoTpl as an error + */ + Result Load(std::istream& stream, std::string tplName = std::string()); + /*! + * \brief Load template from the specified file + * + * Loads file with the specified name and parses it as a source of Jinja2 template. In case of error returns + * detailed diagnostic + * + * @param fileName Name of the file to load + * + * @return Either noting or instance of \ref ErrorInfoTpl as an error + */ + Result LoadFromFile(const std::string& fileName); - void Render(std::ostream& os, const ValuesMap& params); - std::string RenderAsString(const ValuesMap& params); + /*! + * \brief Render previously loaded template to the narrow char stream + * + * Renders previously loaded template to the specified narrow char stream and specified set of params. + * + * @param os Stream to render template to + * @param params Set of params which should be passed to the template engine and can be used within the template + * + * @return Either noting or instance of \ref ErrorInfoTpl as an error + */ + Result Render(std::ostream& os, const ValuesMap& params); + /*! + * \brief Render previously loaded template to the narrow char string + * + * Renders previously loaded template as a narrow char string and with specified set of params. + * + * @param params Set of params which should be passed to the template engine and can be used within the template + * + * @return Either rendered string or instance of \ref ErrorInfoTpl as an error + */ + Result RenderAsString(const ValuesMap& params); + /*! + * \brief Get metadata, provided in the {% meta %} tag + * + * @return Parsed metadata as a generic map value or instance of \ref ErrorInfoTpl as an error + */ + Result GetMetadata(); + /*! + * \brief Get non-parsed metadata, provided in the {% meta %} tag + * + * @return Non-parsed metadata information or instance of \ref ErrorInfoTpl as an error + */ + Result> GetMetadataRaw(); + + /* ! + * \brief compares to an other object of the same type + * + * @return true if equal + */ + bool IsEqual(const Template& other) const; private: std::shared_ptr m_impl; friend class TemplateImpl; }; +bool operator==(const Template& lhs, const Template& rhs); +bool operator!=(const Template& lhs, const Template& rhs); -class TemplateW +/*! + * \brief Template object which is used to render wide char templates + * + * This class is a main class for rendering wide char templates. It can be used independently or together with + * \ref TemplateEnv. In the second case it's possible to use templates inheritance and extension. + * + * Basic usage of Template class: + * ```c++ + * std::string source = "Hello World from Parser!"; + * + * jinja2::Template tpl; + * tpl.Load(source); + * std::string result = tpl.RenderAsString(ValuesMap{}).value(); + * ``` +*/ +class JINJA2CPP_EXPORT TemplateW { public: - TemplateW(TemplateEnv* env = nullptr); + /*! + * \brief Default constructor + */ + TemplateW() + : TemplateW(nullptr) + { + } + /*! + * \brief Initializing constructor + * + * Creates instance of the template with the specified template environment object + * + * @param env Template environment object which created template should refer to + */ + explicit TemplateW(TemplateEnv* env); + /*! + * Destructor + */ ~TemplateW(); - ParseResultW Load(const wchar_t* tpl, std::string tplName = std::string()); - ParseResultW Load(const std::wstring& str, std::string tplName = std::string()); - ParseResultW Load(std::wistream& stream, std::string tplName = std::string()); - ParseResultW LoadFromFile(const std::string& fileName); + /*! + * \brief Load template from the zero-terminated wide char string + * + * Takes specified wide char string and parses it as a Jinja2 template. In case of error returns detailed + * diagnostic + * + * @param tpl Zero-terminated wide char string with template description + * @param tplName Optional name of the template (for the error reporting purposes) + * + * @return Either noting or instance of \ref ErrorInfoTpl as an error + */ + ResultW Load(const wchar_t* tpl, std::string tplName = std::string()); + /*! + * \brief Load template from the std::wstring + * + * Takes specified std::wstring object and parses it as a Jinja2 template. In case of error returns detailed + * diagnostic + * + * @param str std::wstring object with template description + * @param tplName Optional name of the template (for the error reporting purposes) + * + * @return Either noting or instance of \ref ErrorInfoTpl as an error + */ + ResultW Load(const std::wstring& str, std::string tplName = std::string()); + /*! + * \brief Load template from the stream + * + * Takes specified stream object and parses it as a source of Jinja2 template. In case of error returns detailed + * diagnostic + * + * @param stream Stream object with template description + * @param tplName Optional name of the template (for the error reporting purposes) + * + * @return Either noting or instance of \ref ErrorInfoTpl as an error + */ + ResultW Load(std::wistream& stream, std::string tplName = std::string()); + /*! + * \brief Load template from the specified file + * + * Loads file with the specified name and parses it as a source of Jinja2 template. In case of error returns + * detailed diagnostic + * + * @param fileName Name of the file to load + * + * @return Either noting or instance of \ref ErrorInfoTpl as an error + */ + ResultW LoadFromFile(const std::string& fileName); + + /*! + * \brief Render previously loaded template to the wide char stream + * + * Renders previously loaded template to the specified wide char stream and specified set of params. + * + * @param os Stream to render template to + * @param params Set of params which should be passed to the template engine and can be used within the template + * + * @return Either noting or instance of \ref ErrorInfoTpl as an error + */ + ResultW Render(std::wostream& os, const ValuesMap& params); + /*! + * \brief Render previously loaded template to the wide char string + * + * Renders previously loaded template as a wide char string and with specified set of params. + * + * @param params Set of params which should be passed to the template engine and can be used within the template + * + * @return Either rendered string or instance of \ref ErrorInfoTpl as an error + */ + ResultW RenderAsString(const ValuesMap& params); + /*! + * \brief Get metadata, provided in the {% meta %} tag + * + * @return Parsed metadata as a generic map value or instance of \ref ErrorInfoTpl as an error + */ + ResultW GetMetadata(); + /*! + * \brief Get non-parsed metadata, provided in the {% meta %} tag + * + * @return Non-parsed metadata information or instance of \ref ErrorInfoTpl as an error + */ + ResultW> GetMetadataRaw(); - void Render(std::wostream& os, const ValuesMap& params); - std::wstring RenderAsString(const ValuesMap& params); + /* ! + * \brief compares to an other object of the same type + * + * @return true if equal + */ + bool IsEqual(const TemplateW& other) const; private: std::shared_ptr m_impl; friend class TemplateImpl; }; -} // jinja2 -#endif // JINJA2_TEMPLATE_H +bool operator==(const TemplateW& lhs, const TemplateW& rhs); +bool operator!=(const TemplateW& lhs, const TemplateW& rhs); + +} // namespace jinja2 + +#endif // JINJA2CPP_TEMPLATE_H diff --git a/include/jinja2cpp/template_env.h b/include/jinja2cpp/template_env.h index c72267e8..09a6a52c 100644 --- a/include/jinja2cpp/template_env.h +++ b/include/jinja2cpp/template_env.h @@ -1,11 +1,13 @@ #ifndef JINJA2CPP_TEMPLATE_ENV_H #define JINJA2CPP_TEMPLATE_ENV_H +#include "config.h" #include "error_info.h" - #include "filesystem_handler.h" #include "template.h" +#include +#include #include namespace jinja2 @@ -14,51 +16,305 @@ namespace jinja2 class IErrorHandler; class IFilesystemHandler; +//! Compatibility mode for jinja2c++ engine +enum class Jinja2CompatMode +{ + None, //!< Default mode + Vesrsion_2_10, //!< Compatibility with Jinja2 v.2.10 specification +}; + +//! Global template environment settings struct Settings { + /// Extensions set which should be supported + struct Extensions + { + bool Do = false; //!< Enable use of `do` statement + }; + + //! Enables use of line statements (yet not supported) bool useLineStatements = false; + //! Enables blocks trimming the same way as it does python Jinja2 engine bool trimBlocks = false; + //! Enables blocks stripping (from the left) the same way as it does python Jinja2 engine bool lstripBlocks = false; + //! Templates cache size + int cacheSize = 400; + //! If auto_reload is set to true (default) every time a template is requested the loader checks if the source changed and if yes, it will reload the template + bool autoReload = true; + //! Extensions set enabled for templates + Extensions extensions; + //! Controls Jinja2 compatibility mode + Jinja2CompatMode jinja2CompatMode = Jinja2CompatMode::None; + //! Default format for metadata block in the templates + std::string m_defaultMetadataType = "json"; }; -class TemplateEnv +inline bool operator==(const Settings& lhs, const Settings& rhs) +{ + auto lhsTie = std::tie(lhs.useLineStatements, lhs.trimBlocks, lhs.lstripBlocks, lhs.cacheSize, lhs.autoReload, lhs.extensions.Do, lhs.jinja2CompatMode, lhs.m_defaultMetadataType); + auto rhsTie = std::tie(rhs.useLineStatements, rhs.trimBlocks, rhs.lstripBlocks, rhs.cacheSize, rhs.autoReload, rhs.extensions.Do, rhs.jinja2CompatMode, rhs.m_defaultMetadataType); + return lhsTie == rhsTie; +} +inline bool operator!=(const Settings& lhs, const Settings& rhs) +{ + return !(lhs == rhs); +} + +/*! + * \brief Global template environment which controls behaviour of the different \ref Template instances + * + * This class is used for fine tuning of the templates behaviour and for state sharing between them. With this class + * it's possible to control template loading, provide template sources, set global variables, use template inheritance + * and inclusion. + * + * It's possible to load templates from the environment via \ref LoadTemplate or \ref LoadTemplateW methods + * or to pass instance of the environment directly to the \ref Template via constructor. + */ +class JINJA2CPP_EXPORT TemplateEnv { public: - void SetErrorHandler(IErrorHandler* h) - { - m_errorHandler = h; - } - auto GetErrorHandler() const - { - return m_errorHandler; - } + using TimePoint = std::chrono::system_clock::time_point; + using TimeStamp = std::chrono::steady_clock::time_point; + /*! + * \brief Returns global settings for the environment + * + * @return Constant reference to the global settings + */ const Settings& GetSettings() const {return m_settings;} + /*! + * \brief Returns global settings for the environment available for modification + * + * @return Reference to the global settings + */ Settings& GetSettings() {return m_settings;} + + /*! + * \brief Replace global settings for the environment with the new ones + * + * @param setts New settings + */ void SetSettings(const Settings& setts) {m_settings = setts;} + /*! + * \brief Add pointer to file system handler with the specified prefix + * + * Adds filesystem handler which provides access to the external source of templates. With added handlers it's + * possible to load templates from the `import`, `extends` and `include` jinja2 tags. Prefix is used for + * distinguish one templates source from another. \ref LoadTemplate or \ref LoadTemplateW methods use + * handlers to load templates with the specified name. + * Method is thread-unsafe. It's dangerous to add new filesystem handlers and load templates simultaneously. + * + * Basic usage: + * ```c++ + * jinja2::TemplateEnv env; + * + * auto fs = std::make_shared(); + * env.AddFilesystemHandler(std::string(), fs); + * fs->AddFile("base.j2tpl", "Hello World!"); + * ``` + * + * @param prefix Optional prefix of the handler's filesystem. Prefix is a part of the file name and passed to the handler's \ref IFilesystemHandler::OpenStream method + * @param h Shared pointer to the handler + */ void AddFilesystemHandler(std::string prefix, FilesystemHandlerPtr h) { - m_filesystemHandlers.push_back(FsHandler{std::move(prefix), h}); + m_filesystemHandlers.push_back(FsHandler{std::move(prefix), std::move(h)}); } + /*! + * \brief Add reference to file system handler with the specified prefix + * + * Adds filesystem handler which provides access to the external source of templates. With added handlers it's + * possible to load templates from the `import`, `extends` and `include` jinja2 tags. Prefix is used for + * distinguish one templates source from another. \ref LoadTemplate or \ref LoadTemplateW methods use + * handlers to load templates with the specified name. + * Method is thread-unsafe. It's dangerous to add new filesystem handlers and load templates simultaneously. + * + * Basic usage: + * ```c++ + * jinja2::TemplateEnv env; + * + * MemoryFileSystem fs; + * env.AddFilesystemHandler(std::string(), fs); + * fs.AddFile("base.j2tpl", "Hello World!"); + * ``` + * + * @param prefix Optional prefix of the handler's filesystem. Prefix is a part of the file name and passed to the handler's \ref IFilesystemHandler::OpenStream method + * @param h Reference to the handler. It's assumed that lifetime of the handler is controlled externally + */ void AddFilesystemHandler(std::string prefix, IFilesystemHandler& h) { m_filesystemHandlers.push_back(FsHandler{std::move(prefix), std::shared_ptr(&h, [](auto*) {})}); } + /*! + * \brief Load narrow char template with the specified name via registered file handlers + * + * In case of specified file present in any of the registered handlers, template is loaded and parsed. If any + * error occurred during the loading or parsing detailed diagnostic will be returned. + * Method is thread-unsafe. It's dangerous to add new filesystem handlers and load templates simultaneously. + * + * @param fileName Template name to load + * + * @return Either loaded template or load/parse error. See \ref ErrorInfoTpl + */ nonstd::expected LoadTemplate(std::string fileName); + /*! + * \brief Load wide char template with the specified name via registered file handlers + * + * In case of specified file present in any of the registered handlers, template is loaded and parsed. If any + * error occurred during the loading or parsing detailed diagnostic will be returned. + * Method is thread-unsafe. It's dangerous to add new filesystem handlers and load templates simultaneously. + * + * @param fileName Template name to load + * + * @return Either loaded template or load/parse error. See \ref ErrorInfoTpl + */ nonstd::expected LoadTemplateW(std::string fileName); + /*! + * \brief Add global variable to the environment + * + * Adds global variable which can be referred in any template which is loaded within this environment object. + * Method is thread-safe. + * + * @param name Name of the variable + * @param val Value of the variable + */ + void AddGlobal(std::string name, Value val) + { + std::unique_lock l(m_guard); + m_globalValues[std::move(name)] = std::move(val); + } + /*! + * \brief Remove global variable from the environment + * + * Removes global variable from the environment. + * Method is thread-safe. + * + * @param name Name of the variable + */ + void RemoveGlobal(const std::string& name) + { + std::unique_lock l(m_guard); + m_globalValues.erase(name); + } + + /*! + * \brief Call the specified function with the current set of global variables under the internal lock + * + * Main purpose of this method is to help external code to enumerate global variables thread-safely. Provided functional object is called under the + * internal lock with the current set of global variables as an argument. + * + * @tparam Fn Type of the functional object to call + * @param fn Functional object to call + */ + template + void ApplyGlobals(Fn&& fn) + { + std::shared_lock l(m_guard); + fn(m_globalValues); + } + + bool IsEqual(const TemplateEnv& other) const + { + if (m_filesystemHandlers != other.m_filesystemHandlers) + return false; + if (m_settings != other.m_settings) + return false; + if (m_globalValues != other.m_globalValues) + return false; + if (m_templateCache != other.m_templateCache) + return false; + if (m_templateWCache != other.m_templateWCache) + return false; + + return true; + } + +private: + template + auto LoadTemplateImpl(TemplateEnv* env, std::string fileName, const T& filesystemHandlers, Cache& cache); + + private: - IErrorHandler* m_errorHandler; struct FsHandler { std::string prefix; FilesystemHandlerPtr handler; + bool operator==(const FsHandler& rhs) const + { + if (prefix != rhs.prefix) + return false; + if (handler && rhs.handler && !handler->IsEqual(*rhs.handler)) + return false; + if ((!handler && rhs.handler) || (handler && !rhs.handler)) + return false; + return true; + } + bool operator!=(const FsHandler& rhs) const + { + return !(*this == rhs); + } + }; + + struct BaseTemplateInfo + { + nonstd::optional lastModification; + TimeStamp lastAccessTime; + FilesystemHandlerPtr handler; + bool operator==(const BaseTemplateInfo& other) const + { + if (lastModification != other.lastModification) + return false; + if (lastAccessTime != other.lastAccessTime) + return false; + if (handler && other.handler && !handler->IsEqual(*other.handler)) + return false; + if ((!handler && other.handler) || (handler && !other.handler)) + return false; + return true; + } + bool operator!=(const BaseTemplateInfo& other) const + { + return !(*this == other); + } + }; + + struct TemplateCacheEntry : public BaseTemplateInfo + { + Template tpl; + bool operator==(const TemplateCacheEntry& other) const + { + return BaseTemplateInfo::operator==(other) && tpl == other.tpl; + } + bool operator!=(const TemplateCacheEntry& other) const + { + return !(*this == other); + } }; + + struct TemplateWCacheEntry : public BaseTemplateInfo + { + TemplateW tpl; + bool operator==(const TemplateWCacheEntry& other) const + { + return BaseTemplateInfo::operator==(other) && tpl == other.tpl; + } + bool operator!=(const TemplateWCacheEntry& other) const + { + return !(*this == other); + } + }; + std::vector m_filesystemHandlers; Settings m_settings; + ValuesMap m_globalValues; + std::shared_timed_mutex m_guard; + std::unordered_map m_templateCache; + std::unordered_map m_templateWCache; }; -} // jinja2 +} // namespace jinja2 #endif // JINJA2CPP_TEMPLATE_ENV_H diff --git a/include/jinja2cpp/user_callable.h b/include/jinja2cpp/user_callable.h new file mode 100644 index 00000000..dae15f3d --- /dev/null +++ b/include/jinja2cpp/user_callable.h @@ -0,0 +1,456 @@ +#ifndef JINJA2CPP_USER_CALLABLE_H +#define JINJA2CPP_USER_CALLABLE_H + +#include "string_helpers.h" +#include "value.h" + +#include + +#include +#include +#include + +namespace jinja2 +{ +#ifndef JINJA2CPP_NO_DOXYGEN +namespace detail +{ +template +struct CanBeCalled : std::false_type {}; + +template +struct CanBeCalled::value>::type> : std::true_type {}; + +template +struct ArgPromoter +{ + ArgPromoter(const T* val) + : m_ptr(val) + { + } + + operator T() const { return *m_ptr; } + + const T* m_ptr; +}; + +template<> +struct ArgPromoter +{ +public: + ArgPromoter(const EmptyValue*) {} + + template + operator T() + { + return T(); + } +}; + +template +struct ArgPromoter::value>> +{ + ArgPromoter(const T* val) + : m_ptr(val) + { + } + + template::value>> + operator U() const + { + return static_cast(*m_ptr); + } + + const T* m_ptr; +}; + +template +struct ArgPromoter, void> +{ + using string = std::basic_string; + using string_view = nonstd::basic_string_view; + using other_string = std::conditional_t::value, std::wstring, std::string>; + using other_string_view = std::conditional_t::value, nonstd::wstring_view, nonstd::string_view>; + + ArgPromoter(const string* str) + : m_ptr(str) + { + } + + operator const string&() const { return *m_ptr; } + operator string() const { return *m_ptr; } + operator string_view () const { return *m_ptr; } + operator other_string () const + { + return ConvertString(*m_ptr); + } + operator other_string_view () const + { + m_convertedStr = ConvertString(*m_ptr); + return m_convertedStr.value(); + } + + const string* m_ptr; + mutable nonstd::optional m_convertedStr; +}; + +template +struct ArgPromoter, void> +{ + using string = std::basic_string; + using string_view = nonstd::basic_string_view; + using other_string = std::conditional_t::value, std::wstring, std::string>; + using other_string_view = std::conditional_t::value, nonstd::wstring_view, nonstd::string_view>; + + ArgPromoter(const string_view* str) + : m_ptr(str) + { + } + + operator const string_view&() const { return *m_ptr; } + operator string_view() const { return *m_ptr; } + operator string () const { return string(m_ptr->begin(), m_ptr->end()); } + operator other_string () const + { + return ConvertString(*m_ptr); + } + operator other_string_view () const + { + m_convertedStr = ConvertString(*m_ptr); + return m_convertedStr.value(); + } + + const string_view* m_ptr; + mutable nonstd::optional m_convertedStr; +}; + +template +auto Promote(Arg&& arg) +{ + return ArgPromoter>(&arg); +} + +template +struct UCInvoker +{ + const Fn& fn; + const UserCallableParams& params; + + template + struct FuncTester + { + template + static auto TestFn(F&& f) -> decltype(Value(f(Promote(std::declval())...))); + static auto TestFn(...) -> char; + + using result_type = decltype(TestFn(std::declval())); + }; + + UCInvoker(const Fn& f, const UserCallableParams& p) + : fn(f) + , params(p) + {} + + template + auto operator()(Args&& ... args) const -> std::enable_if_t>::value, Value> + { + return Value(fn(Promote(args)...)); + } + + template + auto operator()(Args&&...) const -> std::enable_if_t>::value, Value> + { + return Value(); + } +}; + +inline const Value& GetParamValue(const UserCallableParams& params, const ArgInfo& info) +{ + // static Value empty; + auto p = params.args.find(info.paramName); + if (p != params.args.end()) + return p->second; + else if (info.paramName == "**kwargs") + return params.extraKwArgs; + else if (info.paramName == "*args") + return params.extraPosArgs; + else if (info.paramName == "*context") + return params.context; + + return info.defValue; +} + +template +struct ParamUnwrapper +{ + V* m_visitor{}; + + ParamUnwrapper(V* v) + : m_visitor(v) + {} + + + template + static const auto& UnwrapRecursive(const T& arg) + { + return arg; // std::forward(arg); + } + + template + static auto& UnwrapRecursive(const RecWrapper& arg) + { + if (!arg) + throw std::runtime_error("No value to unwrap"); + return *arg; + } + + template + auto operator()(const Args& ... args) const + { + assert(m_visitor != nullptr); + return (*m_visitor)(UnwrapRecursive(args)...); + } +}; + +template +Value InvokeUserCallable(Fn&& fn, const UserCallableParams& params, ArgDescr&& ... ad) +{ + auto invoker = UCInvoker(fn, params); + return nonstd::visit(ParamUnwrapper>(&invoker), GetParamValue(params, ad).data()...); +} + +template +struct TypedParam +{ + using decayed_t = std::decay_t; + nonstd::variant data; + + bool HasValue() const { return data.index() != 0; } + T GetValue() const + { + if (data.index() == 1) + return nonstd::get(data); + else + return *nonstd::get(data); + } + + void SetPointer(const decayed_t* ptr) { data = ptr; } + + void SetValue(decayed_t&& val) { data = std::move(val); } +}; + +template +struct TypedParamUnwrapper +{ + TypedParam* param; + using ValueType = typename TypedParam::decayed_t; + TypedParamUnwrapper(TypedParam& p) + : param(&p) + { + } + + template + struct PromoteTester + { + using decayed_u = std::decay_t; + struct InvalidType + { + }; + + static auto TestFn(T) -> int; + static auto TestFn(...) -> char; + + template + static auto PromotedType(U1 u) -> decltype(TestFn(Promote(u))); + static auto PromotedType(...) -> char; + + enum { value = std::is_same::value ? false : sizeof(PromotedType(std::declval())) == sizeof(int) }; + }; + + void operator()(const ValueType& val) const { param->SetPointer(&val); } + + void operator()(const EmptyValue&) const { param->SetValue(ValueType()); } + + template + auto operator()(const U& v) -> std::enable_if_t::value && !std::is_same, ValueType>::value> + { + param->SetValue(Promote(v)); + } + + template + auto operator()(const U&) -> std::enable_if_t::value && !std::is_same, ValueType>::value> + { + } +}; + +template +auto TypedUnwrapParam(const V& value) +{ + TypedParam param; + TypedParamUnwrapper visitor(param); + nonstd::visit(ParamUnwrapper>(&visitor), value); + return param; +} + +#if !optional_CPP17_OR_GREATER +inline bool TypedParamHasValue() +{ + return true; +} + +template +bool TypedParamHasValue(const TypedParam& param, Params&&... params) +{ + return param.HasValue() && TypedParamHasValue(params...); +} + +template +Value InvokeTypedUserCallableImpl(Fn&& fn, Tuple&& tuple, std::index_sequence&&) +{ + bool has_value = TypedParamHasValue(std::get(tuple)...); + if (!has_value) + return Value(); + + return fn(std::get(tuple).GetValue()...); +} +#endif + +template +Value InvokeTypedUserCallable(Fn&& fn, const UserCallableParams& params, ArgDescr&&... ad) +{ + auto typed_params = std::make_tuple(TypedUnwrapParam::type>(GetParamValue(params, ad).data())...); +#if !optional_CPP17_OR_GREATER + return InvokeTypedUserCallableImpl(fn, typed_params, std::index_sequence_for()); +#else + return std::apply( + [&fn](auto&... args) { + bool has_value = (true && ... && args.HasValue()); + if (!has_value) + return Value(); + + return Value(fn(args.GetValue()...)); + }, + typed_params); +#endif +} + +template +struct ArgDescrHasType : std::false_type +{ +}; + +template +struct ArgDescrHasType...> : std::true_type +{ +}; +} // namespace detail +#endif // JINJA2CPP_NO_DOXYGEN + +/*! + * \brief Create user-defined callable from the specified function + * + * Creates instance of the UserCallable object which invokes specified function (f) with parsed params according + * to the description (ad). For instance: + * ```c++ + * MakeCallable( + * [](const std::string& str1, const std::string& str2) { + * return str1 + " " + str2; + * }, + * ArgInfo{"str1"}, ArgInfo{"str2", false, "default"} + * ); + * ``` + * In this sample lambda function with two string params will be invoked from the jinja2 template and provided with + * the specified params. Each param is described by \ref ArgInfo structure. Result of the function will be converted + * and passed back to the jinja2 template. + * + * In case the function should accept extra positional args or extra named args this params should be described the + * following name. + * - Extra positional args. \ref ArgInfo should describe this param with name `*args`. Param of the function should + * has \ref ValuesList type + * - Extra named args. \ref ArgInfo should describe this param with name `**kwargs`. Param of the function should + * has \ref ValuesMap type + * - Current template context. \ref ArgInfo should describe this param with name `*context`. Param of the function should + * has \ref GenericMap type + * + * \param f Function which should be called + * \param ad Function param descriptors + * + * \returns Instance of the properly initialized \ref UserCallable structure + */ +template +auto MakeCallable(Fn&& f, ArgDescr&&... ad) -> typename std::enable_if::value, UserCallable>::type +{ + UserCallable::UserCallableFunctionPtr callable = [=, fn = std::forward(f)](const UserCallableParams& params) { + return detail::InvokeUserCallable(fn, params, ad...); + }; + return UserCallable { + callable, + {ArgInfo(std::forward(ad))...} + }; +} + +template +auto MakeCallable(Fn&& f, ArgDescr&&... ad) -> typename std::enable_if::value, UserCallable>::type +{ + UserCallable::UserCallableFunctionPtr callable = [=, fn = std::forward(f)](const UserCallableParams& params) { + return detail::InvokeTypedUserCallable(fn, params, ad...); + }; + return UserCallable { + callable, + { ArgInfo(std::forward(ad))... } + }; +} + +template +auto MakeCallable(R (*f)(Args...), ArgDescr&&... ad) -> UserCallable +{ + UserCallable::UserCallableFunctionPtr callable = [=, fn = f](const UserCallableParams& params) { + return detail::InvokeTypedUserCallable(fn, params, ArgInfoT(ad)...); + }; + return UserCallable { + callable, + { ArgInfoT(std::forward(ad))... } + }; +} + +template +auto MakeCallable(R (T::*f)(Args...), T* obj, ArgDescr&&... ad) -> UserCallable +{ + UserCallable::UserCallableFunctionPtr callable = [=, fn = f](const UserCallableParams& params) { + return detail::InvokeTypedUserCallable( + [fn, obj](Args&&... args) { return (obj->*fn)(std::forward(args)...); }, params, ArgInfoT(ad)...); + }; + return UserCallable { + callable, + { ArgInfoT(std::forward(ad))... } + }; +} + +template +auto MakeCallable(R (T::*f)(Args...) const, const T* obj, ArgDescr&&... ad) -> UserCallable +{ + UserCallable::UserCallableFunctionPtr callable = [=, fn = f](const UserCallableParams& params) { + return detail::InvokeTypedUserCallable( + [fn, obj](Args&&... args) { return (obj->*fn)(std::forward(args)...); }, params, ArgInfoT(ad)...); + }; + return UserCallable { + callable, + { ArgInfoT(std::forward(ad))... } + }; +} + +/*! + * \brief Create user-callable from the function with no params. + * + * \param f Function which should be called + * + * \returns Instance of the properly initialized \ref UserCallable structure + */ +template +auto MakeCallable(Fn&& f) +{ + return UserCallable{ [=, fn = std::forward(f)](const UserCallableParams&) { return fn(); }, {} }; +} +} // namespace jinja2 + +#endif // JINJA2CPP_USER_CALLABLE_H diff --git a/include/jinja2cpp/utils/i_comparable.h b/include/jinja2cpp/utils/i_comparable.h new file mode 100644 index 00000000..76f58d5c --- /dev/null +++ b/include/jinja2cpp/utils/i_comparable.h @@ -0,0 +1,16 @@ +#ifndef JINJA2CPP_ICOMPARABLE_H +#define JINJA2CPP_ICOMPARABLE_H + +#include + +namespace jinja2 { + +struct JINJA2CPP_EXPORT IComparable +{ + virtual ~IComparable() {} + virtual bool IsEqual(const IComparable& other) const = 0; +}; + +} // namespace jinja2 + +#endif // JINJA2CPP_ICOMPARABLE_H diff --git a/include/jinja2cpp/value.h b/include/jinja2cpp/value.h index 431bc736..a22c33ac 100644 --- a/include/jinja2cpp/value.h +++ b/include/jinja2cpp/value.h @@ -1,185 +1,763 @@ -#ifndef JINJA2_VALUE_H -#define JINJA2_VALUE_H +#ifndef JINJA2CPP_VALUE_H +#define JINJA2CPP_VALUE_H -#pragma once +#include +#include +#include +#include +#include +#include + +#include #include #include #include #include -#include +#include +#include namespace jinja2 { -struct EmptyValue {}; +//! Empty value container +struct EmptyValue +{ + template + operator T() const {return T{};} +}; + +inline bool operator==(const EmptyValue& lhs, const EmptyValue& rhs) +{ + (void)lhs; + (void)rhs; + return true; +} + class Value; -struct ListItemAccessor +/*! + * \brief Interface to the generic dictionary type which maps string to some value + */ +struct IMapItemAccessor : IComparable { - virtual ~ListItemAccessor() {} + //! Destructor + virtual ~IMapItemAccessor() = default; + //! Method is called to obtain number of items in the dictionary. Maximum possible size_t value means non-calculable size virtual size_t GetSize() const = 0; - virtual Value GetValueByIndex(int64_t idx) const = 0; -}; -struct MapItemAccessor : public ListItemAccessor -{ - virtual ~MapItemAccessor() {} + /*! + * \brief Method is called to check presence of the item in the dictionary + * + * @param name Name of the item + * + * @return true if item is present and false otherwise. + */ virtual bool HasValue(const std::string& name) const = 0; + /*! + * \brief Method is called for retrieving the value by specified name + * + * @param name Name of the value to retrieve + * + * @return Requestd value or empty \ref Value if item is absent + */ virtual Value GetValueByName(const std::string& name) const = 0; + /*! + * \brief Method is called for retrieving collection of keys in the dictionary + * + * @return Collection of keys if any. Ordering of keys is unspecified. + */ virtual std::vector GetKeys() const = 0; + + /*! + * \brief Compares to object of the same type + * + * @return true if equal + */ +// virtual bool IsEqual(const IMapItemAccessor& rhs) const = 0; }; +/*! + * \brief Helper class for accessing maps specified by the \ref IMapItemAccessor interface + * + * In the \ref Value type can be stored either ValuesMap instance or GenericMap instance. ValuesMap is a simple + * dictionary object based on std::unordered_map. Rather than GenericMap is a more robust object which can provide + * access to the different types of dictionary entities. GenericMap takes the \ref IMapItemAccessor interface instance + * and uses it to access particular items in the dictionaries. + */ class GenericMap { public: + //! Default constructor GenericMap() = default; - GenericMap(std::function accessor) + + /*! + * \brief Initializing constructor + * + * The only one way to get valid non-empty GeneridMap is to construct it with the specified \ref IMapItemAccessor + * implementation provider. This provider is a functional object which returns pointer to the interface instance. + * + * @param accessor Functional object which returns pointer to the \ref IMapItemAccessor interface + */ + explicit GenericMap(std::function accessor) : m_accessor(std::move(accessor)) { } + /*! + * \brief Check the presence the specific item in the dictionary + * + * @param name Name of the the item + * + * @return true of item is present and false otherwise + */ bool HasValue(const std::string& name) const { - return m_accessor()->HasValue(name); + return m_accessor ? m_accessor()->HasValue(name) : false; } + /*! + * \brief Get specific item from the dictionary + * + * @param name Name of the item to get + * + * @return Value of the item or empty \ref Value if no item + */ Value GetValueByName(const std::string& name) const; + /*! + * \brief Get size of the dictionary + * + * @return Size of the dictionary + */ size_t GetSize() const { - return m_accessor()->GetSize(); + return m_accessor ? m_accessor()->GetSize() : 0; } - Value GetValueByIndex(int64_t index) const; + /*! + * \brief Get collection of keys from the dictionary + * + * @return Collection of the keys or empty collection if no keys + */ auto GetKeys() const { - return m_accessor()->GetKeys(); + return m_accessor ? m_accessor()->GetKeys() : std::vector(); } - - std::function m_accessor; -}; - -class GenericList -{ -public: - GenericList() = default; - GenericList(std::function accessor) - : m_accessor(std::move(accessor)) - { - } - - size_t GetSize() const - { - return m_accessor()->GetSize(); - } - - Value GetValueByIndex(int64_t idx) const; - + /*! + * \brief Get the underlying access interface to the dictionary + * + * @return Pointer to the underlying interface or nullptr if no + */ auto GetAccessor() const { return m_accessor(); } - bool IsValid() const - { - return !(!m_accessor); - } + auto operator[](const std::string& name) const; - std::function m_accessor; +private: + std::function m_accessor; }; +bool operator==(const GenericMap& lhs, const GenericMap& rhs); +bool operator!=(const GenericMap& lhs, const GenericMap& rhs); + using ValuesList = std::vector; -using ValuesMap = std::unordered_map; -using ValueData = boost::variant, boost::recursive_wrapper, GenericList, GenericMap>; +struct ValuesMap; +struct UserCallableArgs; +struct ParamInfo; +struct UserCallable; + +template +using RecWrapper = types::ValuePtr; -class Value { +/*! + * \brief Generic value class + * + * Variant-based class which is used for passing values to and from Jinja2C++ template engine. This class store the + * following types of values: + * + * - EmptyValue. In this case instance of this class threated as 'empty' + * - Boolean value. + * - String value. + * - Wide string value + * - String view value (nonstd::string_view) + * - Wide string view value (nonstd::wstring_view) + * - integer (int64_t) value + * - floating point (double) value + * - Simple list of other values (\ref ValuesList) + * - Simple map of other values (\ref ValuesMap) + * - Generic list of other values (\ref GenericList) + * - Generic map of other values (\ref GenericMap) + * - User-defined callable (\ref UserCallable) + * + * Exact value can be accessed via nonstd::visit method applied to the result of the Value::data() call or any of + * asXXX method (ex. \ref Value::asString). In case of string retrieval it's better to use \ref AsString or \ref + * AsWString functions. Thay hide all nececcary transformations between various types of strings (or string views). + */ +class Value +{ public: - Value() = default; + using ValueData = nonstd::variant< + EmptyValue, + bool, + std::string, + std::wstring, + nonstd::string_view, + nonstd::wstring_view, + int64_t, + double, + RecWrapper, + RecWrapper, + GenericList, + GenericMap, + RecWrapper + >; + + template + struct AnyOf : public std::false_type {}; + + template + struct AnyOf : public std::integral_constant, H>::value || AnyOf::value> {}; + + //! Default constructor + Value(); + //! Copy constructor + Value(const Value& val); + //! Move constructor + Value(Value&& val) noexcept; + //! Desctructor + // ~Value(); + ~Value(); + + //! Assignment operator + Value& operator=(const Value&); + //! Move assignment operator + Value& operator=(Value&&) noexcept; + /*! + * \brief Generic initializing constructor + * + * Creates \ref Value from the arbitrary type which is compatible with types listed in \ref Value::ValueData + * + * @tparam T Type of value to create \ref Value instance from + * @param val Value which should be used to initialize \ref Value instance + */ template - Value(T&& val, typename std::enable_if, Value>::value>::type* = nullptr) + Value(T&& val, typename std::enable_if::value>::type* = nullptr) : m_data(std::forward(val)) { } + /*! + * \brief Initializing constructor from pointer to the null-terminated narrow string + * + * @param val Null-terminated string which should be used to initialize \ref Value instance + */ Value(const char* val) : m_data(std::string(val)) { } + /*! + * \brief Initializing constructor from pointer to the null-terminated wide string + * + * @param val Null-terminated string which should be used to initialize \ref Value instance + */ + Value(const wchar_t* val) + : m_data(std::wstring(val)) + { + } + /*! + * \brief Initializing constructor from the narrow string literal + * + * @param val String literal which should be used to initialize \ref Value instance + */ template Value(char (&val)[N]) : m_data(std::string(val)) { } + /*! + * \brief Initializing constructor from the wide string literal + * + * @param val String literal which should be used to initialize \ref Value instance + */ + template + Value(wchar_t (&val)[N]) + : m_data(std::wstring(val)) + { + } + /*! + * \brief Initializing constructor from the int value + * + * @param val Integer value which should be used to initialize \ref Value instance + */ Value(int val) : m_data(static_cast(val)) { } + /*! + * \brief Initializing constructor from the \ref ValuesList + * + * @param list List of values which should be used to initialize \ref Value instance + */ + Value(const ValuesList& list); + /*! + * \brief Initializing constructor from the \ref ValuesMap + * + * @param map Map of values which should be used to initialize \ref Value instance + */ + Value(const ValuesMap& map); + /*! + * \brief Initializing constructor from the \ref UserCallable + * + * @param callable UserCallable which should be used to initialize \ref Value instance + */ + Value(const UserCallable& callable); + /*! + * \brief Initializing move constructor from the \ref ValuesList + * + * @param list List of values which should be used to initialize \ref Value instance + */ + Value(ValuesList&& list) noexcept; + /*! + * \brief Initializing move constructor from the \ref ValuesMap + * + * @param map Map of values which should be used to initialize \ref Value instance + */ + Value(ValuesMap&& map) noexcept; + /*! + * \brief Initializing move constructor from the \ref UserCallable + * + * @param callable UserCallable which should be used to initialize \ref Value instance + */ + Value(UserCallable&& callable); + /*! + * \brief Get the non-mutable stored data object + * + * Returns the stored data object in order to get the typed value from it. For instance: + * ```c++ + * inline std::string AsString(const jinja2::Value& val) + * { + * return nonstd::visit(StringGetter(), val.data()); + * } + * ``` + * + * @return Non-mutable stored data object + */ const ValueData& data() const {return m_data;} - + /*! + * \brief Get the mutable stored data object + * + * Returns the stored data object in order to get the typed value from it. For instance: + * ```c++ + * inline std::string AsString(Value& val) + * { + * return nonstd::visit(StringGetter(), val.data()); + * } + * ``` + * + * @return Mutable stored data object + */ ValueData& data() {return m_data;} + //! Test Value for containing std::string object bool isString() const { - return boost::get(&m_data) != nullptr; + return nonstd::get_if(&m_data) != nullptr; } + /*! + * \brief Returns mutable containing std::string object + * + * Returns containing std::string object. Appropriate exception is thrown in case non-string containing value + * + * @return Mutable containing std::string object + */ auto& asString() { - return boost::get(m_data); + return nonstd::get(m_data); } + /*! + * \brief Returns non-mutable containing std::string object + * + * Returns containing std::string object. Appropriate exception is thrown in case of non-string containing value + * + * @return Non-mutable containing std::string object + */ auto& asString() const { - return boost::get(m_data); + return nonstd::get(m_data); } + //! Test Value for containing std::wstring object + bool isWString() const + { + return nonstd::get_if(&m_data) != nullptr; + } + /*! + * \brief Returns mutable containing std::wstring object + * + * Returns containing std::wstring object. Appropriate exception is thrown in case of non-wstring containing value + * + * @return Mutable containing std::wstring object + */ + auto& asWString() + { + return nonstd::get(m_data); + } + /*! + * \brief Returns non-mutable containing std::wstring object + * + * Returns containing std::wstring object. Appropriate exception is thrown in case of non-wstring containing value + * + * @return Non-mutable containing std::wstring object + */ + auto& asWString() const + { + return nonstd::get(m_data); + } + + //! Test Value for containing jinja2::ValuesList object bool isList() const { - return boost::get(&m_data) != nullptr || boost::get(&m_data) != nullptr; + return nonstd::get_if>(&m_data) != nullptr || nonstd::get_if(&m_data) != nullptr; } + /*! + * \brief Returns mutable containing jinja2::ValuesList object + * + * Returns containing jinja2::ValuesList object. Appropriate exception is thrown in case of non-Valueslist containing value + * + * @return Mutable containing jinja2::ValuesList object + */ auto& asList() { - return boost::get(m_data); + return *nonstd::get>(m_data); } + /*! + * \brief Returns non-mutable containing jinja2::ValuesList object + * + * Returns containing jinja2::ValuesList object. Appropriate exception is thrown in case of non-Valueslist containing value + * + * @return Non-mutable containing jinja2::ValuesList object + */ auto& asList() const { - return boost::get(m_data); + return *nonstd::get>(m_data); } + //! Test Value for containing jinja2::ValuesMap object bool isMap() const { - return boost::get(&m_data) != nullptr || boost::get(&m_data) != nullptr; + return nonstd::get_if>(&m_data) != nullptr || nonstd::get_if(&m_data) != nullptr; } + /*! + * \brief Returns mutable containing jinja2::ValuesMap object + * + * Returns containing jinja2::ValuesMap object. Appropriate exception is thrown in case of non-ValuesMap containing value + * + * @return Mutable containing jinja2::ValuesMap object + */ auto& asMap() { - return boost::get(m_data); + return *nonstd::get>(m_data); } + /*! + * \brief Returns non-mutable containing jinja2::ValuesMap object + * + * Returns containing jinja2::ValuesMap object. Appropriate exception is thrown in case of non-ValuesMap containing value + * + * @return Non-mutable containing jinja2::ValuesMap object + */ auto& asMap() const { - return boost::get(m_data); + return *nonstd::get>(m_data); + } + + template + auto get() + { + return nonstd::get(m_data); + } + + template + auto get() const + { + return nonstd::get(m_data); + } + + template + auto getPtr() + { + return nonstd::get_if(&m_data); // m_data.index() == ValueData::template index_of() ? &m_data.get() : nullptr; } + + template + auto getPtr() const + { + return nonstd::get_if(&m_data); // m_data.index() == ValueData::template index_of() ? &m_data.get() : nullptr; + } + + //! Test Value for emptyness bool isEmpty() const { - return boost::get(&m_data) != nullptr; + return nonstd::get_if(&m_data) != nullptr; } - Value subscript(const Value& index) const; + bool IsEqual(const Value& rhs) const; private: ValueData m_data; }; +JINJA2CPP_EXPORT bool operator==(const Value& lhs, const Value& rhs); +JINJA2CPP_EXPORT bool operator!=(const Value& lhs, const Value& rhs); +bool operator==(const types::ValuePtr& lhs, const types::ValuePtr& rhs); +bool operator!=(const types::ValuePtr& lhs, const types::ValuePtr& rhs); +bool operator==(const types::ValuePtr>& lhs, const types::ValuePtr>& rhs); +bool operator!=(const types::ValuePtr>& lhs, const types::ValuePtr>& rhs); + +struct ValuesMap : std::unordered_map +{ + using unordered_map::unordered_map; +}; + +bool operator==(const types::ValuePtr& lhs, const types::ValuePtr& rhs); +bool operator!=(const types::ValuePtr& lhs, const types::ValuePtr& rhs); + +/*! + * \brief Information about user-callable parameters passed from Jinja2 call context + * + * This structure prepared by the Jinja2C++ engine and filled by information about call parameters gathered from the + * call context. See documentation for \ref UserCallable for detailed information + * + */ +struct UserCallableParams +{ + //! Values of parameters mapped according to \ref UserCallable::argsInfo + ValuesMap args; + //! Values of extra positional args got from the call expression + Value extraPosArgs; + //! Values of extra named args got from the call expression + Value extraKwArgs; + //! Context object which provides access to the current variables set of the template + Value context; + bool paramsParsed = false; + + Value operator[](const std::string& paramName) const + { + auto p = args.find(paramName); + if (p == args.end()) + return Value(); + + return p->second; + } +}; + +/*! + * \brief Information about one argument of the user-defined callable + * + * This structure is used as a description of the user-callable argument. Information from this structure is used + * by the Jinja2C++ engine to map actual call parameters to the expected ones by the user-defined callable. + */ +struct ArgInfo +{ + //! Name of the argument + std::string paramName; + //! Mandatory flag + bool isMandatory; + //! Default value for the argument + Value defValue; + + ArgInfo(std::string name, bool isMandat = false, Value defVal = Value()) + : paramName(std::move(name)) + , isMandatory(isMandat) + , defValue(std::move(defVal)) {} +}; + +inline bool operator==(const ArgInfo& lhs, const ArgInfo& rhs) +{ + if (lhs.paramName != rhs.paramName) + return false; + if (lhs.isMandatory != rhs.isMandatory) + return false; + return lhs.defValue == rhs.defValue; +} + +inline bool operator!=(const ArgInfo& lhs, const ArgInfo& rhs) +{ + return !(lhs == rhs); +} + +template +struct ArgInfoT : public ArgInfo +{ + using type = T; + + using ArgInfo::ArgInfo; + ArgInfoT(const ArgInfo& info) + : ArgInfo(info) + { + } + ArgInfoT(ArgInfo&& info) noexcept + : ArgInfo(std::move(info)) + { + } +}; + +/*! + * \brief User-callable descriptor + * + * This descriptor is used for description of the user-defined callables passed to the Jinja2C++ engine. Information + * from this descriptor is used by the engine to properly parse and prepare of the call parameters and pass it to the + * user-callable. For instance, such kind of user-defined callable passed as a parameter: + * ```c++ + * jinja2::UserCallable uc; + * uc.callable = [](auto& params)->jinja2::Value { + * auto str1 = params["str1"]; + * auto str2 = params["str2"]; + * + * if (str1.isString()) + * return str1.asString() + " " + str2.asString(); + * + * return str1.asWString() + L" " + str2.asWString(); + * }; + * uc.argsInfo = {{"str1", true}, {"str2", true}}; + * params["test"] = std::move(uc); + * ``` + * This declaration defines user-defined callable which takes two named parameters: `str1` and `str2`. Further, it's + * possible to call this user-defined callable from the Jinja2 template this way: + * ```jinja2 + * {{ test('Hello', 'World!') }} + * ``` + * or: + * ``` + * {{ test(str2='World!', str1='Hello') }} + * ``` + * Jinja2C++ engine maps actual call parameters according the information from \ref UserCallable::argsInfo field and + * pass them as a \ref UserCallableParams structure. Every named param (explicitly defined in the call or it's default value) + * passed throught \ref UserCallableParams::args field. Every extra positional param mentoined in call passed as \ref + * UserCallableParams::extraPosArgs. Every extra named param mentoined in call passed as \ref + * UserCallableParams::extraKwArgs. + * + * If any of argument, marked as `mandatory` in the \ref UserCallable::argsInfo field is missed in the point of the + * user-defined call the call is failed. + */ +struct JINJA2CPP_EXPORT UserCallable +{ + using UserCallableFunctionPtr = std::function; + UserCallable() : m_counter(++m_gen) {} + UserCallable(const UserCallableFunctionPtr& fptr, const std::vector& argsInfos) + : callable(fptr) + , argsInfo(argsInfos) + , m_counter(++m_gen) + { + } + UserCallable(const UserCallable& other) + : callable(other.callable) + , argsInfo(other.argsInfo) + , m_counter(other.m_counter) + { + } + UserCallable& operator=(const UserCallable& other) + { + if (*this == other) + return *this; + UserCallable temp(other); + + using std::swap; + swap(callable, temp.callable); + swap(argsInfo, temp.argsInfo); + swap(m_counter, temp.m_counter); + + return *this; + } + + UserCallable(UserCallable&& other) noexcept + : callable(std::move(other.callable)) + , argsInfo(std::move(other.argsInfo)) + , m_counter(other.m_counter) + { + } + + UserCallable& operator=(UserCallable&& other) noexcept + { + callable = std::move(other.callable); + argsInfo = std::move(other.argsInfo); + m_counter = other.m_counter; + + return *this; + } + + bool IsEqual(const UserCallable& other) const + { + return m_counter == other.m_counter; + } + + //! Functional object which is actually handle the call + UserCallableFunctionPtr callable; + //! Information about arguments of the user-defined callable + std::vector argsInfo; + +private: + static std::atomic_uint64_t m_gen; + uint64_t m_counter{}; +}; + +bool operator==(const UserCallable& lhs, const UserCallable& rhs); +bool operator!=(const UserCallable& lhs, const UserCallable& rhs); +bool operator==(const types::ValuePtr& lhs, const types::ValuePtr& rhs); +bool operator!=(const types::ValuePtr& lhs, const types::ValuePtr& rhs); + + +inline Value::Value(const UserCallable& callable) + : m_data(types::MakeValuePtr(callable)) +{ +} + +inline Value::Value(UserCallable&& callable) + : m_data(types::MakeValuePtr(std::move(callable))) +{ +} + inline Value GenericMap::GetValueByName(const std::string& name) const { - return m_accessor()->GetValueByName(name); + return m_accessor ? m_accessor()->GetValueByName(name) : Value(); +} +inline auto GenericMap::operator[](const std::string& name) const +{ + return GetValueByName(name); } -inline Value GenericMap::GetValueByIndex(int64_t index) const +inline Value::Value() = default; +inline Value::Value(const Value& val) = default; +inline Value::Value(Value&& val) noexcept + : m_data(std::move(val.m_data)) { - return m_accessor()->GetValueByIndex(index); } +inline Value::~Value() = default; +inline Value& Value::operator=(const Value&) = default; +inline Value& Value::operator=(Value&& val) noexcept +{ + if (this == &val) + return *this; -inline Value GenericList::GetValueByIndex(int64_t index) const + m_data.swap(val.m_data); + return *this; +} +inline Value::Value(const ValuesMap& map) + : m_data(types::MakeValuePtr(map)) +{ +} +inline Value::Value(const ValuesList& list) + : m_data(types::MakeValuePtr(list)) +{ +} +inline Value::Value(ValuesList&& list) noexcept + : m_data(types::MakeValuePtr(std::move(list))) +{ +} +inline Value::Value(ValuesMap&& map) noexcept + : m_data(types::MakeValuePtr(std::move(map))) { - return m_accessor()->GetValueByIndex(index); } -} // jinja2 +} // namespace jinja2 -#endif // JINJA2_VALUE_H +#endif // JINJA2CPP_VALUE_H diff --git a/include/jinja2cpp/value_ptr.h b/include/jinja2cpp/value_ptr.h new file mode 100644 index 00000000..26eee6b6 --- /dev/null +++ b/include/jinja2cpp/value_ptr.h @@ -0,0 +1,29 @@ +#ifndef JINJA2CPP_VALUE_PTR_H +#define JINJA2CPP_VALUE_PTR_H + +#include "polymorphic_value.h" + +namespace jinja2 { +namespace types { + +using namespace isocpp_p0201; + +template +using ValuePtr = polymorphic_value; + +template +ValuePtr MakeValuePtr(Ts&&... ts) +{ + return make_polymorphic_value(std::forward(ts)...); +} + +template +ValuePtr MakeValuePtr(Ts&&... ts) +{ + return make_polymorphic_value(std::forward(ts)...); +} + +} // namespace types +} // namespace jinja2 + +#endif // JINJA2CPP_VALUE_PTR_H diff --git a/jinja2cpp.pc.in b/jinja2cpp.pc.in new file mode 100644 index 00000000..009fe53c --- /dev/null +++ b/jinja2cpp.pc.in @@ -0,0 +1,12 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=@CMAKE_INSTALL_PREFIX@ +libdir=${exec_prefix}/@CMAKE_INSTALL_LIBDIR@ +includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@ + +Name: @PROJECT_NAME@ +Description: @PROJECT_DESCRIPTION@ +Version: @PROJECT_VERSION@ + + Requires: +Libs: -L${libdir} -ljinja2cpp +Cflags: -I${includedir} \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 00000000..edbf92fc --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -ex + +export BUILD_TARGET="all" +export CMAKE_OPTS="-DCMAKE_VERBOSE_MAKEFILE=OFF" +if [[ "${COMPILER}" != "" ]]; then export CXX=${COMPILER}; fi +if [[ "${BUILD_CONFIG}" == "" ]]; then export BUILD_CONFIG="Release"; fi +if [[ "${COLLECT_COVERAGE}" != "" ]]; then export BUILD_CONFIG="Debug" && export CMAKE_OPTS="${CMAKE_OPTS} -DJINJA2CPP_WITH_COVERAGE=ON"; fi +if [[ "${SANITIZE_BUILD}" != "" ]]; then export BUILD_CONFIG="RelWithDebInfo" && export CMAKE_OPTS="${CMAKE_OPTS} -DJINJA2CPP_WITH_SANITIZERS=${SANITIZE_BUILD}"; fi +$CXX --version +mkdir -p build && cd build +cmake $CMAKE_OPTS -DCMAKE_BUILD_TYPE=$BUILD_CONFIG -DCMAKE_CXX_FLAGS=$CMAKE_CXX_FLAGS -DJINJA2CPP_DEPS_MODE=internal $EXTRA_FLAGS .. && cmake --build . --config $BUILD_CONFIG --target all -- -j4 +ctest -C $BUILD_CONFIG -V diff --git a/src/ast_visitor.h b/src/ast_visitor.h new file mode 100644 index 00000000..01b51982 --- /dev/null +++ b/src/ast_visitor.h @@ -0,0 +1,117 @@ +#ifndef JINJA2CPP_SRC_AST_VISITOR_H +#define JINJA2CPP_SRC_AST_VISITOR_H + +namespace jinja2 +{ +class IRendererBase; +class ExpressionEvaluatorBase; +class Statement; +class ForStatement; +class IfStatement; +class ElseBranchStatement; +class SetStatement; +class ParentBlockStatement; +class BlockStatement; +class ExtendsStatement; +class IncludeStatement; +class ImportStatement; +class MacroStatement; +class MacroCallStatement; +class ComposedRenderer; +class RawTextRenderer; +class ExpressionRenderer; + +class StatementVisitor; + +class VisitableStatement +{ +public: + virtual ~VisitableStatement() = default; + + virtual void ApplyVisitor(StatementVisitor* visitor) = 0; + virtual void ApplyVisitor(StatementVisitor* visitor) const = 0; +}; + +#define VISITABLE_STATEMENT() \ + void ApplyVisitor(StatementVisitor* visitor) override {visitor->DoVisit(this);} \ + void ApplyVisitor(StatementVisitor* visitor) const override {visitor->DoVisit(this);} \ + +namespace detail +{ +template +class VisitorIfaceImpl : public Base +{ +public: + using Base::DoVisit; + + virtual void DoVisit(Type*) {} + virtual void DoVisit(const Type*) {} +}; + +template +class VisitorIfaceImpl +{ +public: + virtual void DoVisit(Type*) {} + virtual void DoVisit(const Type*) {} +}; + +template +struct VisitorBaseImpl; + +template +struct VisitorBaseImpl +{ + using current_base = VisitorIfaceImpl; + using base_type = typename VisitorBaseImpl::base_type; +}; + +template +struct VisitorBaseImpl +{ + using base_type = VisitorIfaceImpl; +}; + + +template +struct VisitorBase +{ + using type = typename VisitorBaseImpl::base_type; +}; +} // namespace detail + +template +using VisitorBase = typename detail::VisitorBase::type; + +class StatementVisitor : public VisitorBase< + IRendererBase, + Statement, + ForStatement, + IfStatement, + ElseBranchStatement, + SetStatement, + ParentBlockStatement, + BlockStatement, + ExtendsStatement, + IncludeStatement, + ImportStatement, + MacroStatement, + MacroCallStatement, + ComposedRenderer, + RawTextRenderer, + ExpressionRenderer> +{ +public: + void Visit(VisitableStatement* stmt) + { + stmt->ApplyVisitor(this); + } + void Visit(const VisitableStatement* stmt) + { + stmt->ApplyVisitor(this); + } +}; +} // namespace jinja2 + + +#endif diff --git a/src/binding/boost_json_serializer.cpp b/src/binding/boost_json_serializer.cpp new file mode 100644 index 00000000..7dbabbbe --- /dev/null +++ b/src/binding/boost_json_serializer.cpp @@ -0,0 +1,194 @@ +#include "boost_json_serializer.h" + +#include "../value_visitors.h" +#include +#include + +template <> struct fmt::formatter : ostream_formatter {}; + +namespace jinja2 +{ +namespace boost_json_serializer +{ +namespace +{ +struct JsonInserter : visitors::BaseVisitor +{ + using BaseVisitor::operator(); + + explicit JsonInserter() {} + + boost::json::value operator()(const ListAdapter& list) const + { + boost::json::array listValue; //(boost::json::kind::array); + + for (auto& v : list) + { + listValue.push_back(Apply(v)); + } + return listValue; + } + + boost::json::value operator()(const MapAdapter& map) const + { + boost::json::object mapNode; //(boost::json::kind::object); + + const auto& keys = map.GetKeys(); + for (auto& k : keys) + { + mapNode.emplace(k.c_str(), Apply(map.GetValueByName(k))); + } + + return mapNode; + } + + boost::json::value operator()(const KeyValuePair& kwPair) const + { + boost::json::object pairNode; //(boost::json::kind::object); + pairNode.emplace(kwPair.key.c_str(), Apply(kwPair.value)); + + return pairNode; + } + + boost::json::value operator()(const std::string& str) const { return boost::json::value(str.c_str()); } + + boost::json::value operator()(const nonstd::string_view& str) const + { + return boost::json::value(boost::json::string(str.data(), str.size())); + } + + boost::json::value operator()(const std::wstring& str) const + { + auto s = ConvertString(str); + return boost::json::value(s.c_str()); + } + + boost::json::value operator()(const nonstd::wstring_view& str) const + { + auto s = ConvertString(str); + return boost::json::value(s.c_str()); + } + + boost::json::value operator()(bool val) const { return boost::json::value(val); } + + boost::json::value operator()(EmptyValue) const { return boost::json::value(); } + + boost::json::value operator()(const Callable&) const { return boost::json::value(""); } + + boost::json::value operator()(double val) const { return boost::json::value(val); } + + boost::json::value operator()(int64_t val) const { return boost::json::value(val); } + +}; +} // namespace + +DocumentWrapper::DocumentWrapper() +{ +} + +ValueWrapper DocumentWrapper::CreateValue(const InternalValue& value) const +{ + auto v = Apply(value); + return ValueWrapper(std::move(v)); +} + +ValueWrapper::ValueWrapper(boost::json::value&& value) + : m_value(std::move(value)) +{ +} + +void PrettyPrint(fmt::basic_memory_buffer& os, const boost::json::value& jv, uint8_t indent = 4, int level = 0) +{ + switch (jv.kind()) + { + case boost::json::kind::object: + { + fmt::format_to(std::back_inserter(os), "{}", '{'); + if (indent != 0) + { + fmt::format_to(std::back_inserter(os), "{}", "\n"); + } + const auto& obj = jv.get_object(); + if (!obj.empty()) + { + auto it = obj.begin(); + for (;;) + { + auto key = boost::json::serialize(it->key()); + fmt::format_to( + std::back_inserter(os), + "{: >{}}{: <{}}", + key, + key.size() + indent * (level + 1), + ":", + (indent == 0) ? 0 : 2 + ); + PrettyPrint(os, it->value(), indent, level + 1); + if (++it == obj.end()) + break; + fmt::format_to(std::back_inserter(os), "{: <{}}", ",", (indent == 0) ? 0 : 2); + } + } + if (indent != 0) + { + fmt::format_to(std::back_inserter(os), "{}", "\n"); + } + fmt::format_to(std::back_inserter(os), "{: >{}}", "}", (indent * level) + 1); + break; + } + + case boost::json::kind::array: + { + fmt::format_to(std::back_inserter(os), "["); + auto const& arr = jv.get_array(); + if (!arr.empty()) + { + auto it = arr.begin(); + for (;;) + { + PrettyPrint(os, *it, indent, level + 1); + if (++it == arr.end()) + break; + fmt::format_to(std::back_inserter(os), "{: <{}}", ",", (indent == 0) ? 0 : 2); + } + } + fmt::format_to(std::back_inserter(os), "]"); + break; + } + + case boost::json::kind::string: + { + fmt::format_to(std::back_inserter(os), "{}", boost::json::serialize(jv.get_string())); + break; + } + + case boost::json::kind::uint64: + case boost::json::kind::int64: + case boost::json::kind::double_: + { + fmt::format_to(std::back_inserter(os), "{}", jv); + break; + } + case boost::json::kind::bool_: + { + fmt::format_to(std::back_inserter(os), "{}", jv.get_bool()); + break; + } + + case boost::json::kind::null: + { + fmt::format_to(std::back_inserter(os), "null"); + break; + } + } +} + +std::string ValueWrapper::AsString(const uint8_t indent) const +{ + fmt::memory_buffer out; + PrettyPrint(out, m_value, indent); + return fmt::to_string(out); +} + +} // namespace boost_json_serializer +} // namespace jinja2 diff --git a/src/binding/boost_json_serializer.h b/src/binding/boost_json_serializer.h new file mode 100644 index 00000000..162333e4 --- /dev/null +++ b/src/binding/boost_json_serializer.h @@ -0,0 +1,47 @@ +#ifndef JINJA2CPP_SRC_BOOST_JSON_SERIALIZER_H +#define JINJA2CPP_SRC_BOOST_JSON_SERIALIZER_H + +#include "../internal_value.h" + +#include + +#include + +namespace jinja2 +{ +namespace boost_json_serializer +{ + +class ValueWrapper +{ + friend class DocumentWrapper; + +public: + ValueWrapper(ValueWrapper&&) = default; + ValueWrapper& operator=(ValueWrapper&&) = default; + + std::string AsString(uint8_t indent = 0) const; + +private: + ValueWrapper(boost::json::value&& value); + + boost::json::value m_value; +}; + +class DocumentWrapper +{ +public: + DocumentWrapper(); + + DocumentWrapper(DocumentWrapper&&) = default; + DocumentWrapper& operator=(DocumentWrapper&&) = default; + + ValueWrapper CreateValue(const InternalValue& value) const; + +private: +}; + +} // namespace boost_json_serializer +} // namespace jinja2 + +#endif // JINJA2CPP_SRC_BOOST_JSON_SERIALIZER_H diff --git a/src/binding/rapid_json_serializer.cpp b/src/binding/rapid_json_serializer.cpp new file mode 100644 index 00000000..bd7e5a32 --- /dev/null +++ b/src/binding/rapid_json_serializer.cpp @@ -0,0 +1,127 @@ +#include "rapid_json_serializer.h" + +#include "../value_visitors.h" + +#include + +namespace jinja2 +{ +namespace rapidjson_serializer +{ +namespace +{ +struct JsonInserter : visitors::BaseVisitor +{ + using BaseVisitor::operator(); + + explicit JsonInserter(rapidjson::Document::AllocatorType& allocator) + : m_allocator(allocator) + { + } + + rapidjson::Value operator()(const ListAdapter& list) const + { + rapidjson::Value listValue(rapidjson::kArrayType); + + for (auto& v : list) + { + listValue.PushBack(Apply(v, m_allocator), m_allocator); + } + return listValue; + } + + rapidjson::Value operator()(const MapAdapter& map) const + { + rapidjson::Value mapNode(rapidjson::kObjectType); + + const auto& keys = map.GetKeys(); + for (auto& k : keys) + { + mapNode.AddMember(rapidjson::Value(k.c_str(), m_allocator), Apply(map.GetValueByName(k), m_allocator), m_allocator); + } + + return mapNode; + } + + rapidjson::Value operator()(const KeyValuePair& kwPair) const + { + rapidjson::Value pairNode(rapidjson::kObjectType); + pairNode.AddMember(rapidjson::Value(kwPair.key.c_str(), m_allocator), Apply(kwPair.value, m_allocator), m_allocator); + + return pairNode; + } + + rapidjson::Value operator()(const std::string& str) const { return rapidjson::Value(str.c_str(), m_allocator); } + + rapidjson::Value operator()(const nonstd::string_view& str) const + { + return rapidjson::Value(str.data(), static_cast(str.size()), m_allocator); + } + + rapidjson::Value operator()(const std::wstring& str) const + { + auto s = ConvertString(str); + return rapidjson::Value(s.c_str(), m_allocator); + } + + rapidjson::Value operator()(const nonstd::wstring_view& str) const + { + auto s = ConvertString(str); + return rapidjson::Value(s.c_str(), m_allocator); + } + + rapidjson::Value operator()(bool val) const { return rapidjson::Value(val); } + + rapidjson::Value operator()(EmptyValue) const { return rapidjson::Value(); } + + rapidjson::Value operator()(const Callable&) const { return rapidjson::Value(""); } + + rapidjson::Value operator()(double val) const { return rapidjson::Value(val); } + + rapidjson::Value operator()(int64_t val) const { return rapidjson::Value(val); } + + rapidjson::Document::AllocatorType& m_allocator; +}; +} // namespace + +DocumentWrapper::DocumentWrapper() + : m_document(std::make_shared()) +{ +} + +ValueWrapper DocumentWrapper::CreateValue(const InternalValue& value) const +{ + auto v = Apply(value, m_document->GetAllocator()); + return ValueWrapper(std::move(v), m_document); +} + +ValueWrapper::ValueWrapper(rapidjson::Value&& value, std::shared_ptr document) + : m_value(std::move(value)) + , m_document(document) +{ +} + +std::string ValueWrapper::AsString(const uint8_t indent) const +{ + using Writer = rapidjson::Writer>; + using PrettyWriter = rapidjson::PrettyWriter>; + + rapidjson::StringBuffer buffer; + if (indent == 0) + { + Writer writer(buffer); + m_value.Accept(writer); + } + else + { + PrettyWriter writer(buffer); + writer.SetIndent(' ', indent); + writer.SetFormatOptions(rapidjson::kFormatSingleLineArray); + m_value.Accept(writer); + } + + return buffer.GetString(); +} + +} // namespace rapidjson_serializer +} // namespace jinja2 diff --git a/src/binding/rapid_json_serializer.h b/src/binding/rapid_json_serializer.h new file mode 100644 index 00000000..66e26c14 --- /dev/null +++ b/src/binding/rapid_json_serializer.h @@ -0,0 +1,50 @@ +#ifndef JINJA2CPP_SRC_RAPID_JSON_SERIALIZER_H +#define JINJA2CPP_SRC_RAPID_JSON_SERIALIZER_H + +#include "../internal_value.h" + +#include +#include + +#include + +namespace jinja2 +{ +namespace rapidjson_serializer +{ + +class ValueWrapper +{ + friend class DocumentWrapper; + +public: + ValueWrapper(ValueWrapper&&) = default; + ValueWrapper& operator=(ValueWrapper&&) = default; + + std::string AsString(uint8_t indent = 0) const; + +private: + ValueWrapper(rapidjson::Value&& value, std::shared_ptr document); + + rapidjson::Value m_value; + std::shared_ptr m_document; +}; + +class DocumentWrapper +{ +public: + DocumentWrapper(); + + DocumentWrapper(DocumentWrapper&&) = default; + DocumentWrapper& operator=(DocumentWrapper&&) = default; + + ValueWrapper CreateValue(const InternalValue& value) const; + +private: + std::shared_ptr m_document; +}; + +} // namespace rapidjson_serializer +} // namespace jinja2 + +#endif // JINJA2CPP_SRC_RAPID_JSON_SERIALIZER_H diff --git a/src/error_handling.h b/src/error_handling.h index 2a2e0916..eb28561d 100644 --- a/src/error_handling.h +++ b/src/error_handling.h @@ -1,5 +1,5 @@ -#ifndef ERROR_HANDLING_H -#define ERROR_HANDLING_H +#ifndef JINJA2CPP_SRC_ERROR_HANDLING_H +#define JINJA2CPP_SRC_ERROR_HANDLING_H #include "lexer.h" #include @@ -24,6 +24,25 @@ struct ParseError , errorToken(tok) , relatedTokens(toks) {} + ParseError(const ParseError&) = default; + ParseError(ParseError&& other) noexcept(true) + : errorCode(std::move(other.errorCode)) + , errorToken(std::move(other.errorToken)) + , relatedTokens(std::move(other.relatedTokens)) + {} + + ParseError& operator =(const ParseError&) = default; + ParseError& operator =(ParseError&& error) noexcept + { + if (this == &error) + return *this; + + std::swap(errorCode, error.errorCode); + std::swap(errorToken, error.errorToken); + std::swap(relatedTokens, error.relatedTokens); + + return *this; + } ErrorCode errorCode; Token errorToken; @@ -40,5 +59,5 @@ inline auto MakeParseError(ErrorCode code, Token tok, std::initializer_list #include "helpers.h" -namespace jinja2 -{ +#include +#include +#include +#include namespace { -template -struct ValueRenderer : boost::static_visitor +template +struct ValueRenderer { - std::basic_ostream& os; - - ValueRenderer(std::basic_ostream& osRef) : os(osRef) {} + using CharT = typename FmtCtx::char_type; + FmtCtx* ctx; - void operator() (bool val) const + explicit ValueRenderer(FmtCtx* c) + : ctx(c) { - os << (val ? UNIVERSAL_STR("True") : UNIVERSAL_STR("False")); } - void operator() (const EmptyValue&) const + + constexpr void operator()(bool val) const { + fmt::format_to( + ctx->out(), + UNIVERSAL_STR("{}").GetValue(), + (val ? UNIVERSAL_STR("True").GetValue(): UNIVERSAL_STR("False").GetValue())); + } + void operator()(const jinja2::EmptyValue&) const { fmt::format_to(ctx->out(), UNIVERSAL_STR("").GetValue()); } + template + void operator()(const std::basic_string& val) const { + fmt::format_to(ctx->out(), UNIVERSAL_STR("{}").GetValue(), jinja2::ConvertString>(val)); } + template - void operator() (const std::basic_string& val) const + void operator()(const nonstd::basic_string_view& val) const { - os << ConvertString>(val); + fmt::format_to(ctx->out(), UNIVERSAL_STR("{}").GetValue(), jinja2::ConvertString>(val)); } - void operator() (const ValuesList& vals) const + void operator()(const jinja2::ValuesList& vals) const { - os << '{'; + fmt::format_to(ctx->out(), UNIVERSAL_STR("{{").GetValue()); bool isFirst = true; for (auto& val : vals) { if (isFirst) isFirst = false; else - os << UNIVERSAL_STR(", "); - - boost::apply_visitor(ValueRenderer(os), val.data()); + fmt::format_to(ctx->out(), UNIVERSAL_STR(", ").GetValue()); + nonstd::visit(ValueRenderer(ctx), val.data()); } - os << '}'; + fmt::format_to(ctx->out(), UNIVERSAL_STR("}}").GetValue()); } - void operator() (const ValuesMap& vals) const + void operator()(const jinja2::ValuesMap& vals) const { - os << '{'; + fmt::format_to(ctx->out(), UNIVERSAL_STR("{{").GetValue()); bool isFirst = true; for (auto& val : vals) { if (isFirst) isFirst = false; else - os << UNIVERSAL_STR(", "); + fmt::format_to(ctx->out(), UNIVERSAL_STR(", ").GetValue()); - os << '{' << '"' << ConvertString>(val.first) << '"' << ','; - boost::apply_visitor(ValueRenderer(os), val.second.data()); - os << '}'; + fmt::format_to(ctx->out(), UNIVERSAL_STR("{{\"{}\",").GetValue(), jinja2::ConvertString>(val.first)); + nonstd::visit(ValueRenderer(ctx), val.second.data()); + fmt::format_to(ctx->out(), UNIVERSAL_STR("}}").GetValue()); } - os << '}'; + fmt::format_to(ctx->out(), UNIVERSAL_STR("}}").GetValue()); } - void operator() (const GenericMap& /*val*/) const + template + void operator()(const jinja2::RecWrapper& val) const { + this->operator()(const_cast(*val)); } - void operator() (const GenericList& /*val*/) const - { - } + void operator()(const jinja2::GenericMap& /*val*/) const {} + + void operator()(const jinja2::GenericList& /*val*/) const {} + + void operator()(const jinja2::UserCallable& /*val*/) const {} template - void operator() (const T& val) const + void operator()(const T& val) const { - os << val; + fmt::format_to(ctx->out(), UNIVERSAL_STR("{}").GetValue(), val); } }; +} // namespace +namespace fmt +{ template -std::basic_ostream& operator << (std::basic_ostream& os, Value val) +struct formatter +{ + template + constexpr auto parse(ParseContext& ctx) + { + return ctx.begin(); + } + + template + auto format(const jinja2::Value& val, FormatContext& ctx) + { + nonstd::visit(ValueRenderer(&ctx), val.data()); + return fmt::format_to(ctx.out(), UNIVERSAL_STR("").GetValue()); + } +}; +} // namespace fmt + +namespace jinja2 { - boost::apply_visitor(ValueRenderer(os), val.data()); - return os; -} -} template -void RenderErrorInfo(std::basic_ostream& os, const ErrorInfoTpl& errInfo) +void RenderErrorInfo(std::basic_string& result, const ErrorInfoTpl& errInfo) { using string_t = std::basic_string; + auto out = fmt::basic_memory_buffer(); auto& loc = errInfo.GetErrorLocation(); - os << ConvertString(loc.fileName) << ':' << loc.line << ':' << loc.col << ':'; - os << UNIVERSAL_STR(" error: "); + + fmt::format_to(std::back_inserter(out), UNIVERSAL_STR("{}:{}:{}: error: ").GetValue(), ConvertString(loc.fileName), loc.line, loc.col); ErrorCode errCode = errInfo.GetCode(); switch (errCode) { case ErrorCode::Unspecified: - os << UNIVERSAL_STR("Parse error"); - break; + format_to(std::back_inserter(out), UNIVERSAL_STR("Parse error").GetValue()); + break; case ErrorCode::UnexpectedException: - os << UNIVERSAL_STR("Unexpected exception occurred during template processing"); + { + auto& extraParams = errInfo.GetExtraParams(); + format_to(std::back_inserter(out), UNIVERSAL_STR("Unexpected exception occurred during template processing. Exception: {}").GetValue(), extraParams[0]); + break; + } + case ErrorCode::MetadataParseError: + { + auto& extraParams = errInfo.GetExtraParams(); + format_to(std::back_inserter(out), UNIVERSAL_STR("Error occurred during template metadata parsing. Error: {}").GetValue(), extraParams[0]); break; + } case ErrorCode::YetUnsupported: - os << UNIVERSAL_STR("This feature has not been supported yet"); + format_to(std::back_inserter(out), UNIVERSAL_STR("This feature has not been supported yet").GetValue()); break; case ErrorCode::FileNotFound: - os << UNIVERSAL_STR("File not found"); + format_to(std::back_inserter(out), UNIVERSAL_STR("File not found").GetValue()); break; case ErrorCode::ExpectedStringLiteral: - os << UNIVERSAL_STR("String expected"); + format_to(std::back_inserter(out), UNIVERSAL_STR("String expected").GetValue()); break; case ErrorCode::ExpectedIdentifier: - os << UNIVERSAL_STR("Identifier expected"); + format_to(std::back_inserter(out), UNIVERSAL_STR("Identifier expected").GetValue()); break; case ErrorCode::ExpectedSquareBracket: - os << UNIVERSAL_STR("']' expected"); + format_to(std::back_inserter(out), UNIVERSAL_STR("']' expected").GetValue()); break; case ErrorCode::ExpectedRoundBracket: - os << UNIVERSAL_STR("')' expected"); + format_to(std::back_inserter(out), UNIVERSAL_STR("')' expected").GetValue()); break; case ErrorCode::ExpectedCurlyBracket: - os << UNIVERSAL_STR("'}' expected"); + format_to(std::back_inserter(out), UNIVERSAL_STR("'}}' expected").GetValue()); break; case ErrorCode::ExpectedToken: { auto& extraParams = errInfo.GetExtraParams(); - os << UNIVERSAL_STR("Unexpected token '") << extraParams[0] << '\''; + format_to(std::back_inserter(out), UNIVERSAL_STR("Unexpected token '{}'").GetValue(), extraParams[0]); if (extraParams.size() > 1) { - os << UNIVERSAL_STR(". Expected: "); - for (int i = 1; i < extraParams.size(); ++ i) + format_to(std::back_inserter(out), UNIVERSAL_STR(". Expected: ").GetValue()); + for (std::size_t i = 1; i < extraParams.size(); ++ i) { if (i != 1) - os << UNIVERSAL_STR(", "); - os << '\'' << extraParams[i] << '\''; + format_to(std::back_inserter(out), UNIVERSAL_STR(", ").GetValue()); + format_to(std::back_inserter(out), UNIVERSAL_STR("\'{}\'").GetValue(), extraParams[i]); } } break; @@ -140,57 +180,110 @@ void RenderErrorInfo(std::basic_ostream& os, const ErrorInfoTpl& e case ErrorCode::ExpectedExpression: { auto& extraParams = errInfo.GetExtraParams(); - os << UNIVERSAL_STR("Expected expression, got: '") << extraParams[0] << '\''; + format_to(std::back_inserter(out), UNIVERSAL_STR("Expected expression, got: '{}'").GetValue(), extraParams[0]); break; } case ErrorCode::ExpectedEndOfStatement: { auto& extraParams = errInfo.GetExtraParams(); - os << UNIVERSAL_STR("Expected end of statement, got: '") << extraParams[0] << '\''; + format_to(std::back_inserter(out), UNIVERSAL_STR("Expected end of statement, got: '{}'").GetValue(), extraParams[0]); break; } + case ErrorCode::ExpectedRawEnd: + format_to(std::back_inserter(out), UNIVERSAL_STR("Expected end of raw block").GetValue()); + break; + case ErrorCode::ExpectedMetaEnd: + format_to(std::back_inserter(out), UNIVERSAL_STR("Expected end of meta block").GetValue()); + break; case ErrorCode::UnexpectedToken: { auto& extraParams = errInfo.GetExtraParams(); - os << UNIVERSAL_STR("Unexpected token: '") << extraParams[0] << '\''; + format_to(std::back_inserter(out), UNIVERSAL_STR("Unexpected token: '{}'").GetValue(), extraParams[0]); break; } case ErrorCode::UnexpectedStatement: { auto& extraParams = errInfo.GetExtraParams(); - os << UNIVERSAL_STR("Unexpected statement: '") << extraParams[0] << '\''; + format_to(std::back_inserter(out), UNIVERSAL_STR("Unexpected statement: '{}'").GetValue(), extraParams[0]); break; } case ErrorCode::UnexpectedCommentBegin: - os << UNIVERSAL_STR("Unexpected comment begin"); + format_to(std::back_inserter(out), UNIVERSAL_STR("Unexpected comment begin").GetValue()); break; case ErrorCode::UnexpectedCommentEnd: - os << UNIVERSAL_STR("Unexpected comment end"); + format_to(std::back_inserter(out), UNIVERSAL_STR("Unexpected comment end").GetValue()); + break; + case ErrorCode::UnexpectedRawBegin: + format_to(std::back_inserter(out), UNIVERSAL_STR("Unexpected raw block begin").GetValue()); + break; + case ErrorCode::UnexpectedRawEnd: + format_to(std::back_inserter(out), UNIVERSAL_STR("Unexpected raw block end").GetValue()); + break; + case ErrorCode::UnexpectedMetaBegin: + format_to(std::back_inserter(out), UNIVERSAL_STR("Unexpected meta block begin").GetValue()); + break; + case ErrorCode::UnexpectedMetaEnd: + format_to(std::back_inserter(out), UNIVERSAL_STR("Unexpected meta block end").GetValue()); break; case ErrorCode::UnexpectedExprBegin: - os << UNIVERSAL_STR("Unexpected expression block begin"); + format_to(std::back_inserter(out), UNIVERSAL_STR("Unexpected expression block begin").GetValue()); break; case ErrorCode::UnexpectedExprEnd: - os << UNIVERSAL_STR("Unexpected expression block end"); + format_to(std::back_inserter(out), UNIVERSAL_STR("Unexpected expression block end").GetValue()); break; case ErrorCode::UnexpectedStmtBegin: - os << UNIVERSAL_STR("Unexpected statement block begin"); + format_to(std::back_inserter(out), UNIVERSAL_STR("Unexpected statement block begin").GetValue()); break; case ErrorCode::UnexpectedStmtEnd: - os << UNIVERSAL_STR("Unexpected statement block end"); + format_to(std::back_inserter(out), UNIVERSAL_STR("Unexpected statement block end").GetValue()); + break; + case ErrorCode::TemplateNotParsed: + format_to(std::back_inserter(out), UNIVERSAL_STR("Template not parsed").GetValue()); + break; + case ErrorCode::TemplateNotFound: + format_to(std::back_inserter(out), UNIVERSAL_STR("Template(s) not found: {}").GetValue(), errInfo.GetExtraParams()[0]); + break; + case ErrorCode::InvalidTemplateName: + format_to(std::back_inserter(out), UNIVERSAL_STR("Invalid template name: {}").GetValue(), errInfo.GetExtraParams()[0]); + break; + case ErrorCode::InvalidValueType: + format_to(std::back_inserter(out), UNIVERSAL_STR("Invalid value type").GetValue()); + break; + case ErrorCode::ExtensionDisabled: + format_to(std::back_inserter(out), UNIVERSAL_STR("Extension disabled").GetValue()); + break; + case ErrorCode::TemplateEnvAbsent: + format_to(std::back_inserter(out), UNIVERSAL_STR("Template environment doesn't set").GetValue()); break; } - os << std::endl << errInfo.GetLocationDescr(); + format_to(std::back_inserter(out), UNIVERSAL_STR("\n{}").GetValue(), errInfo.GetLocationDescr()); + result = to_string(out); +} + +template<> +std::string ErrorInfoTpl::ToString() const +{ + std::string result; + RenderErrorInfo(result, *this); + return result; +} + +template<> +std::wstring ErrorInfoTpl::ToString() const +{ + std::wstring result; + RenderErrorInfo(result, *this); + return result; } std::ostream& operator << (std::ostream& os, const ErrorInfo& res) { - RenderErrorInfo(os, res); + os << res.ToString(); return os; } std::wostream& operator << (std::wostream& os, const ErrorInfoW& res) { - RenderErrorInfo(os, res); + os << res.ToString(); return os; } -} // jinja2 +} // namespace jinja2 diff --git a/src/expression_evaluator.cpp b/src/expression_evaluator.cpp index d5bb2306..019f1493 100644 --- a/src/expression_evaluator.cpp +++ b/src/expression_evaluator.cpp @@ -1,5 +1,6 @@ #include "expression_evaluator.h" #include "filters.h" +#include "generic_adapters.h" #include "internal_value.h" #include "out_stream.h" #include "testers.h" @@ -9,6 +10,7 @@ #include #include +#include namespace jinja2 { @@ -25,19 +27,17 @@ InternalValue FullExpressionEvaluator::Evaluate(RenderContext& values) if (!m_expression) return InternalValue(); - InternalValue origVal = m_expression->Evaluate(values); - if (m_filter) - origVal = m_filter->Evaluate(origVal, values); + auto result = m_expression->Evaluate(values); if (m_tester && !m_tester->Evaluate(values)) return m_tester->EvaluateAltValue(values); - return origVal; + return result; } void FullExpressionEvaluator::Render(OutStream& stream, RenderContext& values) { - if (!m_filter && !m_tester) + if (!m_tester) m_expression->Render(stream, values); else Expression::Render(stream, values); @@ -55,7 +55,24 @@ InternalValue ValueRefExpression::Evaluate(RenderContext& values) InternalValue SubscriptExpression::Evaluate(RenderContext& values) { - return Subscript(m_value->Evaluate(values), m_subscriptExpr->Evaluate(values)); + InternalValue cur = m_value->Evaluate(values); + + for (auto idx : m_subscriptExprs) + { + auto subscript = idx->Evaluate(values); + auto newVal = Subscript(cur, subscript, &values); + if (cur.ShouldExtendLifetime()) + newVal.SetParentData(cur); + std::swap(newVal, cur); + } + + return cur; +} + +InternalValue FilteredExpression::Evaluate(RenderContext& values) +{ + auto origResult = m_expression->Evaluate(values); + return m_filter->Evaluate(origResult, values); } InternalValue UnaryExpression::Evaluate(RenderContext& values) @@ -70,7 +87,7 @@ BinaryExpression::BinaryExpression(BinaryExpression::Operation oper, ExpressionE { if (m_oper == In) { - CallParams params; + CallParamsInfo params; params.kwParams["seq"] = rightExpr; m_inTester = CreateTester("in", params); } @@ -89,7 +106,7 @@ InternalValue BinaryExpression::Evaluate(RenderContext& context) bool left = ConvertToBool(leftVal); if (left) left = ConvertToBool(rightVal); - result = left; + result = static_cast(left); break; } case jinja2::BinaryExpression::LogicalOr: @@ -97,7 +114,7 @@ InternalValue BinaryExpression::Evaluate(RenderContext& context) bool left = ConvertToBool(leftVal); if (!left) left = ConvertToBool(rightVal); - result = left; + result = static_cast(left); break; } case jinja2::BinaryExpression::LogicalEq: @@ -110,7 +127,7 @@ InternalValue BinaryExpression::Evaluate(RenderContext& context) case jinja2::BinaryExpression::Minus: case jinja2::BinaryExpression::Mul: case jinja2::BinaryExpression::Div: - case jinja2::BinaryExpression::DivReminder: + case jinja2::BinaryExpression::DivRemainder: case jinja2::BinaryExpression::DivInteger: case jinja2::BinaryExpression::Pow: result = Apply2(leftVal, rightVal, m_oper); @@ -125,16 +142,16 @@ InternalValue BinaryExpression::Evaluate(RenderContext& context) auto leftStr = context.GetRendererCallback()->GetAsTargetString(leftVal); auto rightStr = context.GetRendererCallback()->GetAsTargetString(rightVal); TargetString resultStr; - std::string* nleftStr = boost::get(&leftStr); + std::string* nleftStr = GetIf(&leftStr); if (nleftStr != nullptr) { - auto* nrightStr = boost::get(&rightStr); + auto* nrightStr = GetIf(&rightStr); resultStr = *nleftStr + *nrightStr; } else { - auto* wleftStr = boost::get(&leftStr); - auto* wrightStr = boost::get(&rightStr); + auto* wleftStr = GetIf(&leftStr); + auto* wrightStr = GetIf(&rightStr); resultStr = *wleftStr + *wrightStr; } result = InternalValue(std::move(resultStr)); @@ -165,12 +182,12 @@ InternalValue DictCreator::Evaluate(RenderContext& context) result[e.first] = e.second->Evaluate(context); } - return MapAdapter::CreateAdapter(std::move(result));; + return CreateMapAdapter(std::move(result));; } -ExpressionFilter::ExpressionFilter(std::string filterName, CallParams params) +ExpressionFilter::ExpressionFilter(const std::string& filterName, CallParamsInfo params) { - m_filter = CreateFilter(std::move(filterName), std::move(params)); + m_filter = CreateFilter(filterName, std::move(params)); if (!m_filter) throw std::runtime_error("Can't find filter '" + filterName + "'"); } @@ -183,10 +200,10 @@ InternalValue ExpressionFilter::Evaluate(const InternalValue& baseVal, RenderCon return m_filter->Filter(baseVal, context); } -IsExpression::IsExpression(ExpressionEvaluatorPtr<> value, std::string tester, CallParams params) +IsExpression::IsExpression(ExpressionEvaluatorPtr<> value, const std::string& tester, CallParamsInfo params) : m_value(value) { - m_tester = CreateTester(std::move(tester), std::move(params)); + m_tester = CreateTester(tester, std::move(params)); if (!m_tester) throw std::runtime_error("Can't find tester '" + tester + "'"); } @@ -220,18 +237,7 @@ InternalValue DictionaryCreator::Evaluate(RenderContext& context) InternalValue CallExpression::Evaluate(RenderContext& values) { - enum - { - InvalidFn = -1, - RangeFn = 1, - LoopCycleFn = 2 - }; - - auto& scope = values.EnterScope(); - scope["range"] = InternalValue(static_cast(RangeFn)); - scope["loop"] = MapAdapter::CreateAdapter(InternalValueMap{{"cycle", InternalValue(static_cast(LoopCycleFn))}}); auto fn = m_valueRef->Evaluate(values); - values.ExitScope(); auto fnId = ConvertToInt(fn, InvalidFn); @@ -243,20 +249,18 @@ InternalValue CallExpression::Evaluate(RenderContext& values) case LoopCycleFn: return CallLoopCycle(values); default: - break; + return CallArbitraryFn(values); } - - return InternalValue(); } void CallExpression::Render(OutStream& stream, RenderContext& values) { auto fnVal = m_valueRef->Evaluate(values); - Callable* callable = boost::get(&fnVal); + const Callable* callable = GetIf(&fnVal); if (callable == nullptr) { - fnVal = Subscript(fnVal, std::string("operator()")); - callable = boost::get(&fnVal); + fnVal = Subscript(fnVal, std::string("operator()"), &values); + callable = GetIf(&fnVal); if (callable == nullptr) { Expression::Render(stream, values); @@ -264,21 +268,52 @@ void CallExpression::Render(OutStream& stream, RenderContext& values) } } + auto callParams = helpers::EvaluateCallParams(m_params, values); + if (callable->GetType() == Callable::Type::Expression) { - stream.WriteValue(callable->GetExpressionCallable()(m_params, values)); + stream.WriteValue(callable->GetExpressionCallable()(callParams, values)); } else { - callable->GetStatementCallable()(m_params, stream, values); + callable->GetStatementCallable()(callParams, stream, values); + } +} + +InternalValue CallExpression::CallArbitraryFn(RenderContext& values) +{ + auto fnVal = m_valueRef->Evaluate(values); + Callable* callable = GetIf(&fnVal); + if (callable == nullptr) + { + fnVal = Subscript(fnVal, std::string("operator()"), nullptr); + callable = GetIf(&fnVal); + if (callable == nullptr) + return InternalValue(); } + + auto kind = callable->GetKind(); + if (kind != Callable::GlobalFunc && kind != Callable::UserCallable && kind != Callable::Macro) + return InternalValue(); + + auto callParams = helpers::EvaluateCallParams(m_params, values); + + if (callable->GetType() == Callable::Type::Expression) + { + return callable->GetExpressionCallable()(callParams, values); + } + + TargetString resultStr; + auto stream = values.GetRendererCallback()->GetStreamOnString(resultStr); + callable->GetStatementCallable()(callParams, stream, values); + return resultStr; } InternalValue CallExpression::CallGlobalRange(RenderContext& values) { bool isArgsParsed = true; - auto args = helpers::ParseCallParams({{"start"}, {"stop", true}, {"step"}}, m_params, isArgsParsed); + auto args = helpers::ParseCallParamsInfo({ { "start" }, { "stop", true }, { "step" } }, m_params, isArgsParsed); if (!isArgsParsed) return InternalValue(); @@ -305,34 +340,12 @@ InternalValue CallExpression::CallGlobalRange(RenderContext& values) return InternalValue(); } - class RangeGenerator : public IListAccessor - { - public: - RangeGenerator(int64_t start, int64_t stop, int64_t step) - : m_start(start) - , m_stop(stop) - , m_step(step) - { - } + auto distance = stop - start; + auto items_count = distance / step; + items_count = items_count < 0 ? 0 : static_cast(items_count); - size_t GetSize() const override - { - auto distance = m_stop - m_start; - auto count = distance / m_step; - return count < 0 ? 0 : static_cast(count); - } - InternalValue GetValueByIndex(int64_t idx) const override - { - return m_start + m_step * idx; - } - - private: - int64_t m_start; - int64_t m_stop; - int64_t m_step; - }; - - return ListAdapter([accessor = RangeGenerator(start, stop, step)]() -> const IListAccessor* {return &accessor;}); + return ListAdapter::CreateAdapter(static_cast(items_count), + [start, step](size_t idx) { return InternalValue(static_cast(start + step * idx)); }); } InternalValue CallExpression::CallLoopCycle(RenderContext& values) @@ -342,12 +355,19 @@ InternalValue CallExpression::CallLoopCycle(RenderContext& values) if (!loopFound) return InternalValue(); - auto loop = boost::get(&loopValP->second); + auto loop = GetIf(&loopValP->second); int64_t baseIdx = Apply(loop->GetValueByName("index0")); auto idx = static_cast(baseIdx % m_params.posParams.size()); return m_params.posParams[idx]->Evaluate(values); } + +void SetupGlobals(InternalValueMap& globalParams) +{ + globalParams["range"] = InternalValue(static_cast(RangeFn)); + // globalParams["loop"] = MapAdapter::CreateAdapter(InternalValueMap{{"cycle", InternalValue(static_cast(LoopCycleFn))}}); +} + namespace helpers { enum ArgState @@ -355,7 +375,8 @@ enum ArgState NotFound, NotFoundMandatory, Keyword, - Positional + Positional, + Ignored }; enum ParamState @@ -366,8 +387,23 @@ enum ParamState MappedKw, }; -template -ParsedArguments ParseCallParamsImpl(const T& args, const CallParams& params, bool& isSucceeded) +template +struct ParsedArgumentDefaultValGetter; + +template<> +struct ParsedArgumentDefaultValGetter +{ + static auto Get(const InternalValue& val) { return val; } +}; + +template<> +struct ParsedArgumentDefaultValGetter +{ + static auto Get(const InternalValue& val) { return std::make_shared(val); } +}; + +template +Result ParseCallParamsImpl(const T& args, const P& params, bool& isSucceeded) { struct ArgInfo { @@ -382,23 +418,32 @@ ParsedArguments ParseCallParamsImpl(const T& args, const CallParams& params, boo isSucceeded = true; - ParsedArguments result; + Result result; int argIdx = 0; int firstMandatoryIdx = -1; int prevNotFound = -1; int foundKwArgs = 0; + (void)foundKwArgs; // extremely odd bug in clang warning + // Wunused-but-set-variable // Find all provided keyword args for (auto& argInfo : args) { argsInfo[argIdx].info = &argInfo; + + if (argInfo.name == "*args" || argInfo.name=="**kwargs") + { + argsInfo[argIdx ++].state = Ignored; + continue; + } + auto p = params.kwParams.find(argInfo.name); if (p != params.kwParams.end()) { result.args[argInfo.name] = p->second; argsInfo[argIdx].state = Keyword; - ++ foundKwArgs; + ++foundKwArgs; } else { @@ -425,13 +470,13 @@ ParsedArguments ParseCallParamsImpl(const T& args, const CallParams& params, boo ++ argIdx; } - int startPosArg = firstMandatoryIdx == -1 ? 0 : firstMandatoryIdx; - int curPosArg = startPosArg; - int eatenPosArgs = 0; + std::size_t startPosArg = firstMandatoryIdx == -1 ? 0 : firstMandatoryIdx; + std::size_t curPosArg = startPosArg; + std::size_t eatenPosArgs = 0; // Determine the range for positional arguments scanning bool isFirstTime = true; - for (; eatenPosArgs < posParamsInfo.size(); ++ eatenPosArgs) + for (; eatenPosArgs < posParamsInfo.size() && startPosArg < args.size(); eatenPosArgs = eatenPosArgs + (argsInfo[startPosArg].state == Ignored ? 0 : 1)) { if (isFirstTime) { @@ -439,13 +484,15 @@ ParsedArguments ParseCallParamsImpl(const T& args, const CallParams& params, boo ; isFirstTime = false; + if (startPosArg == args.size()) + break; continue; } - int prevNotFound = argsInfo[startPosArg].prevNotFound; + prevNotFound = argsInfo[startPosArg].prevNotFound; if (prevNotFound != -1) { - startPosArg = prevNotFound; + startPosArg = static_cast(prevNotFound); } else if (curPosArg == args.size()) { @@ -456,31 +503,44 @@ ParsedArguments ParseCallParamsImpl(const T& args, const CallParams& params, boo int nextPosArg = argsInfo[curPosArg].nextNotFound; if (nextPosArg == -1) break; - curPosArg = nextPosArg; + curPosArg = static_cast(nextPosArg); } } // Map positional params to the desired arguments - int curArg = startPosArg; - for (int idx = 0; idx < eatenPosArgs && curArg != -1; ++ idx, curArg = argsInfo[curArg].nextNotFound) + auto curArg = static_cast(startPosArg); + for (std::size_t idx = 0; idx < eatenPosArgs && curArg != -1 && static_cast(curArg) < argsInfo.size(); ++ idx, curArg = argsInfo[curArg].nextNotFound) { + if (argsInfo[curArg].state == Ignored) + continue; + result.args[argsInfo[curArg].info->name] = params.posParams[idx]; argsInfo[curArg].state = Positional; } // Fill default arguments (if missing) and check for mandatory - for (int idx = 0; idx < argsInfo.size(); ++ idx) + for (std::size_t idx = 0; idx < argsInfo.size(); ++ idx) { auto& argInfo = argsInfo[idx]; switch (argInfo.state) { case Positional: case Keyword: + case Ignored: continue; case NotFound: { if (!IsEmpty(argInfo.info->defaultVal)) - result.args[argInfo.info->name] = std::make_shared(argInfo.info->defaultVal); + { +#if __cplusplus >= 201703L + if constexpr (std::is_same::value) + result.args[argInfo.info->name] = std::make_shared(argInfo.info->defaultVal); + else + result.args[argInfo.info->name] = argInfo.info->defaultVal; +#else + result.args[argInfo.info->name] = ParsedArgumentDefaultValGetter::Get(argInfo.info->defaultVal); +#endif + } break; } case NotFoundMandatory: @@ -507,13 +567,36 @@ ParsedArguments ParseCallParamsImpl(const T& args, const CallParams& params, boo ParsedArguments ParseCallParams(const std::initializer_list& args, const CallParams& params, bool& isSucceeded) { - return ParseCallParamsImpl(args, params, isSucceeded); + return ParseCallParamsImpl(args, params, isSucceeded); } ParsedArguments ParseCallParams(const std::vector& args, const CallParams& params, bool& isSucceeded) { - return ParseCallParamsImpl(args, params, isSucceeded); + return ParseCallParamsImpl(args, params, isSucceeded); +} + +ParsedArgumentsInfo ParseCallParamsInfo(const std::initializer_list& args, const CallParamsInfo& params, bool& isSucceeded) +{ + return ParseCallParamsImpl(args, params, isSucceeded); } +ParsedArgumentsInfo ParseCallParamsInfo(const std::vector& args, const CallParamsInfo& params, bool& isSucceeded) +{ + return ParseCallParamsImpl(args, params, isSucceeded); } + +CallParams EvaluateCallParams(const CallParamsInfo& info, RenderContext& context) +{ + CallParams result; + + for (auto& p : info.posParams) + result.posParams.push_back(p->Evaluate(context)); + + for (auto& kw : info.kwParams) + result.kwParams[kw.first] = kw.second->Evaluate(context); + + return result; } + +} // namespace helpers +} // namespace jinja2 diff --git a/src/expression_evaluator.h b/src/expression_evaluator.h index fab44320..5bd7031d 100644 --- a/src/expression_evaluator.h +++ b/src/expression_evaluator.h @@ -1,16 +1,25 @@ -#ifndef EXPRESSION_EVALUATOR_H -#define EXPRESSION_EVALUATOR_H +#ifndef JINJA2CPP_SRC_EXPRESSION_EVALUATOR_H +#define JINJA2CPP_SRC_EXPRESSION_EVALUATOR_H #include "internal_value.h" #include "render_context.h" +#include + #include #include namespace jinja2 { -class ExpressionEvaluatorBase +enum +{ + InvalidFn = -1, + RangeFn = 1, + LoopCycleFn = 2 +}; + +class ExpressionEvaluatorBase : public IComparable { public: virtual ~ExpressionEvaluatorBase() {} @@ -23,16 +32,63 @@ template using ExpressionEvaluatorPtr = std::shared_ptr; using Expression = ExpressionEvaluatorBase; +inline bool operator==(const ExpressionEvaluatorPtr<>& lhs, const ExpressionEvaluatorPtr<>& rhs) +{ + if (lhs && rhs && !lhs->IsEqual(*rhs)) + return false; + if ((lhs && !rhs) || (!lhs && rhs)) + return false; + return true; +} +inline bool operator!=(const ExpressionEvaluatorPtr<>& lhs, const ExpressionEvaluatorPtr<>& rhs) +{ + return !(lhs == rhs); +} + struct CallParams +{ + std::unordered_map kwParams; + std::vector posParams; +}; + +inline bool operator==(const CallParams& lhs, const CallParams& rhs) +{ + if (lhs.kwParams != rhs.kwParams) + return false; + if (lhs.posParams != rhs.posParams) + return false; + return true; +} + +inline bool operator!=(const CallParams& lhs, const CallParams& rhs) +{ + return !(lhs == rhs); +} + +struct CallParamsInfo { std::unordered_map> kwParams; std::vector> posParams; }; +inline bool operator==(const CallParamsInfo& lhs, const CallParamsInfo& rhs) +{ + if (lhs.kwParams != rhs.kwParams) + return false; + if (lhs.posParams != rhs.posParams) + return false; + return true; +} + +inline bool operator!=(const CallParamsInfo& lhs, const CallParamsInfo& rhs) +{ + return !(lhs == rhs); +} + struct ArgumentInfo { std::string name; - bool mandatory; + bool mandatory = false; InternalValue defaultVal; ArgumentInfo(std::string argName, bool isMandatory = false, InternalValue def = InternalValue()) @@ -43,13 +99,29 @@ struct ArgumentInfo } }; -struct ParsedArguments +inline bool operator==(const ArgumentInfo& lhs, const ArgumentInfo& rhs) +{ + if (lhs.name != rhs.name) + return false; + if (lhs.mandatory != rhs.mandatory) + return false; + if (!(lhs.defaultVal == rhs.defaultVal)) + return false; + return true; +} + +inline bool operator!=(const ArgumentInfo& lhs, const ArgumentInfo& rhs) +{ + return !(lhs == rhs); +} + +struct ParsedArgumentsInfo { std::unordered_map> args; std::unordered_map> extraKwArgs; std::vector> extraPosArgs; - ExpressionEvaluatorPtr<> operator[](std::string name) const + ExpressionEvaluatorPtr<> operator[](const std::string& name) const { auto p = args.find(name); if (p == args.end()) @@ -59,6 +131,54 @@ struct ParsedArguments } }; +inline bool operator==(const ParsedArgumentsInfo& lhs, const ParsedArgumentsInfo& rhs) +{ + if (lhs.args != rhs.args) + return false; + if (lhs.extraKwArgs != rhs.extraKwArgs) + return false; + if (lhs.extraPosArgs != rhs.extraPosArgs) + return false; + return true; +} + +inline bool operator!=(const ParsedArgumentsInfo& lhs, const ParsedArgumentsInfo& rhs) +{ + return !(lhs == rhs); +} + +struct ParsedArguments +{ + std::unordered_map args; + std::unordered_map extraKwArgs; + std::vector extraPosArgs; + + InternalValue operator[](const std::string& name) const + { + auto p = args.find(name); + if (p == args.end()) + return InternalValue(); + + return p->second; + } +}; + +inline bool operator==(const ParsedArguments& lhs, const ParsedArguments& rhs) +{ + if (lhs.args != rhs.args) + return false; + if (lhs.extraKwArgs != rhs.extraKwArgs) + return false; + if (lhs.extraPosArgs != rhs.extraPosArgs) + return false; + return true; +} + +inline bool operator!=(const ParsedArguments& lhs, const ParsedArguments& rhs) +{ + return !(lhs == rhs); +} + class ExpressionFilter; class IfExpression; @@ -67,21 +187,28 @@ class FullExpressionEvaluator : public ExpressionEvaluatorBase public: void SetExpression(ExpressionEvaluatorPtr expr) { - m_expression = expr; - } - void SetFilter(ExpressionEvaluatorPtr expr) - { - m_filter = expr; + m_expression = std::move(expr); } void SetTester(ExpressionEvaluatorPtr expr) { - m_tester = expr; + m_tester = std::move(expr); } InternalValue Evaluate(RenderContext& values) override; void Render(OutStream &stream, RenderContext &values) override; + + bool IsEqual(const IComparable& other) const override + { + const auto* eval = dynamic_cast(&other); + if (!eval) + return false; + if (m_expression != eval->m_expression) + return false; + if (m_tester != eval->m_tester) + return false; + return true; + } private: ExpressionEvaluatorPtr m_expression; - ExpressionEvaluatorPtr m_filter; ExpressionEvaluatorPtr m_tester; }; @@ -89,10 +216,18 @@ class ValueRefExpression : public Expression { public: ValueRefExpression(std::string valueName) - : m_valueName(valueName) + : m_valueName(std::move(valueName)) { } InternalValue Evaluate(RenderContext& values) override; + + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + return m_valueName != value->m_valueName; + } private: std::string m_valueName; }; @@ -100,15 +235,57 @@ class ValueRefExpression : public Expression class SubscriptExpression : public Expression { public: - SubscriptExpression(ExpressionEvaluatorPtr value, ExpressionEvaluatorPtr subscriptExpr) + SubscriptExpression(ExpressionEvaluatorPtr value) : m_value(value) - , m_subscriptExpr(subscriptExpr) { } InternalValue Evaluate(RenderContext& values) override; + void AddIndex(ExpressionEvaluatorPtr value) + { + m_subscriptExprs.push_back(value); + } + + bool IsEqual(const IComparable& other) const override + { + auto* otherPtr = dynamic_cast(&other); + if (!otherPtr) + return false; + if (m_value != otherPtr->m_value) + return false; + if (m_subscriptExprs != otherPtr->m_subscriptExprs) + return false; + return true; + } + private: ExpressionEvaluatorPtr m_value; - ExpressionEvaluatorPtr m_subscriptExpr; + std::vector> m_subscriptExprs; +}; + +class FilteredExpression : public Expression +{ +public: + explicit FilteredExpression(ExpressionEvaluatorPtr expression, ExpressionEvaluatorPtr filter) + : m_expression(std::move(expression)) + , m_filter(std::move(filter)) + { + } + InternalValue Evaluate(RenderContext&) override; + bool IsEqual(const IComparable& other) const override + { + auto* otherPtr = dynamic_cast(&other); + if (!otherPtr) + return false; + if (m_expression != otherPtr->m_expression) + return false; + if (m_filter != otherPtr->m_filter) + return false; + return true; + } + +private: + ExpressionEvaluatorPtr m_expression; + ExpressionEvaluatorPtr m_filter; }; class ConstantExpression : public Expression @@ -121,6 +298,14 @@ class ConstantExpression : public Expression { return m_constant; } + + bool IsEqual(const IComparable& other) const override + { + auto* otherVal = dynamic_cast(&other); + if (!otherVal) + return false; + return m_constant == otherVal->m_constant; + } private: InternalValue m_constant; }; @@ -135,6 +320,13 @@ class TupleCreator : public Expression InternalValue Evaluate(RenderContext&) override; + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return m_exprs == val->m_exprs; + } private: std::vector> m_exprs; }; @@ -163,6 +355,13 @@ class DictCreator : public Expression InternalValue Evaluate(RenderContext&) override; + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return m_exprs == val->m_exprs; + } private: std::unordered_map> m_exprs; }; @@ -182,6 +381,19 @@ class UnaryExpression : public Expression , m_expr(expr) {} InternalValue Evaluate(RenderContext&) override; + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_oper != val->m_oper) + return false; + if (m_expr != val->m_expr) + return false; + return true; + } + private: Operation m_oper; ExpressionEvaluatorPtr<> m_expr; @@ -192,20 +404,34 @@ class IsExpression : public Expression public: virtual ~IsExpression() {} - struct ITester + struct ITester : IComparable { virtual ~ITester() {} virtual bool Test(const InternalValue& baseVal, RenderContext& context) = 0; }; + using TesterPtr = std::shared_ptr; + using TesterFactoryFn = std::function; - using TesterFactoryFn = std::function (CallParams params)>; - - IsExpression(ExpressionEvaluatorPtr<> value, std::string tester, CallParams params); + IsExpression(ExpressionEvaluatorPtr<> value, const std::string& tester, CallParamsInfo params); InternalValue Evaluate(RenderContext& context) override; + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_value != val->m_value) + return false; + if (m_tester != val->m_tester) + return false; + if (m_tester && val->m_tester && !m_tester->IsEqual(*val->m_tester)) + return false; + return true; + } + private: ExpressionEvaluatorPtr<> m_value; - std::shared_ptr m_tester; + TesterPtr m_tester; }; class BinaryExpression : public Expression @@ -226,7 +452,7 @@ class BinaryExpression : public Expression Minus, Mul, Div, - DivReminder, + DivRemainder, DivInteger, Pow, StringConcat @@ -241,11 +467,29 @@ class BinaryExpression : public Expression BinaryExpression(Operation oper, ExpressionEvaluatorPtr<> leftExpr, ExpressionEvaluatorPtr<> rightExpr); InternalValue Evaluate(RenderContext&) override; + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_oper != val->m_oper) + return false; + if (m_leftExpr != val->m_leftExpr) + return false; + if (m_rightExpr != val->m_rightExpr) + return false; + if (m_inTester && val->m_inTester && !m_inTester->IsEqual(*val->m_inTester)) + return false; + if ((!m_inTester && val->m_inTester) || (m_inTester && !val->m_inTester)) + return false; + return true; + } private: Operation m_oper; ExpressionEvaluatorPtr<> m_leftExpr; ExpressionEvaluatorPtr<> m_rightExpr; - std::shared_ptr m_inTester; + IsExpression::TesterPtr m_inTester; }; @@ -254,7 +498,7 @@ class CallExpression : public Expression public: virtual ~CallExpression() {} - CallExpression(ExpressionEvaluatorPtr<> valueRef, CallParams params) + CallExpression(ExpressionEvaluatorPtr<> valueRef, CallParamsInfo params) : m_valueRef(std::move(valueRef)) , m_params(std::move(params)) { @@ -266,42 +510,67 @@ class CallExpression : public Expression auto& GetValueRef() const {return m_valueRef;} auto& GetParams() const {return m_params;} + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_valueRef != val->m_valueRef) + return false; + return m_params == val->m_params; + } private: + InternalValue CallArbitraryFn(RenderContext &values); InternalValue CallGlobalRange(RenderContext &values); InternalValue CallLoopCycle(RenderContext &values); private: ExpressionEvaluatorPtr<> m_valueRef; - CallParams m_params; + CallParamsInfo m_params; }; -class ExpressionFilter +class ExpressionFilter : public IComparable { public: virtual ~ExpressionFilter() {} - struct IExpressionFilter + struct IExpressionFilter : IComparable { virtual ~IExpressionFilter() {} virtual InternalValue Filter(const InternalValue& baseVal, RenderContext& context) = 0; }; + using ExpressionFilterPtr = std::shared_ptr; + using FilterFactoryFn = std::function; - using FilterFactoryFn = std::function (CallParams params)>; - - ExpressionFilter(std::string filterName, CallParams params); + ExpressionFilter(const std::string& filterName, CallParamsInfo params); InternalValue Evaluate(const InternalValue& baseVal, RenderContext& context); void SetParentFilter(std::shared_ptr parentFilter) { - m_parentFilter = parentFilter; + m_parentFilter = std::move(parentFilter); + } + bool IsEqual(const IComparable& other) const override + { + auto* valuePtr = dynamic_cast(&other); + if (!valuePtr) + return false; + if (m_filter && valuePtr->m_filter && !m_filter->IsEqual(*valuePtr->m_filter)) + return false; + if ((m_filter && !valuePtr->m_filter) || (!m_filter && !valuePtr->m_filter)) + return false; + if (m_parentFilter != valuePtr->m_parentFilter) + return false; + return true; } + + private: - std::shared_ptr m_filter; + ExpressionFilterPtr m_filter; std::shared_ptr m_parentFilter; }; -class IfExpression +class IfExpression : public IComparable { public: virtual ~IfExpression() {} @@ -317,7 +586,19 @@ class IfExpression void SetAltValue(ExpressionEvaluatorPtr<> altValue) { - m_altValue = altValue; + m_altValue = std::move(altValue); + } + + bool IsEqual(const IComparable& other) const override + { + auto* valPtr = dynamic_cast(&other); + if (!valPtr) + return false; + if (m_testExpr != valPtr->m_testExpr) + return false; + if (m_altValue != valPtr->m_altValue) + return false; + return true; } private: @@ -329,7 +610,10 @@ namespace helpers { ParsedArguments ParseCallParams(const std::initializer_list& argsInfo, const CallParams& params, bool& isSucceeded); ParsedArguments ParseCallParams(const std::vector& args, const CallParams& params, bool& isSucceeded); -} -} // jinja2 +ParsedArgumentsInfo ParseCallParamsInfo(const std::initializer_list& argsInfo, const CallParamsInfo& params, bool& isSucceeded); +ParsedArgumentsInfo ParseCallParamsInfo(const std::vector& args, const CallParamsInfo& params, bool& isSucceeded); +CallParams EvaluateCallParams(const CallParamsInfo& info, RenderContext& context); +} // namespace helpers +} // namespace jinja2 -#endif // EXPRESSION_EVALUATOR_H +#endif // JINJA2CPP_SRC_EXPRESSION_EVALUATOR_H diff --git a/src/expression_parser.cpp b/src/expression_parser.cpp index bd7945cd..3567d966 100644 --- a/src/expression_parser.cpp +++ b/src/expression_parser.cpp @@ -1,6 +1,8 @@ #include "expression_parser.h" #include +#include +#include namespace jinja2 { @@ -15,7 +17,7 @@ auto ReplaceErrorIfPossible(T& result, const Token& pivotTok, ErrorCode newError return result.get_unexpected(); } -ExpressionParser::ExpressionParser() +ExpressionParser::ExpressionParser(const Settings& /* settings */, TemplateEnv* /* env */) { } @@ -51,23 +53,13 @@ ExpressionParser::ParseResult> E return value.get_unexpected(); evaluator->SetExpression(*value); - if (lexer.EatIfEqual('|')) - { - auto filter = ParseFilterExpression(lexer); - if (!filter) - return filter.get_unexpected(); - evaluator->SetFilter(*filter); - } - if (includeIfPart && lexer.EatIfEqual(Token::If)) + if (includeIfPart && lexer.EatIfEqual(Keyword::If)) { - if (includeIfPart) - { - auto ifExpr = ParseIfExpression(lexer); - if (!ifExpr) - return ifExpr.get_unexpected(); - evaluator->SetTester(*ifExpr); - } + auto ifExpr = ParseIfExpression(lexer); + if (!ifExpr) + return ifExpr.get_unexpected(); + evaluator->SetTester(*ifExpr); } saver.Commit(); @@ -79,7 +71,7 @@ ExpressionParser::ParseResult> ExpressionPars { auto left = ParseLogicalAnd(lexer); - if (left && lexer.EatIfEqual(Token::LogicalOr)) + if (left && lexer.EatIfEqual(Keyword::LogicalOr)) { auto right = ParseLogicalOr(lexer); if (!right) @@ -95,7 +87,7 @@ ExpressionParser::ParseResult> ExpressionPars { auto left = ParseLogicalCompare(lexer); - if (left && lexer.EatIfEqual(Token::LogicalAnd)) + if (left && lexer.EatIfEqual(Keyword::LogicalAnd)) { auto right = ParseLogicalAnd(lexer); if (!right) @@ -135,29 +127,33 @@ ExpressionParser::ParseResult> ExpressionPars case Token::LessEqual: operation = BinaryExpression::LogicalLe; break; - case Token::In: - operation = BinaryExpression::In; - break; - case Token::Is: - { - Token nextTok = lexer.NextToken(); - if (nextTok != Token::Identifier) - return MakeParseError(ErrorCode::ExpectedIdentifier, nextTok); - - std::string name = AsString(nextTok.value); - ParseResult params; - - if (lexer.EatIfEqual('(')) - params = ParseCallParams(lexer); - - if (!params) - return params.get_unexpected(); - - return std::make_shared(*left, std::move(name), std::move(*params)); - } default: - lexer.ReturnToken(); - return left; + switch (lexer.GetAsKeyword(tok)) + { + case Keyword::In: + operation = BinaryExpression::In; + break; + case Keyword::Is: + { + Token nextTok = lexer.NextToken(); + if (nextTok != Token::Identifier) + return MakeParseError(ErrorCode::ExpectedIdentifier, nextTok); + + std::string name = AsString(nextTok.value); + ParseResult params; + + if (lexer.EatIfEqual('(')) + params = ParseCallParams(lexer); + + if (!params) + return params.get_unexpected(); + + return std::make_shared(*left, std::move(name), std::move(*params)); + } + default: + lexer.ReturnToken(); + return left; + } } auto right = ParseStringConcat(lexer); @@ -246,7 +242,7 @@ ExpressionParser::ParseResult> ExpressionPars operation = BinaryExpression::DivInteger; break; case '%': - operation = BinaryExpression::DivReminder; + operation = BinaryExpression::DivRemainder; break; default: lexer.ReturnToken(); @@ -262,31 +258,50 @@ ExpressionParser::ParseResult> ExpressionPars ExpressionParser::ParseResult> ExpressionParser::ParseUnaryPlusMinus(LexScanner& lexer) { - Token tok = lexer.NextToken(); - if (tok != '+' && tok != '-' && tok != Token::LogicalNot) - { + const auto tok = lexer.NextToken(); + const auto isUnary = tok == '+' || tok == '-' || lexer.GetAsKeyword(tok) == Keyword::LogicalNot; + if (!isUnary) lexer.ReturnToken(); - return ParseValueExpression(lexer); - } - + auto subExpr = ParseValueExpression(lexer); if (!subExpr) return subExpr; - return std::make_shared(tok == '+' ? UnaryExpression::UnaryPlus : (tok == '-' ? UnaryExpression::UnaryMinus : UnaryExpression::LogicalNot), *subExpr); + ExpressionEvaluatorPtr result; + if (isUnary) + result = std::make_shared(tok == '+' ? UnaryExpression::UnaryPlus : (tok == '-' ? UnaryExpression::UnaryMinus : UnaryExpression::LogicalNot), *subExpr); + else + result = subExpr.value(); + + if (lexer.EatIfEqual('|')) + { + auto filter = ParseFilterExpression(lexer); + if (!filter) + return filter.get_unexpected(); + result = std::make_shared(std::move(result), *filter); + } + + return result; } ExpressionParser::ParseResult> ExpressionParser::ParseValueExpression(LexScanner& lexer) { Token tok = lexer.NextToken(); + static const std::unordered_set forbiddenKw = {Keyword::Is, Keyword::In, Keyword::If, Keyword::Else}; ParseResult> valueRef; switch (tok.type) { case Token::Identifier: + { + auto kwType = lexer.GetAsKeyword(tok); + if (forbiddenKw.count(kwType) != 0) + return MakeParseError(ErrorCode::UnexpectedToken, tok); + valueRef = std::make_shared(AsString(tok.value)); break; + } case Token::IntegerNum: case Token::FloatNum: case Token::String: @@ -355,7 +370,7 @@ ExpressionParser::ParseResult> ExpressionPars ExpressionEvaluatorPtr result; std::unordered_map> items; - if (lexer.EatIfEqual(']')) + if (lexer.EatIfEqual('}')) return std::make_shared(std::move(items)); do @@ -420,7 +435,7 @@ ExpressionParser::ParseResult> ExpressionPars { ExpressionEvaluatorPtr result; - ParseResult params = ParseCallParams(lexer); + ParseResult params = ParseCallParams(lexer); if (!params) return params.get_unexpected(); @@ -429,9 +444,9 @@ ExpressionParser::ParseResult> ExpressionPars return result; } -ExpressionParser::ParseResult ExpressionParser::ParseCallParams(LexScanner& lexer) +ExpressionParser::ParseResult ExpressionParser::ParseCallParams(LexScanner& lexer) { - CallParams result; + CallParamsInfo result; if (lexer.EatIfEqual(')')) return result; @@ -471,6 +486,7 @@ ExpressionParser::ParseResult ExpressionParser::ParseCallParams(LexS ExpressionParser::ParseResult> ExpressionParser::ParseSubscript(LexScanner& lexer, ExpressionEvaluatorPtr valueRef) { + ExpressionEvaluatorPtr result = std::make_shared(valueRef); for (Token tok = lexer.NextToken(); tok.type == '.' || tok.type == '['; tok = lexer.NextToken()) { ParseResult> indexExpr; @@ -496,12 +512,12 @@ ExpressionParser::ParseResult> ExpressionPars return MakeParseError(ErrorCode::ExpectedSquareBracket, lexer.PeekNextToken()); } - valueRef = std::make_shared(valueRef, *indexExpr); + result->AddIndex(*indexExpr); } lexer.ReturnToken(); - return valueRef; + return result; } ExpressionParser::ParseResult> ExpressionParser::ParseFilterExpression(LexScanner& lexer) @@ -518,7 +534,7 @@ ExpressionParser::ParseResult> Expressi return MakeParseError(ErrorCode::ExpectedIdentifier, tok); std::string name = AsString(tok.value); - ParseResult params; + ParseResult params; if (lexer.NextToken() == '(') params = ParseCallParams(lexer); @@ -564,7 +580,7 @@ ExpressionParser::ParseResult> ExpressionPa return testExpr.get_unexpected(); ParseResult> altValue; - if (lexer.PeekNextToken() == Token::Else) + if (lexer.GetAsKeyword(lexer.PeekNextToken()) == Keyword::Else) { lexer.EatToken(); auto value = ParseFullExpression(lexer); @@ -587,4 +603,4 @@ ExpressionParser::ParseResult> ExpressionPa return result; } -} // jinja2 +} // namespace jinja2 diff --git a/src/expression_parser.h b/src/expression_parser.h index 9ba0d909..c9053707 100644 --- a/src/expression_parser.h +++ b/src/expression_parser.h @@ -1,5 +1,5 @@ -#ifndef EXPRESSION_PARSER_H -#define EXPRESSION_PARSER_H +#ifndef JINJA2CPP_SRC_EXPRESSION_PARSER_H +#define JINJA2CPP_SRC_EXPRESSION_PARSER_H #include "lexer.h" #include "error_handling.h" @@ -7,6 +7,7 @@ #include "renderer.h" #include +#include namespace jinja2 { @@ -16,10 +17,11 @@ class ExpressionParser template using ParseResult = nonstd::expected; - ExpressionParser(); + explicit ExpressionParser(const Settings& settings, TemplateEnv* env = nullptr); ParseResult Parse(LexScanner& lexer); ParseResult> ParseFullExpression(LexScanner& lexer, bool includeIfPart = true); - ParseResult ParseCallParams(LexScanner& lexer); + ParseResult ParseCallParams(LexScanner& lexer); + ParseResult> ParseFilterExpression(LexScanner& lexer); private: ParseResult> ParseLogicalNot(LexScanner& lexer); ParseResult> ParseLogicalOr(LexScanner& lexer); @@ -36,13 +38,12 @@ class ExpressionParser ParseResult> ParseTuple(LexScanner& lexer); ParseResult> ParseCall(LexScanner& lexer, ExpressionEvaluatorPtr valueRef); ParseResult> ParseSubscript(LexScanner& lexer, ExpressionEvaluatorPtr valueRef); - ParseResult> ParseFilterExpression(LexScanner& lexer); ParseResult> ParseIfExpression(LexScanner& lexer); private: - ComposedRenderer* m_topLevelRenderer; + ComposedRenderer* m_topLevelRenderer = nullptr; }; -} // jinja2 +} // namespace jinja2 -#endif // EXPRESSION_PARSER_H +#endif // JINJA2CPP_SRC_EXPRESSION_PARSER_H diff --git a/src/filesystem_handler.cpp b/src/filesystem_handler.cpp index a29cdbe3..cade4b18 100644 --- a/src/filesystem_handler.cpp +++ b/src/filesystem_handler.cpp @@ -1,6 +1,8 @@ #include +#include #include +#include #include #include @@ -8,9 +10,9 @@ namespace jinja2 { -using TargetFileStream = boost::variant; +using TargetFileStream = nonstd::variant; -struct FileContentConverter : public boost::static_visitor +struct FileContentConverter { void operator() (const std::string& content, CharFileStreamPtr* sPtr) const { @@ -21,13 +23,13 @@ struct FileContentConverter : public boost::static_visitor { sPtr->reset(new std::wistringstream(content)); } - void operator() (const std::wstring& content, CharFileStreamPtr* sPtr) const + void operator() (const std::wstring&, CharFileStreamPtr*) const { // CharFileStreamPtr stream(new std::istringstream(content), [](std::istream* s) {delete static_cast(s);}); // std::swap(*sPtr, stream); } - void operator() (const std::string& content, WCharFileStreamPtr* sPtr) const + void operator() (const std::string&, WCharFileStreamPtr*) const { // WCharFileStreamPtr stream(new std::wistringstream(content), [](std::wistream* s) {delete static_cast(s);}); // std::swap(*sPtr, stream); @@ -36,12 +38,12 @@ struct FileContentConverter : public boost::static_visitor void MemoryFileSystem::AddFile(std::string fileName, std::string fileContent) { - m_filesMap[std::move(fileName)] = FileContent(std::move(fileContent)); + m_filesMap[std::move(fileName)] = FileContent{std::move(fileContent), {}}; } void MemoryFileSystem::AddFile(std::string fileName, std::wstring fileContent) { - m_filesMap[std::move(fileName)] = FileContent(std::move(fileContent)); + m_filesMap[std::move(fileName)] = FileContent{ {}, std::move(fileContent) }; } CharFileStreamPtr MemoryFileSystem::OpenStream(const std::string& name) const @@ -51,8 +53,15 @@ CharFileStreamPtr MemoryFileSystem::OpenStream(const std::string& name) const if (p == m_filesMap.end()) return result; - TargetFileStream targetStream(&result); - boost::apply_visitor(FileContentConverter(), p->second, targetStream); + auto& content = p->second; + + if (!content.narrowContent && !content.wideContent) + return result; + + if (!content.narrowContent) + content.narrowContent = ConvertString(content.wideContent.value()); + + result.reset(new std::istringstream(content.narrowContent.value())); return result; } @@ -64,42 +73,91 @@ WCharFileStreamPtr MemoryFileSystem::OpenWStream(const std::string& name) const if (p == m_filesMap.end()) return result; - TargetFileStream targetStream(&result); - boost::apply_visitor(FileContentConverter(), p->second, targetStream); + auto& content = p->second; + + if (!content.narrowContent && !content.wideContent) + return result; + + if (!content.wideContent) + content.wideContent = ConvertString(content.narrowContent.value()); + + result.reset(new std::wistringstream(content.wideContent.value())); return result; } +nonstd::optional MemoryFileSystem::GetLastModificationDate(const std::string&) const +{ + return nonstd::optional(); +} + +bool MemoryFileSystem::IsEqual(const IComparable& other) const +{ + auto* ptr = dynamic_cast(&other); + if (!ptr) + return false; + return m_filesMap == ptr->m_filesMap; +} RealFileSystem::RealFileSystem(std::string rootFolder) - : m_rootFolder(rootFolder) + : m_rootFolder(std::move(rootFolder)) { } -CharFileStreamPtr RealFileSystem::OpenStream(const std::string& name) const +std::string RealFileSystem::GetFullFilePath(const std::string& name) const { boost::filesystem::path root(m_rootFolder); root /= name; - const auto& filePath = root.string(); + return root.string(); +} + +CharFileStreamPtr RealFileSystem::OpenStream(const std::string& name) const +{ + auto filePath = GetFullFilePath(name); CharFileStreamPtr result(new std::ifstream(filePath), [](std::istream* s) {delete static_cast(s);}); if (result->good()) return result; - return CharFileStreamPtr(nullptr, [](std::istream* s){}); + return CharFileStreamPtr(nullptr, [](std::istream*){}); } WCharFileStreamPtr RealFileSystem::OpenWStream(const std::string& name) const +{ + auto filePath = GetFullFilePath(name); + + WCharFileStreamPtr result(new std::wifstream(filePath), [](std::wistream* s) {delete static_cast(s);}); + if (result->good()) + return result; + + return WCharFileStreamPtr(nullptr, [](std::wistream*){;}); +} +nonstd::optional RealFileSystem::GetLastModificationDate(const std::string& name) const { boost::filesystem::path root(m_rootFolder); root /= name; - const auto& filePath = root.string(); - WCharFileStreamPtr result(new std::wifstream(filePath), [](std::wistream* s) {delete static_cast(s);}); + auto modify_time = boost::filesystem::last_write_time(root); + + return std::chrono::system_clock::from_time_t(modify_time); +} +CharFileStreamPtr RealFileSystem::OpenByteStream(const std::string& name) const +{ + auto filePath = GetFullFilePath(name); + + CharFileStreamPtr result(new std::ifstream(filePath, std::ios_base::binary), [](std::istream* s) {delete static_cast(s);}); if (result->good()) return result; - return WCharFileStreamPtr(nullptr, [](std::wistream* s){;}); + return CharFileStreamPtr(nullptr, [](std::istream*){}); +} + +bool RealFileSystem::IsEqual(const IComparable& other) const +{ + auto* ptr = dynamic_cast(&other); + if (!ptr) + return false; + return m_rootFolder == ptr->m_rootFolder; } -} // jinja2 +} // namespace jinja2 diff --git a/src/filters.cpp b/src/filters.cpp index 1db7c579..3ab9f338 100644 --- a/src/filters.cpp +++ b/src/filters.cpp @@ -1,13 +1,17 @@ #include "filters.h" + +#include "binding/rapid_json_serializer.h" +#include "generic_adapters.h" +#include "out_stream.h" #include "testers.h" -#include "value_visitors.h" #include "value_helpers.h" +#include "value_visitors.h" #include #include +#include #include #include -#include using namespace std::string_literals; @@ -17,72 +21,74 @@ namespace jinja2 template struct FilterFactory { - static FilterPtr Create(FilterParams params) - { - return std::make_shared(std::move(params)); - } + static FilterPtr Create(FilterParams params) { return std::make_shared(std::move(params)); } - template - static ExpressionFilter::FilterFactoryFn MakeCreator(Args&& ... args) + template + static ExpressionFilter::FilterFactoryFn MakeCreator(Args&&... args) { - return [args...](FilterParams params) {return std::make_shared(std::move(params), args...);}; + return [args...](FilterParams params) { return std::make_shared(std::move(params), args...); }; } }; std::unordered_map s_filters = { - {"abs", FilterFactory::MakeCreator(filters::ValueConverter::AbsMode)}, - {"attr", &FilterFactory::Create}, - {"batch", FilterFactory::MakeCreator(filters::Slice::BatchMode)}, - {"camelize", FilterFactory::MakeCreator(filters::StringConverter::CamelMode)}, - {"capitalize", FilterFactory::MakeCreator(filters::StringConverter::CapitalMode)}, - {"default", &FilterFactory::Create}, - {"d", &FilterFactory::Create}, - {"dictsort", &FilterFactory::Create}, - {"escape", FilterFactory::MakeCreator(filters::StringConverter::EscapeHtmlMode)}, - {"escapecpp", FilterFactory::MakeCreator(filters::StringConverter::EscapeCppMode)}, - {"first", FilterFactory::MakeCreator(filters::SequenceAccessor::FirstItemMode)}, - {"float", FilterFactory::MakeCreator(filters::ValueConverter::ToFloatMode)}, - {"format", FilterFactory::MakeCreator(filters::StringFormat::PythonMode)}, - {"groupby", &FilterFactory::Create}, - {"int", FilterFactory::MakeCreator(filters::ValueConverter::ToIntMode)}, - {"join", &FilterFactory::Create}, - {"last", FilterFactory::MakeCreator(filters::SequenceAccessor::LastItemMode)}, - {"length", FilterFactory::MakeCreator(filters::SequenceAccessor::LengthMode)}, - {"list", FilterFactory::MakeCreator(filters::ValueConverter::ToListMode)}, - {"lower", FilterFactory::MakeCreator(filters::StringConverter::LowerMode)}, - {"map", &FilterFactory::Create}, - {"max", FilterFactory::MakeCreator(filters::SequenceAccessor::MaxItemMode)}, - {"min", FilterFactory::MakeCreator(filters::SequenceAccessor::MinItemMode)}, - {"pprint", &FilterFactory::Create}, - {"random", FilterFactory::MakeCreator(filters::SequenceAccessor::RandomMode)}, - {"reject", FilterFactory::MakeCreator(filters::Tester::RejectMode)}, - {"rejectattr", FilterFactory::MakeCreator(filters::Tester::RejectAttrMode)}, - {"replace", FilterFactory::MakeCreator(filters::StringConverter::ReplaceMode)}, - {"round", FilterFactory::MakeCreator(filters::ValueConverter::RoundMode)}, - {"reverse", FilterFactory::MakeCreator(filters::SequenceAccessor::ReverseMode)}, - {"select", FilterFactory::MakeCreator(filters::Tester::SelectMode)}, - {"selectattr", FilterFactory::MakeCreator(filters::Tester::SelectAttrMode)}, - {"slice", FilterFactory::MakeCreator(filters::Slice::SliceMode)}, - {"sort", &FilterFactory::Create}, - {"sum", FilterFactory::MakeCreator(filters::SequenceAccessor::SumItemsMode)}, - {"title", FilterFactory::MakeCreator(filters::StringConverter::TitleMode)}, - {"tojson", FilterFactory::MakeCreator(filters::Serialize::JsonMode)}, - {"toxml", FilterFactory::MakeCreator(filters::Serialize::XmlMode)}, - {"toyaml", FilterFactory::MakeCreator(filters::Serialize::YamlMode)}, - {"trim", FilterFactory::MakeCreator(filters::StringConverter::TrimMode)}, - {"truncate", FilterFactory::MakeCreator(filters::StringConverter::TruncateMode)}, - {"unique", FilterFactory::MakeCreator(filters::SequenceAccessor::UniqueItemsMode)}, - {"upper", FilterFactory::MakeCreator(filters::StringConverter::UpperMode)}, - {"urlencode", FilterFactory::MakeCreator(filters::StringConverter::UrlEncodeMode)}, - {"wordcount", FilterFactory::MakeCreator(filters::StringConverter::WordCountMode)}, - {"wordwrap", FilterFactory::MakeCreator(filters::StringConverter::WordWrapMode)}, - {"underscorize", FilterFactory::MakeCreator(filters::StringConverter::UnderscoreMode)},}; - -extern FilterPtr CreateFilter(std::string filterName, CallParams params) + { "abs", FilterFactory::MakeCreator(filters::ValueConverter::AbsMode) }, + { "applymacro", &FilterFactory::Create }, + { "attr", &FilterFactory::Create }, + { "batch", FilterFactory::MakeCreator(filters::Slice::BatchMode) }, + { "camelize", FilterFactory::MakeCreator(filters::StringConverter::CamelMode) }, + { "capitalize", FilterFactory::MakeCreator(filters::StringConverter::CapitalMode) }, + { "center", FilterFactory::MakeCreator(filters::StringConverter::CenterMode) }, + { "default", &FilterFactory::Create }, + { "d", &FilterFactory::Create }, + { "dictsort", &FilterFactory::Create }, + { "escape", FilterFactory::MakeCreator(filters::StringConverter::EscapeHtmlMode) }, + { "escapecpp", FilterFactory::MakeCreator(filters::StringConverter::EscapeCppMode) }, + { "first", FilterFactory::MakeCreator(filters::SequenceAccessor::FirstItemMode) }, + { "float", FilterFactory::MakeCreator(filters::ValueConverter::ToFloatMode) }, + { "format", FilterFactory::Create }, + { "groupby", &FilterFactory::Create }, + { "int", FilterFactory::MakeCreator(filters::ValueConverter::ToIntMode) }, + { "join", &FilterFactory::Create }, + { "last", FilterFactory::MakeCreator(filters::SequenceAccessor::LastItemMode) }, + { "length", FilterFactory::MakeCreator(filters::SequenceAccessor::LengthMode) }, + { "list", FilterFactory::MakeCreator(filters::ValueConverter::ToListMode) }, + { "lower", FilterFactory::MakeCreator(filters::StringConverter::LowerMode) }, + { "map", &FilterFactory::Create }, + { "max", FilterFactory::MakeCreator(filters::SequenceAccessor::MaxItemMode) }, + { "min", FilterFactory::MakeCreator(filters::SequenceAccessor::MinItemMode) }, + { "pprint", &FilterFactory::Create }, + { "random", FilterFactory::MakeCreator(filters::SequenceAccessor::RandomMode) }, + { "reject", FilterFactory::MakeCreator(filters::Tester::RejectMode) }, + { "rejectattr", FilterFactory::MakeCreator(filters::Tester::RejectAttrMode) }, + { "replace", FilterFactory::MakeCreator(filters::StringConverter::ReplaceMode) }, + { "round", FilterFactory::MakeCreator(filters::ValueConverter::RoundMode) }, + { "reverse", FilterFactory::MakeCreator(filters::SequenceAccessor::ReverseMode) }, + { "select", FilterFactory::MakeCreator(filters::Tester::SelectMode) }, + { "selectattr", FilterFactory::MakeCreator(filters::Tester::SelectAttrMode) }, + { "slice", FilterFactory::MakeCreator(filters::Slice::SliceMode) }, + { "sort", &FilterFactory::Create }, + { "striptags", FilterFactory::MakeCreator(filters::StringConverter::StriptagsMode) }, + { "sum", FilterFactory::MakeCreator(filters::SequenceAccessor::SumItemsMode) }, + { "title", FilterFactory::MakeCreator(filters::StringConverter::TitleMode) }, + { "tojson", FilterFactory::MakeCreator(filters::Serialize::JsonMode) }, + { "toxml", FilterFactory::MakeCreator(filters::Serialize::XmlMode) }, + { "toyaml", FilterFactory::MakeCreator(filters::Serialize::YamlMode) }, + { "trim", FilterFactory::MakeCreator(filters::StringConverter::TrimMode) }, + { "truncate", FilterFactory::MakeCreator(filters::StringConverter::TruncateMode) }, + { "unique", FilterFactory::MakeCreator(filters::SequenceAccessor::UniqueItemsMode) }, + { "upper", FilterFactory::MakeCreator(filters::StringConverter::UpperMode) }, + { "urlencode", FilterFactory::MakeCreator(filters::StringConverter::UrlEncodeMode) }, + { "wordcount", FilterFactory::MakeCreator(filters::StringConverter::WordCountMode) }, + { "wordwrap", FilterFactory::MakeCreator(filters::StringConverter::WordWrapMode) }, + { "underscorize", FilterFactory::MakeCreator(filters::StringConverter::UnderscoreMode) }, + { "xmlattr", &FilterFactory::Create } +}; + +extern FilterPtr CreateFilter(std::string filterName, CallParamsInfo params) { auto p = s_filters.find(filterName); if (p == s_filters.end()) - return FilterPtr(); + return std::make_shared(std::move(filterName), std::move(params)); return p->second(std::move(params)); } @@ -92,7 +98,7 @@ namespace filters Join::Join(FilterParams params) { - ParseParams({{"d", false, std::string()}, {"attribute"}}, params); + ParseParams({ { "d", false, std::string() }, { "attribute" } }, params); } InternalValue Join::Filter(const InternalValue& baseVal, RenderContext& context) @@ -123,7 +129,7 @@ InternalValue Join::Filter(const InternalValue& baseVal, RenderContext& context) Sort::Sort(FilterParams params) { - ParseParams({{"reverse", false, InternalValue(false)}, {"case_sensitive", false, InternalValue(false)}, {"attribute", false}}, params); + ParseParams({ { "reverse", false, InternalValue(false) }, { "case_sensitive", false, InternalValue(false) }, { "attribute", false } }, params); } InternalValue Sort::Filter(const InternalValue& baseVal, RenderContext& context) @@ -138,17 +144,15 @@ InternalValue Sort::Filter(const InternalValue& baseVal, RenderContext& context) return InternalValue(); InternalValueList values = origValues.ToValueList(); - BinaryExpression::Operation oper = - ConvertToBool(isReverseVal) ? BinaryExpression::LogicalGt : BinaryExpression::LogicalLt; - BinaryExpression::CompareType compType = - ConvertToBool(isCsVal) ? BinaryExpression::CaseSensitive : BinaryExpression::CaseInsensitive; + BinaryExpression::Operation oper = ConvertToBool(isReverseVal) ? BinaryExpression::LogicalGt : BinaryExpression::LogicalLt; + BinaryExpression::CompareType compType = ConvertToBool(isCsVal) ? BinaryExpression::CaseSensitive : BinaryExpression::CaseInsensitive; - std::sort(values.begin(), values.end(), [&attrName, oper, compType](auto& val1, auto& val2) { + std::sort(values.begin(), values.end(), [&attrName, oper, compType, &context](auto& val1, auto& val2) { InternalValue cmpRes; if (IsEmpty(attrName)) cmpRes = Apply2(val1, val2, oper, compType); else - cmpRes = Apply2(Subscript(val1, attrName), Subscript(val2, attrName), oper, compType); + cmpRes = Apply2(Subscript(val1, attrName, &context), Subscript(val2, attrName, &context), oper, compType); return ConvertToBool(cmpRes); }); @@ -158,18 +162,21 @@ InternalValue Sort::Filter(const InternalValue& baseVal, RenderContext& context) Attribute::Attribute(FilterParams params) { - ParseParams({{"name", true}}, params); + ParseParams({ { "name", true }, { "default", false } }, params); } InternalValue Attribute::Filter(const InternalValue& baseVal, RenderContext& context) { - InternalValue attrNameVal = GetArgumentValue("name", context); - return Subscript(baseVal, attrNameVal); + const auto attrNameVal = GetArgumentValue("name", context); + const auto result = Subscript(baseVal, attrNameVal, &context); + if (result.IsEmpty()) + return GetArgumentValue("default", context); + return result; } Default::Default(FilterParams params) { - ParseParams({{"default_value", false, InternalValue(""s)}, {"boolean", false, InternalValue(false)}}, params); + ParseParams({ { "default_value", false, InternalValue(""s) }, { "boolean", false, InternalValue(false) } }, params); } InternalValue Default::Filter(const InternalValue& baseVal, RenderContext& context) @@ -188,12 +195,12 @@ InternalValue Default::Filter(const InternalValue& baseVal, RenderContext& conte DictSort::DictSort(FilterParams params) { - ParseParams({{"case_sensitive", false}, {"by", false, "key"s}, {"reverse", false}}, params); + ParseParams({ { "case_sensitive", false }, { "by", false, "key"s }, { "reverse", false } }, params); } InternalValue DictSort::Filter(const InternalValue& baseVal, RenderContext& context) { - const MapAdapter* map = boost::get(&baseVal); + const MapAdapter* map = GetIf(&baseVal); if (map == nullptr) return InternalValue(); @@ -207,15 +214,11 @@ InternalValue DictSort::Filter(const InternalValue& baseVal, RenderContext& cont { if (ConvertToBool(isCsVal)) { - comparator = [](const KeyValuePair& left, const KeyValuePair& right) - { - return left.key < right.key; - }; + comparator = [](const KeyValuePair& left, const KeyValuePair& right) { return left.key < right.key; }; } else { - comparator = [](const KeyValuePair& left, const KeyValuePair& right) - { + comparator = [](const KeyValuePair& left, const KeyValuePair& right) { return boost::lexicographical_compare(left.key, right.key, boost::algorithm::is_iless()); }; } @@ -224,16 +227,16 @@ InternalValue DictSort::Filter(const InternalValue& baseVal, RenderContext& cont { if (ConvertToBool(isCsVal)) { - comparator = [](const KeyValuePair& left, const KeyValuePair& right) - { - return ConvertToBool(Apply2(left.value, right.value, BinaryExpression::LogicalLt, BinaryExpression::CaseSensitive)); + comparator = [](const KeyValuePair& left, const KeyValuePair& right) { + return ConvertToBool( + Apply2(left.value, right.value, BinaryExpression::LogicalLt, BinaryExpression::CaseSensitive)); }; } else { - comparator = [](const KeyValuePair& left, const KeyValuePair& right) - { - return ConvertToBool(Apply2(left.value, right.value, BinaryExpression::LogicalLt, BinaryExpression::CaseInsensitive)); + comparator = [](const KeyValuePair& left, const KeyValuePair& right) { + return ConvertToBool( + Apply2(left.value, right.value, BinaryExpression::LogicalLt, BinaryExpression::CaseInsensitive)); }; } } @@ -242,26 +245,32 @@ InternalValue DictSort::Filter(const InternalValue& baseVal, RenderContext& cont std::vector tempVector; tempVector.reserve(map->GetSize()); - for (int64_t idx = 0; idx < map->GetSize(); ++ idx) + for (auto& key : map->GetKeys()) { - auto val = map->GetValueByIndex(idx); - auto& kvVal = boost::get(val); - tempVector.push_back(std::move(kvVal)); + auto val = map->GetValueByName(key); + tempVector.push_back(KeyValuePair{ key, val }); } if (ConvertToBool(isReverseVal)) - std::sort(tempVector.begin(), tempVector.end(), [comparator](auto& l, auto& r) {return comparator(r, l);}); + std::sort(tempVector.begin(), tempVector.end(), [comparator](auto& l, auto& r) { return comparator(r, l); }); else - std::sort(tempVector.begin(), tempVector.end(), [comparator](auto& l, auto& r) {return comparator(l, r);}); + std::sort(tempVector.begin(), tempVector.end(), [comparator](auto& l, auto& r) { return comparator(l, r); }); - InternalValueList resultList(tempVector.begin(), tempVector.end()); + InternalValueList resultList; + for (auto& tmpVal : tempVector) + { + auto resultVal = InternalValue(std::move(tmpVal)); + if (baseVal.ShouldExtendLifetime()) + resultVal.SetParentData(baseVal); + resultList.push_back(std::move(resultVal)); + } return InternalValue(ListAdapter::CreateAdapter(std::move(resultList))); } GroupBy::GroupBy(FilterParams params) { - ParseParams({{"attribute", true}}, params); + ParseParams({ { "attribute", true } }, params); } InternalValue GroupBy::Filter(const InternalValue& baseVal, RenderContext& context) @@ -290,10 +299,10 @@ InternalValue GroupBy::Filter(const InternalValue& baseVal, RenderContext& conte for (auto& item : list) { - auto attr = Subscript(item, attrName); - auto p = std::find_if(groups.begin(), groups.end(), [&equalComparator, &attr](auto& i) {return equalComparator(i.grouper, attr);}); + auto attr = Subscript(item, attrName, &context); + auto p = std::find_if(groups.begin(), groups.end(), [&equalComparator, &attr](auto& i) { return equalComparator(i.grouper, attr); }); if (p == groups.end()) - groups.push_back(GroupInfo{attr, {item}}); + groups.push_back(GroupInfo{ attr, { item } }); else p->items.push_back(item); } @@ -301,168 +310,114 @@ InternalValue GroupBy::Filter(const InternalValue& baseVal, RenderContext& conte InternalValueList result; for (auto& g : groups) { - InternalValueMap groupItem{{"grouper", std::move(g.grouper)}, {"list", ListAdapter::CreateAdapter(std::move(g.items))}}; - result.push_back(MapAdapter::CreateAdapter(std::move(groupItem))); + InternalValueMap groupItem{ { "grouper", std::move(g.grouper) }, { "list", ListAdapter::CreateAdapter(std::move(g.items)) } }; + result.push_back(CreateMapAdapter(std::move(groupItem))); } return ListAdapter::CreateAdapter(std::move(result)); } -Map::Map(FilterParams params) +ApplyMacro::ApplyMacro(FilterParams params) { - FilterParams newParams; - - if (params.kwParams.size() == 1 && params.posParams.empty() && params.kwParams.count("attribute") == 1) - { - newParams.kwParams["name"] = params.kwParams["attribute"]; - newParams.kwParams["filter"] = std::make_shared("attr"s); - } - else - { - newParams = std::move(params); - } - - ParseParams({{"filter", true}}, newParams); + ParseParams({ { "macro", true } }, params); m_mappingParams.kwParams = m_args.extraKwArgs; m_mappingParams.posParams = m_args.extraPosArgs; } -InternalValue Map::Filter(const InternalValue& baseVal, RenderContext& context) +InternalValue ApplyMacro::Filter(const InternalValue& baseVal, RenderContext& context) { - InternalValue filterName = GetArgumentValue("filter", context); - if (IsEmpty(filterName)) + InternalValue macroName = GetArgumentValue("macro", context); + if (IsEmpty(macroName)) return InternalValue(); - auto filter = CreateFilter(AsString(filterName), m_mappingParams); - if (!filter) + bool macroFound = false; + auto macroValPtr = context.FindValue(AsString(macroName), macroFound); + if (!macroFound) return InternalValue(); - bool isConverted = false; - auto list = ConvertToList(baseVal, isConverted); - if (!isConverted) + const Callable* callable = GetIf(¯oValPtr->second); + if (callable == nullptr || callable->GetKind() != Callable::Macro) return InternalValue(); - InternalValueList resultList; - resultList.reserve(list.GetSize()); - std::transform(list.begin(), list.end(), std::back_inserter(resultList), [filter, &context](auto& val) {return filter->Filter(val, context);}); + CallParams tmpCallParams = helpers::EvaluateCallParams(m_mappingParams, context); + CallParams callParams; + callParams.kwParams = std::move(tmpCallParams.kwParams); + callParams.posParams.reserve(tmpCallParams.posParams.size() + 1); + callParams.posParams.push_back(baseVal); + for (auto& p : tmpCallParams.posParams) + callParams.posParams.push_back(std::move(p)); - return ListAdapter::CreateAdapter(std::move(resultList)); -} - -struct PrettyPrinter : visitors::BaseVisitor -{ - using BaseVisitor::operator(); - - PrettyPrinter(const RenderContext* context) - : m_context(context) - {} - - InternalValue operator()(const ListAdapter& list) const - { - std::ostringstream os; - - os << "["; - bool isFirst = true; - - for (auto& v : list) - { - if (isFirst) - isFirst = false; - else - os << ", "; - os << AsString(Apply(v, m_context)); - } - os << "]"; - - return InternalValue(os.str()); - } - - InternalValue operator()(const MapAdapter& map) const - { - std::ostringstream os; - os << "{"; - - const auto& keys = map.GetKeys(); - - bool isFirst = true; - for (auto& k : keys) - { - if (isFirst) - isFirst = false; - else - os << ", "; - - os << "'" << k << "': "; - os << AsString(Apply(map.GetValueByName(k), m_context)); - } - - os << "}"; - - return InternalValue(os.str()); - } - - InternalValue operator() (const KeyValuePair& kwPair) const + InternalValue result; + if (callable->GetType() == Callable::Type::Expression) { - std::ostringstream os; - - os << "'" << kwPair.key << "': "; - os << AsString(Apply(kwPair.value, m_context)); - - return InternalValue(os.str()); + result = callable->GetExpressionCallable()(callParams, context); } - - InternalValue operator()(const std::string& str) const + else { - return "'"s + str + "'"s; + TargetString resultStr; + auto stream = context.GetRendererCallback()->GetStreamOnString(resultStr); + callable->GetStatementCallable()(callParams, stream, context); + result = std::move(resultStr); } - InternalValue operator()(const std::wstring& str) const - { - return "''"s; - } + return result; +} - InternalValue operator()(bool val) const - { - return val ? "true"s : "false"s; - } +Map::Map(FilterParams params) +{ + ParseParams({ { "filter", true } }, MakeParams(std::move(params))); + m_mappingParams.kwParams = m_args.extraKwArgs; + m_mappingParams.posParams = m_args.extraPosArgs; +} - InternalValue operator()(EmptyValue val) const +FilterParams Map::MakeParams(FilterParams params) +{ + if (!params.posParams.empty() || params.kwParams.empty() || params.kwParams.size() > 2) { - return "none"s; + return params; } - InternalValue operator()(double val) const + const auto attributeIt = params.kwParams.find("attribute"); + if (attributeIt == params.kwParams.cend()) { - std::ostringstream os; - os << val; - return InternalValue(os.str()); + return params; } - InternalValue operator()(int64_t val) const - { - std::ostringstream os; - os << val; - return InternalValue(os.str()); - } + FilterParams result; + result.kwParams["name"] = attributeIt->second; + result.kwParams["filter"] = std::make_shared("attr"s); - const RenderContext* m_context; -}; + const auto defaultIt = params.kwParams.find("default"); + if (defaultIt != params.kwParams.cend()) + result.kwParams["default"] = defaultIt->second; -PrettyPrint::PrettyPrint(FilterParams params) -{ + return result; } -InternalValue PrettyPrint::Filter(const InternalValue& baseVal, RenderContext& context) +InternalValue Map::Filter(const InternalValue& baseVal, RenderContext& context) { - return Apply(baseVal, &context); -} + InternalValue filterName = GetArgumentValue("filter", context); + if (IsEmpty(filterName)) + return InternalValue(); -Random::Random(FilterParams params) -{ + auto filter = CreateFilter(AsString(filterName), m_mappingParams); + if (!filter) + return InternalValue(); + + bool isConverted = false; + auto list = ConvertToList(baseVal, isConverted); + if (!isConverted) + return InternalValue(); + InternalValueList resultList; + resultList.reserve(list.GetSize().value_or(0)); + std::transform(list.begin(), list.end(), std::back_inserter(resultList), [filter, &context](auto& val) { return filter->Filter(val, context); }); + + return ListAdapter::CreateAdapter(std::move(resultList)); } +Random::Random(FilterParams params) {} -InternalValue Random::Filter(const InternalValue& baseVal, RenderContext& context) +InternalValue Random::Filter(const InternalValue&, RenderContext&) { return InternalValue(); } @@ -472,27 +427,25 @@ SequenceAccessor::SequenceAccessor(FilterParams params, SequenceAccessor::Mode m { switch (mode) { - case FirstItemMode: - break; - case LastItemMode: - break; - case LengthMode: - break; - case MaxItemMode: - ParseParams({{"case_sensitive", false, InternalValue(false)}, {"attribute", false}}, params); - break; - case MinItemMode: - ParseParams({{"case_sensitive", false, InternalValue(false)}, {"attribute", false}}, params); - break; - case RandomMode: - case ReverseMode: - break; - case SumItemsMode: - ParseParams({{"attribute", false}, {"start", false}}, params); - break; - case UniqueItemsMode: - ParseParams({{"attribute", false}}, params); - break; + case FirstItemMode: + break; + case LastItemMode: + break; + case LengthMode: + break; + case MaxItemMode: + case MinItemMode: + ParseParams({ { "case_sensitive", false, InternalValue(false) }, { "attribute", false } }, params); + break; + case RandomMode: + case ReverseMode: + break; + case SumItemsMode: + ParseParams({ { "attribute", false }, { "start", false } }, params); + break; + case UniqueItemsMode: + ParseParams({ { "attribute", false } }, params); + break; } } @@ -505,174 +458,296 @@ InternalValue SequenceAccessor::Filter(const InternalValue& baseVal, RenderConte if (!isConverted) return result; + + auto ProtectedValue = [&baseVal](InternalValue value) { + if (baseVal.ShouldExtendLifetime()) + value.SetParentData(baseVal); + return value; + }; InternalValue attrName = GetArgumentValue("attribute", context); InternalValue isCsVal = GetArgumentValue("case_sensitive", context, InternalValue(false)); - BinaryExpression::CompareType compType = - ConvertToBool(isCsVal) ? BinaryExpression::CaseSensitive : BinaryExpression::CaseInsensitive; + BinaryExpression::CompareType compType = ConvertToBool(isCsVal) ? BinaryExpression::CaseSensitive : BinaryExpression::CaseInsensitive; - auto lessComparator = [&attrName, &compType](auto& val1, auto& val2) { + auto lessComparator = [&attrName, &compType, &context](auto& val1, auto& val2) { InternalValue cmpRes; if (IsEmpty(attrName)) cmpRes = Apply2(val1, val2, BinaryExpression::LogicalLt, compType); else - cmpRes = Apply2(Subscript(val1, attrName), Subscript(val2, attrName), BinaryExpression::LogicalLt, compType); + cmpRes = Apply2( + Subscript(val1, attrName, &context), Subscript(val2, attrName, &context), BinaryExpression::LogicalLt, compType); return ConvertToBool(cmpRes); }; - auto equalComparator = [&attrName, &compType](auto& val1, auto& val2) { - InternalValue cmpRes; - - if (IsEmpty(attrName)) - cmpRes = Apply2(val1, val2, BinaryExpression::LogicalEq, compType); - else - cmpRes = Apply2(Subscript(val1, attrName), Subscript(val2, attrName), BinaryExpression::LogicalEq, compType); - - return ConvertToBool(cmpRes); - }; + const auto& listSize = list.GetSize(); switch (m_mode) { - case FirstItemMode: - result = list.GetSize() == 0 ? InternalValue() : list.GetValueByIndex(0); - break; - case LastItemMode: - result = list.GetSize() == 0 ? InternalValue() : list.GetValueByIndex(list.GetSize() - 1); - break; - case LengthMode: - result = static_cast(list.GetSize()); - break; - case RandomMode: - { - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_int_distribution<> dis(0, static_cast(list.GetSize()) - 1); - result = list.GetValueByIndex(dis(gen)); - break; - } - case MaxItemMode: - { - auto b = list.begin(); - auto e = list.end(); - auto p = std::max_element(b, e, lessComparator); - result = p != e ? *p : InternalValue(); - break; - } - case MinItemMode: - { - auto b = list.begin(); - auto e = list.end(); - auto p = std::min_element(b, e, lessComparator); - result = p != e ? *p : InternalValue(); - break; - } - case ReverseMode: - { - InternalValueList resultList(list.GetSize()); - for (int n = 0; n < list.GetSize(); ++ n) - resultList[list.GetSize() - n - 1] = list.GetValueByIndex(n); - - result = ListAdapter::CreateAdapter(std::move(resultList)); - break; - } - case SumItemsMode: - { - ListAdapter l1; - ListAdapter* actualList; - if (IsEmpty(attrName)) + case FirstItemMode: + if (listSize) + result = ProtectedValue( list.GetValueByIndex(0) ); + else + { + auto it = list.begin(); + if (it != list.end()) + result = ProtectedValue(*it); + } + break; + case LastItemMode: + if (listSize) + result = ProtectedValue(list.GetValueByIndex(listSize.value() - 1)); + else + { + auto it = list.begin(); + auto end = list.end(); + for (; it != end; ++it) + result = ProtectedValue(*it); + } + break; + case LengthMode: + if (listSize) + result = static_cast(listSize.value()); + else + result = static_cast(std::distance(list.begin(), list.end())); + break; + case RandomMode: { - actualList = &list; + std::random_device rd; + std::mt19937 gen(rd()); + if (listSize) + { + std::uniform_int_distribution<> dis(0, static_cast(listSize.value()) - 1); + result = ProtectedValue(list.GetValueByIndex(dis(gen))); + } + else + { + auto it = list.begin(); + auto end = list.end(); + size_t count = 0; + for (; it != end; ++it, ++count) + { + bool doCopy = count == 0 || std::uniform_int_distribution(0, count)(gen) == 0; + if (doCopy) + result = ProtectedValue(*it); + } + } + break; } - else + case MaxItemMode: { - l1 = list.ToSubscriptedList(attrName, true); - actualList = &l1; + auto b = list.begin(); + auto e = list.end(); + auto p = std::max_element(list.begin(), list.end(), lessComparator); + result = p != e ? ProtectedValue(*p) : InternalValue(); + break; } - InternalValue start = GetArgumentValue("start", context); - InternalValue resultVal = std::accumulate(actualList->begin(), actualList->end(), start, [](const InternalValue& cur, const InternalValue& val) { - if (IsEmpty(cur)) - return val; - - return Apply2(cur, val, BinaryExpression::Plus); - }); + case MinItemMode: + { + auto b = list.begin(); + auto e = list.end(); + auto p = std::min_element(b, e, lessComparator); + result = p != e ? ProtectedValue(*p) : InternalValue(); + break; + } + case ReverseMode: + { + if (listSize) + { + auto size = listSize.value(); + InternalValueList resultList(size); + for (std::size_t n = 0; n < size; ++n) + resultList[size - n - 1] = ProtectedValue( list.GetValueByIndex(n) ); + result = ListAdapter::CreateAdapter(std::move(resultList)); + } + else + { + InternalValueList resultList; + auto it = list.begin(); + auto end = list.end(); + for (; it != end; ++it) + resultList.push_back( ProtectedValue(*it) ); - result = std::move(resultVal); - break; - } - case UniqueItemsMode: - { - InternalValueList resultList; + std::reverse(resultList.begin(), resultList.end()); + result = ListAdapter::CreateAdapter(std::move(resultList)); + } - struct Item + break; + } + case SumItemsMode: { - InternalValue val; - int64_t idx; - }; - std::vector items; + ListAdapter l1; + ListAdapter* actualList; + if (IsEmpty(attrName)) + { + actualList = &list; + } + else + { + l1 = list.ToSubscriptedList(attrName, true); + actualList = &l1; + } + InternalValue start = GetArgumentValue("start", context); + InternalValue resultVal = std::accumulate(actualList->begin(), actualList->end(), start, [](const InternalValue& cur, const InternalValue& val) { + if (IsEmpty(cur)) + return val; + + return Apply2(cur, val, BinaryExpression::Plus); + }); + + result = std::move(resultVal); + break; + } + case UniqueItemsMode: + { + InternalValueList resultList; - int idx = 0; - for (auto& v : list) - items.push_back(std::move(Item{IsEmpty(attrName) ? v : Subscript(v, attrName), idx ++})); + struct Item + { + InternalValue val; + int64_t idx; + }; + std::vector items; - std::sort(items.begin(), items.end(), [&compType](auto& i1, auto& i2) { - auto cmpRes = Apply2(i1.val, i2.val, BinaryExpression::LogicalLt, compType); + int idx = 0; + for (auto& v : list) + items.push_back(Item{ IsEmpty(attrName) ? v : Subscript(v, attrName, &context), idx++ }); - return ConvertToBool(cmpRes); - }); + std::stable_sort(items.begin(), items.end(), [&compType](auto& i1, auto& i2) { + auto cmpRes = Apply2(i1.val, i2.val, BinaryExpression::LogicalLt, compType); - auto end = std::unique(items.begin(), items.end(), [&compType](auto& i1, auto& i2) { - auto cmpRes = Apply2(i1.val, i2.val, BinaryExpression::LogicalEq, compType); + return ConvertToBool(cmpRes); + }); - return ConvertToBool(cmpRes); - }); - items.erase(end, items.end()); + auto end = std::unique(items.begin(), items.end(), [&compType](auto& i1, auto& i2) { + auto cmpRes = Apply2(i1.val, i2.val, BinaryExpression::LogicalEq, compType); - std::sort(items.begin(), items.end(), [&compType](auto& i1, auto& i2) { - return i1.idx < i2.idx; - }); + return ConvertToBool(cmpRes); + }); + items.erase(end, items.end()); - for (auto& i : items) - resultList.push_back(list.GetValueByIndex(i.idx)); + std::stable_sort(items.begin(), items.end(), [](auto& i1, auto& i2) { return i1.idx < i2.idx; }); - result = ListAdapter::CreateAdapter(std::move(resultList)); - break; - } + for (auto& i : items) + resultList.push_back( ProtectedValue( list.GetValueByIndex(i.idx) )); + + result = ListAdapter::CreateAdapter(std::move(resultList)); + break; + } } return result; } - -Serialize::Serialize(FilterParams params, Serialize::Mode mode) +Slice::Slice(FilterParams params, Slice::Mode mode) + : m_mode{ mode } { - + if (m_mode == BatchMode) + ParseParams({ { "linecount"s, true }, { "fill_with"s, false } }, params); + else + ParseParams({ { "slices"s, true }, { "fill_with"s, false } }, params); } -InternalValue Serialize::Filter(const InternalValue& baseVal, RenderContext& context) +InternalValue Slice::Filter(const InternalValue& baseVal, RenderContext& context) { - return InternalValue(); -} + if (m_mode == BatchMode) + return Batch(baseVal, context); -Slice::Slice(FilterParams params, Slice::Mode mode) -{ + InternalValue result; -} + bool isConverted = false; + ListAdapter list = ConvertToList(baseVal, isConverted); -InternalValue Slice::Filter(const InternalValue& baseVal, RenderContext& context) -{ - return InternalValue(); + if (!isConverted) + return result; + + auto ProtectedValue = [&baseVal](InternalValue value) { + if (baseVal.ShouldExtendLifetime()) + value.SetParentData(baseVal); + return value; + }; + + InternalValue sliceLengthValue = GetArgumentValue("slices", context); + int64_t sliceLength = ConvertToInt(sliceLengthValue); + InternalValue fillWith = GetArgumentValue("fill_with", context); + + InternalValueList resultList; + InternalValueList sublist; + int sublistItemIndex = 0; + for (auto& item : list) + { + if (sublistItemIndex == 0) + sublist.clear(); + if (sublistItemIndex == sliceLength) + { + resultList.push_back(ListAdapter::CreateAdapter(std::move(sublist))); + sublist.clear(); + sublistItemIndex %= sliceLength; + } + sublist.push_back( ProtectedValue(item) ); + ++sublistItemIndex; + } + if (!IsEmpty(fillWith)) + { + while (sublistItemIndex++ < sliceLength) + sublist.push_back(fillWith); + } + if (sublistItemIndex > 0) + resultList.push_back(ListAdapter::CreateAdapter(std::move(sublist))); + + return InternalValue(ListAdapter::CreateAdapter(std::move(resultList))); } -StringFormat::StringFormat(FilterParams params, StringFormat::Mode mode) +InternalValue Slice::Batch(const InternalValue& baseVal, RenderContext& context) { + auto linecount_value = ConvertToInt(GetArgumentValue("linecount", context)); + InternalValue fillWith = GetArgumentValue("fill_with", context); + + if (linecount_value <= 0) + return InternalValue(); + auto linecount = static_cast(linecount_value); + bool isConverted = false; + auto list = ConvertToList(baseVal, isConverted); + if (!isConverted) + return InternalValue(); + + auto elementsCount = list.GetSize().value_or(0); + if (!elementsCount) + return InternalValue(); + + InternalValueList resultList; + resultList.reserve(linecount); + + auto ProtectedValue = [&baseVal](InternalValue value) { + if (baseVal.ShouldExtendLifetime()) + value.SetParentData(baseVal); + return value; + }; + + const auto remainder = elementsCount % linecount; + const auto columns = elementsCount / linecount + (remainder > 0 ? 1 : 0); + for (std::size_t line = 0, idx = 0; line < linecount; ++line) + { + const auto elems = columns - (remainder && line >= remainder ? 1 : 0); + InternalValueList row; + row.reserve(columns); + std::fill_n(std::back_inserter(row), columns, fillWith); + + for (std::size_t column = 0; column < elems; ++column) + row[column] = ProtectedValue( list.GetValueByIndex(idx++) ); + + resultList.push_back(ListAdapter::CreateAdapter(std::move(row))); + } + return ListAdapter::CreateAdapter(std::move(resultList)); } -InternalValue StringFormat::Filter(const InternalValue& baseVal, RenderContext& context) +StringFormat::StringFormat(FilterParams params) { - return InternalValue(); + ParseParams({}, params); + m_params.kwParams = std::move(m_args.extraKwArgs); + m_params.posParams = std::move(m_args.extraPosArgs); } Tester::Tester(FilterParams params, Tester::Mode mode) @@ -687,9 +762,9 @@ Tester::Tester(FilterParams params, Tester::Mode mode) } if (mode == RejectMode || mode == SelectMode) - ParseParams({{"tester", false}}, params); + ParseParams({ { "tester", false } }, params); else - ParseParams({{"attribute", true}, {"tester", false}}, params); + ParseParams({ { "attribute", true }, { "tester", false } }, params); m_testingParams.kwParams = std::move(m_args.extraKwArgs); m_testingParams.posParams = std::move(m_args.extraPosArgs); @@ -716,13 +791,12 @@ InternalValue Tester::Filter(const InternalValue& baseVal, RenderContext& contex return InternalValue(); InternalValueList resultList; - resultList.reserve(list.GetSize()); - std::copy_if(list.begin(), list.end(), std::back_inserter(resultList), [this, tester, attrName, &context](auto& val) - { + resultList.reserve(list.GetSize().value_or(0)); + std::copy_if(list.begin(), list.end(), std::back_inserter(resultList), [this, tester, attrName, &context](auto& val) { InternalValue attrVal; bool isAttr = !IsEmpty(attrName); if (isAttr) - attrVal = Subscript(val, attrName); + attrVal = Subscript(val, attrName, &context); bool result = false; if (tester) @@ -741,19 +815,18 @@ ValueConverter::ValueConverter(FilterParams params, ValueConverter::Mode mode) { switch (mode) { - case ToFloatMode: - ParseParams({{"default", false}}, params); - break; - case ToIntMode: - ParseParams({{"default", false}, {"base", false, static_cast(10)}}, params); - break; - case ToListMode: - case AbsMode: - break; - case RoundMode: - ParseParams({{"precision", false}, {"method", false, "common"s}}, params); - break; - + case ToFloatMode: + ParseParams({ { "default"s, false } }, params); + break; + case ToIntMode: + ParseParams({ { "default"s, false }, { "base"s, false, static_cast(10) } }, params); + break; + case ToListMode: + case AbsMode: + break; + case RoundMode: + ParseParams({ { "precision"s, false }, { "method"s, false, "common"s } }, params); + break; } } @@ -780,18 +853,18 @@ struct ValueConverterImpl : visitors::BaseVisitor<> InternalValue result; switch (m_params.mode) { - case ValueConverter::ToFloatMode: - result = InternalValue(static_cast(val)); - break; - case ValueConverter::AbsMode: - result = InternalValue(static_cast(abs(val))); - break; - case ValueConverter::ToIntMode: - case ValueConverter::RoundMode: - result = val; - break; - default: - break; + case ValueConverter::ToFloatMode: + result = InternalValue(static_cast(val)); + break; + case ValueConverter::AbsMode: + result = InternalValue(static_cast(std::abs(val))); + break; + case ValueConverter::ToIntMode: + case ValueConverter::RoundMode: + result = InternalValue(static_cast(val)); + break; + default: + break; } return result; @@ -802,129 +875,142 @@ struct ValueConverterImpl : visitors::BaseVisitor<> InternalValue result; switch (m_params.mode) { - case ValueConverter::ToFloatMode: - result = val; - break; - case ValueConverter::ToIntMode: - result = static_cast(val); - break; - case ValueConverter::AbsMode: - result = InternalValue(fabs(val)); - break; - case ValueConverter::RoundMode: - { - auto method = AsString(m_params.roundMethod); - auto prec = GetAs(m_params.prec); - double pow10 = std::pow(10, static_cast(prec)); - val *= pow10; - if (method == "ceil") - val = val < 0 ? std::floor(val) : std::ceil(val); - else if (method == "floor") - val = val > 0 ? std::floor(val) : std::ceil(val); - else if (method == "common") - val = std::round(val); - result = InternalValue(val / pow10); - break; - } - default: - break; + case ValueConverter::ToFloatMode: + result = static_cast(val); + break; + case ValueConverter::ToIntMode: + result = static_cast(val); + break; + case ValueConverter::AbsMode: + result = InternalValue(fabs(val)); + break; + case ValueConverter::RoundMode: + { + auto method = AsString(m_params.roundMethod); + auto prec = GetAs(m_params.prec); + double pow10 = std::pow(10, static_cast(prec)); + val *= pow10; + if (method == "ceil") + val = val < 0 ? std::floor(val) : std::ceil(val); + else if (method == "floor") + val = val > 0 ? std::floor(val) : std::ceil(val); + else if (method == "common") + val = std::round(val); + result = InternalValue(val / pow10); + break; + } + default: + break; } return result; } - template - struct StringAdapter : public IListAccessor + static double ConvertToDouble(const char* buff, bool& isConverted) { - using string = std::basic_string; - StringAdapter(const string* str) - : m_str(str) - { - } - - size_t GetSize() const override {return m_str->size();} - InternalValue GetValueByIndex(int64_t idx) const override {return m_str->substr(static_cast(idx), 1);} - - const string* m_str; - }; + char* endBuff = nullptr; + double dblVal = strtod(buff, &endBuff); + isConverted = *endBuff == 0; + return dblVal; + } - struct Map2ListAdapter : public IListAccessor + static double ConvertToDouble(const wchar_t* buff, bool& isConverted) { - Map2ListAdapter(const MapAdapter* map) - : m_map(map) - { - } + wchar_t* endBuff = nullptr; + double dblVal = wcstod(buff, &endBuff); + isConverted = *endBuff == 0; + return dblVal; + } - size_t GetSize() const override {return m_map->GetSize();} - InternalValue GetValueByIndex(int64_t idx) const override {return m_map->GetValueByIndex(idx);} + static long long ConvertToInt(const char* buff, int base, bool& isConverted) + { + char* endBuff = nullptr; + long long intVal = strtoll(buff, &endBuff, base); + isConverted = *endBuff == 0; + return intVal; + } - const MapAdapter* m_map; - }; + static long long ConvertToInt(const wchar_t* buff, int base, bool& isConverted) + { + wchar_t* endBuff = nullptr; + long long intVal = wcstoll(buff, &endBuff, base); + isConverted = *endBuff == 0; + return intVal; + } - InternalValue operator()(const std::string& val) const + template + InternalValue operator()(const std::basic_string& val) const { InternalValue result; switch (m_params.mode) { - case ValueConverter::ToFloatMode: - { - char* endBuff = nullptr; - double dblVal = strtod(val.c_str(), &endBuff); - if (*endBuff != 0) - result = m_params.defValule; - else - result = dblVal; - break; - } - case ValueConverter::ToIntMode: - { - char* endBuff = nullptr; - int base = static_cast(GetAs(m_params.base)); - int64_t dblVal = strtoll(val.c_str(), &endBuff, base); - if (*endBuff != 0) - result = m_params.defValule; - else - result = dblVal; - break; - } - case ValueConverter::ToListMode: - result = ListAdapter([adapter = StringAdapter(&val)]() {return &adapter;}); - default: - break; + case ValueConverter::ToFloatMode: + { + bool converted = false; + double dblVal = ConvertToDouble(val.c_str(), converted); + + if (!converted) + result = m_params.defValule; + else + result = dblVal; + break; + } + case ValueConverter::ToIntMode: + { + int base = static_cast(GetAs(m_params.base)); + bool converted = false; + long long intVal = ConvertToInt(val.c_str(), base, converted); + + if (!converted) + result = m_params.defValule; + else + result = static_cast(intVal); + break; + } + case ValueConverter::ToListMode: + result = ListAdapter::CreateAdapter(val.size(), [str = val](size_t idx) { return InternalValue(TargetString(str.substr(idx, 1))); }); + default: + break; } return result; } - InternalValue operator()(const std::wstring& val) const + template + InternalValue operator()(const nonstd::basic_string_view& val) const { InternalValue result; switch (m_params.mode) { - case ValueConverter::ToFloatMode: - { - wchar_t* endBuff = nullptr; - double dblVal = wcstod(val.c_str(), &endBuff); - if (*endBuff != 0) - result = m_params.defValule; - else - result = dblVal; - break; - } - case ValueConverter::ToIntMode: - { - wchar_t* endBuff = nullptr; - int64_t dblVal = wcstoll(val.c_str(), &endBuff, static_cast(GetAs(m_params.base))); - if (*endBuff != 0) - result = m_params.defValule; - else - result = dblVal; - break; - } - case ValueConverter::ToListMode: - result = ListAdapter([adapter = StringAdapter(&val)]() {return &adapter;}); - default: - break; + case ValueConverter::ToFloatMode: + { + bool converted = false; + std::basic_string str(val.begin(), val.end()); + double dblVal = ConvertToDouble(str.c_str(), converted); + + if (!converted) + result = m_params.defValule; + else + result = static_cast(dblVal); + break; + } + case ValueConverter::ToIntMode: + { + int base = static_cast(GetAs(m_params.base)); + bool converted = false; + std::basic_string str(val.begin(), val.end()); + long long intVal = ConvertToInt(str.c_str(), base, converted); + + if (!converted) + result = m_params.defValule; + else + result = static_cast(intVal); + break; + } + case ValueConverter::ToListMode: + result = ListAdapter::CreateAdapter(val.size(), [str = val](size_t idx) { return InternalValue(str.substr(idx, 1)); }); + default: + break; } return result; @@ -940,10 +1026,12 @@ struct ValueConverterImpl : visitors::BaseVisitor<> InternalValue operator()(const MapAdapter& val) const { - if (m_params.mode == ValueConverter::ToListMode) - return ListAdapter([adapter = Map2ListAdapter(&val)]() {return &adapter;}); + if (m_params.mode != ValueConverter::ToListMode) + return InternalValue(); - return InternalValue(); + auto keys = val.GetKeys(); + auto num_keys = keys.size(); + return ListAdapter::CreateAdapter(num_keys, [values = std::move(keys)](size_t idx) { return InternalValue(values[idx]); }); } template @@ -953,7 +1041,7 @@ struct ValueConverterImpl : visitors::BaseVisitor<> params.mode = ValueConverter::ToIntMode; params.base = static_cast(10); InternalValue intVal = Apply(val, params); - T* result = boost::get(&intVal); + const T* result = GetIf(&intVal); if (result == nullptr) return defValue; @@ -971,7 +1059,46 @@ InternalValue ValueConverter::Filter(const InternalValue& baseVal, RenderContext params.base = GetArgumentValue("base", context); params.prec = GetArgumentValue("precision", context); params.roundMethod = GetArgumentValue("method", context); - return Apply(baseVal, params); + auto result = Apply(baseVal, params); + if (baseVal.ShouldExtendLifetime()) + result.SetParentData(baseVal); + + return result; +} + +UserDefinedFilter::UserDefinedFilter(std::string filterName, FilterParams params) + : m_filterName(std::move(filterName)) +{ + ParseParams({ { "*args" }, { "**kwargs" } }, params); + m_callParams.kwParams = m_args.extraKwArgs; + m_callParams.posParams = m_args.extraPosArgs; +} + +InternalValue UserDefinedFilter::Filter(const InternalValue& baseVal, RenderContext& context) +{ + bool filterFound = false; + auto filterValPtr = context.FindValue(m_filterName, filterFound); + if (!filterFound) + throw std::runtime_error("Can't find filter '" + m_filterName + "'"); + + const Callable* callable = GetIf(&filterValPtr->second); + if (callable == nullptr || callable->GetKind() != Callable::UserCallable) + return InternalValue(); + + CallParams tmpCallParams = helpers::EvaluateCallParams(m_callParams, context); + CallParams callParams; + callParams.kwParams = std::move(tmpCallParams.kwParams); + callParams.posParams.reserve(tmpCallParams.posParams.size() + 1); + callParams.posParams.push_back(baseVal); + for (auto& p : tmpCallParams.posParams) + callParams.posParams.push_back(std::move(p)); + + InternalValue result; + if (callable->GetType() != Callable::Type::Expression) + return InternalValue(); + + return callable->GetExpressionCallable()(callParams, context); } -} // filters -} // jinja2 + +} // namespace filters +} // namespace jinja2 diff --git a/src/filters.h b/src/filters.h index 23d9ac24..1fdfc465 100644 --- a/src/filters.h +++ b/src/filters.h @@ -1,5 +1,5 @@ -#ifndef FILTERS_H -#define FILTERS_H +#ifndef JINJA2CPP_SRC_FILTERS_H +#define JINJA2CPP_SRC_FILTERS_H #include "expression_evaluator.h" #include "function_base.h" @@ -12,9 +12,9 @@ namespace jinja2 { using FilterPtr = std::shared_ptr; -using FilterParams = CallParams; +using FilterParams = CallParamsInfo; -extern FilterPtr CreateFilter(std::string filterName, CallParams params); +extern FilterPtr CreateFilter(std::string filterName, CallParamsInfo params); namespace filters { @@ -22,12 +22,43 @@ class FilterBase : public FunctionBase, public ExpressionFilter::IExpressionFilt { }; +class ApplyMacro : public FilterBase +{ +public: + ApplyMacro(FilterParams params); + + InternalValue Filter(const InternalValue& baseVal, RenderContext& context) override; + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + if (m_args != value->m_args) + return false; + if (m_mappingParams != value->m_mappingParams) + return false; + return true; + } +private: + FilterParams m_mappingParams; +}; + class Attribute : public FilterBase { public: Attribute(FilterParams params); - InternalValue Filter(const InternalValue& baseVal, RenderContext& context); + InternalValue Filter(const InternalValue& baseVal, RenderContext& context) override; + + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + if (m_args != value->m_args) + return false; + return true; + } }; class Default : public FilterBase @@ -35,7 +66,16 @@ class Default : public FilterBase public: Default(FilterParams params); - InternalValue Filter(const InternalValue& baseVal, RenderContext& context); + InternalValue Filter(const InternalValue& baseVal, RenderContext& context) override; + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + if (m_args != value->m_args) + return false; + return true; + } }; class DictSort : public FilterBase @@ -43,7 +83,16 @@ class DictSort : public FilterBase public: DictSort(FilterParams params); - InternalValue Filter(const InternalValue& baseVal, RenderContext& context); + InternalValue Filter(const InternalValue& baseVal, RenderContext& context) override; + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + if (m_args != value->m_args) + return false; + return true; + } }; class GroupBy : public FilterBase @@ -51,7 +100,16 @@ class GroupBy : public FilterBase public: GroupBy(FilterParams params); - InternalValue Filter(const InternalValue& baseVal, RenderContext& context); + InternalValue Filter(const InternalValue& baseVal, RenderContext& context) override; + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + if (m_args != value->m_args) + return false; + return true; + } }; class Join : public FilterBase @@ -59,7 +117,16 @@ class Join : public FilterBase public: Join(FilterParams params); - InternalValue Filter(const InternalValue& baseVal, RenderContext& context); + InternalValue Filter(const InternalValue& baseVal, RenderContext& context) override; + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + if (m_args != value->m_args) + return false; + return true; + } }; class Map : public FilterBase @@ -67,9 +134,21 @@ class Map : public FilterBase public: Map(FilterParams params); - InternalValue Filter(const InternalValue& baseVal, RenderContext& context); - + InternalValue Filter(const InternalValue& baseVal, RenderContext& context) override; + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + if (m_args != value->m_args) + return false; + if (m_mappingParams != value->m_mappingParams) + return false; + return true; + } private: + static FilterParams MakeParams(FilterParams); + FilterParams m_mappingParams; }; @@ -78,7 +157,16 @@ class PrettyPrint : public FilterBase public: PrettyPrint(FilterParams params); - InternalValue Filter(const InternalValue& baseVal, RenderContext& context); + InternalValue Filter(const InternalValue& baseVal, RenderContext& context) override; + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + if (m_args != value->m_args) + return false; + return true; + } }; class Random : public FilterBase @@ -86,7 +174,16 @@ class Random : public FilterBase public: Random(FilterParams params); - InternalValue Filter(const InternalValue& baseVal, RenderContext& context); + InternalValue Filter(const InternalValue& baseVal, RenderContext& context) override; + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + if (m_args != value->m_args) + return false; + return true; + } }; class SequenceAccessor : public FilterBase @@ -108,8 +205,19 @@ class SequenceAccessor : public FilterBase SequenceAccessor(FilterParams params, Mode mode); - InternalValue Filter(const InternalValue& baseVal, RenderContext& context); + InternalValue Filter(const InternalValue& baseVal, RenderContext& context) override; + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + if (m_args != value->m_args) + return false; + if (m_mode != value->m_mode) + return false; + return true; + } private: Mode m_mode; }; @@ -126,7 +234,21 @@ class Serialize : public FilterBase Serialize(FilterParams params, Mode mode); - InternalValue Filter(const InternalValue& baseVal, RenderContext& context); + InternalValue Filter(const InternalValue& baseVal, RenderContext& context) override; + + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + if (m_args != value->m_args) + return false; + if (m_mode != value->m_mode) + return false; + return true; + } +private: + Mode m_mode; }; class Slice : public FilterBase @@ -140,7 +262,22 @@ class Slice : public FilterBase Slice(FilterParams params, Mode mode); - InternalValue Filter(const InternalValue& baseVal, RenderContext& context); + InternalValue Filter(const InternalValue& baseVal, RenderContext& context) override; + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + if (m_args != value->m_args) + return false; + if (m_mode != value->m_mode) + return false; + return true; + } +private: + InternalValue Batch(const InternalValue& baseVal, RenderContext& context); + + Mode m_mode; }; class Sort : public FilterBase @@ -148,7 +285,16 @@ class Sort : public FilterBase public: Sort(FilterParams params); - InternalValue Filter(const InternalValue& baseVal, RenderContext& context); + InternalValue Filter(const InternalValue& baseVal, RenderContext& context) override; + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + if (m_args != value->m_args) + return false; + return true; + } }; class StringConverter : public FilterBase @@ -162,6 +308,7 @@ class StringConverter : public FilterBase EscapeHtmlMode, LowerMode, ReplaceMode, + StriptagsMode, TitleMode, TrimMode, TruncateMode, @@ -169,13 +316,25 @@ class StringConverter : public FilterBase WordCountMode, WordWrapMode, UnderscoreMode, - UrlEncodeMode + UrlEncodeMode, + CenterMode }; StringConverter(FilterParams params, Mode mode); - InternalValue Filter(const InternalValue& baseVal, RenderContext& context); + InternalValue Filter(const InternalValue& baseVal, RenderContext& context) override; + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + if (m_args != value->m_args) + return false; + if (m_mode != value->m_mode) + return false; + return true; + } private: Mode m_mode; }; @@ -183,14 +342,24 @@ class StringConverter : public FilterBase class StringFormat : public FilterBase { public: - enum Mode - { - PythonMode, - }; + StringFormat(FilterParams params); + + InternalValue Filter(const InternalValue& baseVal, RenderContext& context) override; - StringFormat(FilterParams params, Mode mode); + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + if (m_args != value->m_args) + return false; + if (m_params != value->m_params) + return false; + return true; + } - InternalValue Filter(const InternalValue& baseVal, RenderContext& context); +private: + FilterParams m_params; }; class Tester : public FilterBase @@ -206,11 +375,24 @@ class Tester : public FilterBase Tester(FilterParams params, Mode mode); - InternalValue Filter(const InternalValue& baseVal, RenderContext& context); + InternalValue Filter(const InternalValue& baseVal, RenderContext& context) override; + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + if (m_args != value->m_args) + return false; + if (m_mode != value->m_mode) + return false; + if (m_testingParams != value->m_testingParams) + return false; + return true; + } private: Mode m_mode; - CallParams m_testingParams; + FilterParams m_testingParams; bool m_noParams = false; }; @@ -228,12 +410,65 @@ class ValueConverter : public FilterBase ValueConverter(FilterParams params, Mode mode); - InternalValue Filter(const InternalValue& baseVal, RenderContext& context); + InternalValue Filter(const InternalValue& baseVal, RenderContext& context) override; + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + if (m_args != value->m_args) + return false; + return m_mode == value->m_mode; + } private: Mode m_mode; }; -} // filters -} // jinja2 -#endif // FILTERS_H +class XmlAttrFilter : public FilterBase +{ +public: + explicit XmlAttrFilter(FilterParams params); + + InternalValue Filter(const InternalValue& baseVal, RenderContext& context) override; + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + if (m_args != value->m_args) + return false; + return true; + } +}; + +class UserDefinedFilter : public FilterBase +{ +public: + UserDefinedFilter(std::string filterName, FilterParams params); + + InternalValue Filter(const InternalValue& baseVal, RenderContext& context) override; + + bool IsEqual(const IComparable& other) const override + { + auto* value = dynamic_cast(&other); + if (!value) + return false; + if (m_args != value->m_args) + return false; + if (m_filterName != value->m_filterName) + return false; + if (m_callParams != m_callParams) + return false; + return true; + } + +private: + std::string m_filterName; + FilterParams m_callParams; +}; + +} // namespace filters +} // namespace jinja2 + +#endif // JINJA2CPP_SRC_FILTERS_H diff --git a/src/function_base.h b/src/function_base.h index 5be168c3..578545b0 100644 --- a/src/function_base.h +++ b/src/function_base.h @@ -1,5 +1,5 @@ -#ifndef FUNCTION_BASE_H -#define FUNCTION_BASE_H +#ifndef JINJA2CPP_SRC_FUNCTION_BASE_H +#define JINJA2CPP_SRC_FUNCTION_BASE_H #include "expression_evaluator.h" #include "internal_value.h" @@ -9,29 +9,41 @@ namespace jinja2 class FunctionBase { public: + bool operator==(const FunctionBase& other) const + { + return m_args == other.m_args; + } + bool operator!=(const FunctionBase& other) const + { + return !(*this == other); + } protected: - bool ParseParams(const std::initializer_list& argsInfo, const CallParams& params); - InternalValue GetArgumentValue(std::string argName, RenderContext& context, InternalValue defVal = InternalValue()); + bool ParseParams(const std::initializer_list& argsInfo, const CallParamsInfo& params); + InternalValue GetArgumentValue(const std::string& argName, RenderContext& context, InternalValue defVal = InternalValue()); protected: - ParsedArguments m_args; + ParsedArgumentsInfo m_args; }; +//bool operator==(const FunctionBase& lhs, const FunctionBase& rhs) +//{ +// return +//} -inline bool FunctionBase::ParseParams(const std::initializer_list& argsInfo, const CallParams& params) +inline bool FunctionBase::ParseParams(const std::initializer_list& argsInfo, const CallParamsInfo& params) { bool result = true; - m_args = helpers::ParseCallParams(argsInfo, params, result); + m_args = helpers::ParseCallParamsInfo(argsInfo, params, result); return result; } -inline InternalValue FunctionBase::GetArgumentValue(std::string argName, RenderContext& context, InternalValue defVal) +inline InternalValue FunctionBase::GetArgumentValue(const std::string& argName, RenderContext& context, InternalValue defVal) { auto argExpr = m_args[argName]; return argExpr ? argExpr->Evaluate(context) : std::move(defVal); } -} // jinja2 +} // namespace jinja2 -#endif // FUNCTION_BASE_H +#endif // JINJA2CPP_SRC_FUNCTION_BASE_H diff --git a/src/generic_adapters.h b/src/generic_adapters.h new file mode 100644 index 00000000..a6493d2a --- /dev/null +++ b/src/generic_adapters.h @@ -0,0 +1,215 @@ +#ifndef JINJA2CPP_SRC_GENERIC_ADAPTERS_H +#define JINJA2CPP_SRC_GENERIC_ADAPTERS_H + +#include +#include "internal_value.h" + +namespace jinja2 +{ + +template +class IndexedEnumeratorImpl : public Base +{ +public: + using ValueType = ValType; + using ThisType = IndexedEnumeratorImpl; + + IndexedEnumeratorImpl(const List* list) + : m_list(list) + , m_maxItems(list->GetSize().value()) + { } + + void Reset() override + { + m_curItem = m_invalidIndex; + } + + bool MoveNext() override + { + if (m_curItem == m_invalidIndex) + m_curItem = 0; + else + ++ m_curItem; + + return m_curItem < m_maxItems; + } + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_list && val->m_list && !m_list->IsEqual(*val->m_list)) + return false; + if ((m_list && !val->m_list) || (!m_list && val->m_list)) + return false; + if (m_curItem != val->m_curItem) + return false; + if (m_maxItems != val->m_maxItems) + return false; + return true; + } + +protected: + constexpr static auto m_invalidIndex = std::numeric_limits::max(); + const List* m_list{}; + size_t m_curItem = m_invalidIndex; + size_t m_maxItems{}; +}; + + +template +class IndexedListItemAccessorImpl : public IListItemAccessor, public IIndexBasedAccessor +{ +public: + using ThisType = IndexedListItemAccessorImpl; + class Enumerator : public IndexedEnumeratorImpl + { + public: + using BaseClass = IndexedEnumeratorImpl; +#if defined(_MSC_VER) + using IndexedEnumeratorImpl::IndexedEnumeratorImpl; +#else + using BaseClass::BaseClass; +#endif + + typename BaseClass::ValueType GetCurrent() const override + { + auto indexer = this->m_list->GetIndexer(); + if (!indexer) + return Value(); + + return indexer->GetItemByIndex(this->m_curItem); + } + ListEnumeratorPtr Clone() const override + { + auto result = MakeEnumerator(this->m_list); + auto base = static_cast(&(*result)); + base->m_curItem = this->m_curItem; + return result; + } + + ListEnumeratorPtr Move() override + { + auto result = MakeEnumerator(this->m_list); + auto base = static_cast(&(*result)); + base->m_curItem = this->m_curItem; + this->m_list = nullptr; + this->m_curItem = this->m_invalidIndex; + this->m_maxItems = 0; + return result; + } + }; + + Value GetItemByIndex(int64_t idx) const override + { + return IntValue2Value(std::move(static_cast(this)->GetItem(idx).value())); + } + + nonstd::optional GetSize() const override + { + return static_cast(this)->GetItemsCountImpl(); + } + + const IIndexBasedAccessor* GetIndexer() const override + { + return this; + } + + ListEnumeratorPtr CreateEnumerator() const override; + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + auto enumerator = CreateEnumerator(); + auto otherEnum = val->CreateEnumerator(); + if (enumerator && otherEnum && !enumerator->IsEqual(*otherEnum)) + return false; + return true; + } +}; + +template +class IndexedListAccessorImpl : public IListAccessor, public IndexedListItemAccessorImpl +{ +public: + using ThisType = IndexedListAccessorImpl; + class Enumerator : public IndexedEnumeratorImpl + { + public: + using BaseClass = IndexedEnumeratorImpl; +#if defined(_MSC_VER) + using IndexedEnumeratorImpl::IndexedEnumeratorImpl; +#else + using BaseClass::BaseClass; +#endif + + typename BaseClass::ValueType GetCurrent() const override + { + const auto& result = this->m_list->GetItem(this->m_curItem); + if (!result) + return InternalValue(); + + return result.value(); + } + + IListAccessorEnumerator* Clone() const override + { + auto result = new Enumerator(this->m_list); + auto base = result; + base->m_curItem = this->m_curItem; + return result; + } + + IListAccessorEnumerator* Transfer() override + { + auto result = new Enumerator(std::move(*this)); + auto base = result; + base->m_curItem = this->m_curItem; + this->m_list = nullptr; + this->m_curItem = this->m_invalidIndex; + this->m_maxItems = 0; + return result; + } + }; + + nonstd::optional GetSize() const override + { + return static_cast(this)->GetItemsCountImpl(); + } + ListAccessorEnumeratorPtr CreateListAccessorEnumerator() const override; +}; + +template +class MapItemAccessorImpl : public IMapItemAccessor +{ +public: + Value GetValueByName(const std::string& name) const + { + return IntValue2Value(static_cast(this)->GetItem(name)); + } +}; + +template +class MapAccessorImpl : public IMapAccessor, public MapItemAccessorImpl +{ +public: +}; + +template +inline ListAccessorEnumeratorPtr IndexedListAccessorImpl::CreateListAccessorEnumerator() const +{ + return ListAccessorEnumeratorPtr(new Enumerator(this)); +} + +template +inline ListEnumeratorPtr IndexedListItemAccessorImpl::CreateEnumerator() const +{ + return MakeEnumerator(this); +} + +} // namespace jinja2 + +#endif // JINJA2CPP_SRC_GENERIC_ADAPTERS_H diff --git a/src/generic_list.cpp b/src/generic_list.cpp new file mode 100644 index 00000000..883ef361 --- /dev/null +++ b/src/generic_list.cpp @@ -0,0 +1,38 @@ +#include +#include + +namespace jinja2 { + +detail::GenericListIterator GenericList::begin() const +{ + return m_accessor && m_accessor() ? detail::GenericListIterator(m_accessor()->CreateEnumerator()) : detail::GenericListIterator(); +} + +detail::GenericListIterator GenericList::end() const +{ + return detail::GenericListIterator(); +} + +auto GenericList::cbegin() const {return begin();} +auto GenericList::cend() const {return end();} + +bool GenericList::IsEqual(const GenericList& rhs) const +{ + if (IsValid() && rhs.IsValid() && !GetAccessor()->IsEqual(*rhs.GetAccessor())) + return false; + if ((IsValid() && !rhs.IsValid()) || (!IsValid() && rhs.IsValid())) + return false; + return true; +} + +bool operator==(const GenericList& lhs, const GenericList& rhs) +{ + return lhs.IsEqual(rhs); +} + +bool operator!=(const GenericList& lhs, const GenericList& rhs) +{ + return !(lhs == rhs); +} + +} // namespace jinja2 diff --git a/src/helpers.h b/src/helpers.h index d75ae2e2..3af280fb 100644 --- a/src/helpers.h +++ b/src/helpers.h @@ -1,5 +1,8 @@ -#ifndef HELPERS_H -#define HELPERS_H +#ifndef JINJA2CPP_SRC_HELPERS_H +#define JINJA2CPP_SRC_HELPERS_H + +#include +#include #include #include @@ -12,10 +15,27 @@ struct MultiStringLiteral const char* charValue; const wchar_t* wcharValue; + constexpr MultiStringLiteral(const char* val, const wchar_t* wval) + : charValue(val) + , wcharValue(wval) + { + } + + template + constexpr auto GetValue() const + { +#if __cplusplus < 202002L + return GetValueStr(); +#else + constexpr auto memPtr = SelectMemberPtr::GetPtr(); + return nonstd::basic_string_view(this->*memPtr); +#endif + } + template - auto GetValue() const + constexpr auto GetValueStr() const { - auto memPtr = SelectMemberPtr::GetPtr(); + constexpr auto memPtr = SelectMemberPtr::GetPtr(); return std::basic_string(this->*memPtr); } @@ -25,13 +45,13 @@ struct MultiStringLiteral template struct SelectMemberPtr { - static auto GetPtr() {return charMemPtr;} + static constexpr auto GetPtr() {return charMemPtr;} }; template struct SelectMemberPtr { - static auto GetPtr() {return wcharMemPtr;} + static constexpr auto GetPtr() {return wcharMemPtr;} }; template @@ -42,86 +62,58 @@ struct MultiStringLiteral } }; -#define UNIVERSAL_STR(Str) MultiStringLiteral{Str, L##Str} - -namespace detail -{ -template -struct StringConverter; - -template -struct StringConverter -{ - static Src DoConvert(const Src& from) - { - return from; - } -}; - -template<> -struct StringConverter -{ - static std::string DoConvert(const std::wstring& from) - { - 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; - } -}; - -template<> -struct StringConverter -{ - static std::wstring DoConvert(const std::string& from) - { - 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; - } -}; - -} // detail - -template -Dst ConvertString(Src&& from) +#define UNIVERSAL_STR(Str) \ + ::jinja2::MultiStringLiteral { Str, L##Str } + +//! CompileEscapes replaces escape characters by their meanings. +/** + * @param[in] s Characters sequence with zero or more escape characters. + * @return Characters sequence copy where replaced all escape characters by + * their meanings. + */ +template +Sequence CompileEscapes(Sequence s) { - return detail::StringConverter, std::decay_t>::DoConvert(std::forward(from)); + auto itr1 = s.begin(); + auto itr2 = s.begin(); + const auto end = s.cend(); + + auto removalCount = 0; + + while (end != itr1) + { + if ('\\' == *itr1) + { + ++removalCount; + + if (end == ++itr1) + break; + if ('\\' != *itr1) + { + switch (*itr1) + { + case 'n': *itr1 = '\n'; break; + case 'r': *itr1 = '\r'; break; + case 't': *itr1 = '\t'; break; + default: break; + } + + continue; + } + } + + if (itr1 != itr2) + *itr2 = *itr1; + + ++itr1; + ++itr2; + } + + s.resize(s.size() - removalCount); + + return s; } -} // jinja2 +} // namespace jinja2 -#endif // HELPERS_H +#endif // JINJA2CPP_SRC_HELPERS_H diff --git a/src/internal_value.cpp b/src/internal_value.cpp index 67a00233..3f64eeff 100644 --- a/src/internal_value.cpp +++ b/src/internal_value.cpp @@ -1,30 +1,150 @@ #include "internal_value.h" + +#include "expression_evaluator.h" +#include "generic_adapters.h" +#include "helpers.h" #include "value_visitors.h" namespace jinja2 { +std::atomic_uint64_t UserCallable::m_gen{}; + +bool Value::IsEqual(const Value& rhs) const +{ + return this->m_data == rhs.m_data; +} + +bool operator==(const Value& lhs, const Value& rhs) +{ + return lhs.IsEqual(rhs); +} + +bool operator!=(const Value& lhs, const Value& rhs) +{ + return !(lhs == rhs); +} + +bool operator==(const GenericMap& lhs, const GenericMap& rhs) +{ + auto* lhsAccessor = lhs.GetAccessor(); + auto* rhsAccessor = rhs.GetAccessor(); + return lhsAccessor && rhsAccessor && lhsAccessor->IsEqual(*rhsAccessor); +} + +bool operator!=(const GenericMap& lhs, const GenericMap& rhs) +{ + return !(lhs == rhs); +} + +bool operator==(const UserCallable& lhs, const UserCallable& rhs) +{ + // TODO: rework + return lhs.IsEqual(rhs); +} + +bool operator!=(const UserCallable& lhs, const UserCallable& rhs) +{ + return !(lhs == rhs); +} + +bool operator==(const types::ValuePtr& lhs, const types::ValuePtr& rhs) +{ + if (lhs && rhs) + return *lhs == *rhs; + if ((lhs && !rhs) || (!lhs && rhs)) + return false; + return true; +} + +bool operator!=(const types::ValuePtr& lhs, const types::ValuePtr& rhs) +{ + return !(lhs == rhs); +} + +bool operator==(const types::ValuePtr& lhs, const types::ValuePtr& rhs) +{ + if (lhs && rhs) + return *lhs == *rhs; + if ((lhs && !rhs) || (!lhs && rhs)) + return false; + return true; +} + +bool operator!=(const types::ValuePtr& lhs, const types::ValuePtr& rhs) +{ + return !(lhs == rhs); +} + +bool operator==(const types::ValuePtr& lhs, const types::ValuePtr& rhs) +{ + if (lhs && rhs) + return *lhs == *rhs; + if ((lhs && !rhs) || (!lhs && rhs)) + return false; + return true; +} + +bool operator!=(const types::ValuePtr& lhs, const types::ValuePtr& rhs) +{ + return !(lhs == rhs); +} + +bool operator==(const types::ValuePtr>& lhs, const types::ValuePtr>& rhs) +{ + if (lhs && rhs) + return *lhs == *rhs; + if ((lhs && !rhs) || (!lhs && rhs)) + return false; + return true; +} + +bool operator!=(const types::ValuePtr>& lhs, const types::ValuePtr>& rhs) +{ + return !(lhs == rhs); +} + +bool InternalValue::IsEqual(const InternalValue &other) const +{ + if (m_data != other.m_data) + return false; + return m_parentData == other.m_parentData; +} + +InternalValue Value2IntValue(const Value& val); +InternalValue Value2IntValue(Value&& val); + struct SubscriptionVisitor : public visitors::BaseVisitor<> { - using BaseVisitor::operator (); + using BaseVisitor<>::operator(); - InternalValue operator() (const MapAdapter& values, const std::string& field) const + template + InternalValue operator()(const MapAdapter& values, const std::basic_string& fieldName) const { + auto field = ConvertString(fieldName); if (!values.HasValue(field)) return InternalValue(); return values.GetValueByName(field); } - InternalValue operator() (const ListAdapter& values, int64_t index) const + template + InternalValue operator()(const MapAdapter& values, const nonstd::basic_string_view& fieldName) const { - if (index < 0 || static_cast(index) >= values.GetSize()) + auto field = ConvertString(fieldName); + if (!values.HasValue(field)) return InternalValue(); - return values.GetValueByIndex(index); + return values.GetValueByName(field); + } + + template + InternalValue operator()(std::basic_string value, const std::basic_string& /*fieldName*/) const + { + return TargetString(std::move(value)); } - InternalValue operator() (const MapAdapter& values, int64_t index) const + InternalValue operator()(const ListAdapter& values, int64_t index) const { if (index < 0 || static_cast(index) >= values.GetSize()) return InternalValue(); @@ -32,9 +152,22 @@ struct SubscriptionVisitor : public visitors::BaseVisitor<> return values.GetValueByIndex(index); } + InternalValue operator()(const MapAdapter& /*values*/, int64_t /*index*/) const { return InternalValue(); } + + template + InternalValue operator()(const std::basic_string& str, int64_t index) const + { + if (index < 0 || static_cast(index) >= str.size()) + return InternalValue(); + + std::basic_string resultStr(1, str[static_cast(index)]); + return TargetString(std::move(resultStr)); + } + template - InternalValue operator() (const std::basic_string& str, int64_t index) const + InternalValue operator()(const nonstd::basic_string_view& str, int64_t index) const { + // std::cout << "operator() (const std::basic_string& str, int64_t index)" << ": index = " << index << std::endl; if (index < 0 || static_cast(index) >= str.size()) return InternalValue(); @@ -42,8 +175,21 @@ struct SubscriptionVisitor : public visitors::BaseVisitor<> return TargetString(std::move(result)); } - InternalValue operator() (const KeyValuePair& values, const std::string& field) + template + InternalValue operator()(const KeyValuePair& values, const std::basic_string& fieldName) const + { + return SubscriptKvPair(values, ConvertString(fieldName)); + } + + template + InternalValue operator()(const KeyValuePair& values, const nonstd::basic_string_view& fieldName) const + { + return SubscriptKvPair(values, ConvertString(fieldName)); + } + + InternalValue SubscriptKvPair(const KeyValuePair& values, const std::string& field) const { + // std::cout << "operator() (const KeyValuePair& values, const std::string& field)" << ": field = " << field << std::endl; if (field == "key") return InternalValue(values.key); else if (field == "value") @@ -53,55 +199,91 @@ struct SubscriptionVisitor : public visitors::BaseVisitor<> } }; -InternalValue Subscript(const InternalValue& val, const InternalValue& subscript) +InternalValue Subscript(const InternalValue& val, const InternalValue& subscript, RenderContext* values) { - return Apply2(val, subscript); + static const std::string callOperName = "value()"; + auto result = Apply2(val, subscript); + + if (!values) + return result; + + auto map = GetIf(&result); + if (!map || !map->HasValue(callOperName)) + return result; + + auto callableVal = map->GetValueByName(callOperName); + auto callable = GetIf(&callableVal); + if (!callable || callable->GetKind() == Callable::Macro || callable->GetType() == Callable::Type::Statement) + return result; + + CallParams callParams; + return callable->GetExpressionCallable()(callParams, *values); } -InternalValue Subscript(const InternalValue& val, const std::string& subscript) +InternalValue Subscript(const InternalValue& val, const std::string& subscript, RenderContext* values) { - return Apply2(val, InternalValue(subscript)); + return Subscript(val, InternalValue(subscript), values); } -std::string AsString(const InternalValue& val) +struct StringGetter : public visitors::BaseVisitor { - auto* str = boost::get(&val); - auto* tstr = boost::get(&val); - if (str != nullptr) - return *str; - else - { - str = boost::get(tstr); - if (str != nullptr) - return *str; - } + using BaseVisitor::operator(); + + std::string operator()(const std::string& str) const { return str; } + std::string operator()(const nonstd::string_view& str) const { return std::string(str.begin(), str.end()); } + std::string operator()(const std::wstring& str) const { return ConvertString(str); } + std::string operator()(const nonstd::wstring_view& str) const { return ConvertString(str); } +}; - return std::string(); +std::string AsString(const InternalValue& val) +{ + return Apply(val); } struct ListConverter : public visitors::BaseVisitor> { - using BaseVisitor::operator (); + using BaseVisitor::operator(); using result_t = boost::optional; + bool strictConvertion; - result_t operator() (const ListAdapter& list) const + ListConverter(bool strict) + : strictConvertion(strict) { - return list; } -// template -// result_t operator() (const std::basic_string& str) const -// { -// return result_t(ListAdapter([adaptor = StringAdapter(&str)]() {return &adaptor;})); -// } + result_t operator()(const ListAdapter& list) const { return list; } + result_t operator()(const MapAdapter& map) const + { + if (strictConvertion) + return result_t(); + + InternalValueList list; + for (auto& k : map.GetKeys()) + list.push_back(TargetString(k)); + return ListAdapter::CreateAdapter(std::move(list)); + } + + template + result_t operator() (const std::basic_string& str) const + { + return strictConvertion ? result_t() : result_t(ListAdapter::CreateAdapter(str.size(), [str](size_t idx) { + return TargetString(str.substr(idx, 1));})); + } + + template + result_t operator()(const nonstd::basic_string_view& str) const + { + return strictConvertion ? result_t() : result_t(ListAdapter::CreateAdapter(str.size(), [str](size_t idx) { + return TargetString(std::basic_string(str[idx], 1)); })); + } }; -ListAdapter ConvertToList(const InternalValue& val, bool& isConverted) +ListAdapter ConvertToList(const InternalValue& val, bool& isConverted, bool strictConversion) { - auto result = Apply(val); + auto result = Apply(val, strictConversion); if (!result) { isConverted = false; @@ -111,9 +293,9 @@ ListAdapter ConvertToList(const InternalValue& val, bool& isConverted) return result.get(); } -ListAdapter ConvertToList(const InternalValue& val, InternalValue subscipt, bool& isConverted) +ListAdapter ConvertToList(const InternalValue& val, InternalValue subscipt, bool& isConverted, bool strictConversion) { - auto result = Apply(val); + auto result = Apply(val, strictConversion); if (!result) { isConverted = false; @@ -122,7 +304,7 @@ ListAdapter ConvertToList(const InternalValue& val, InternalValue subscipt, bool isConverted = true; if (IsEmpty(subscipt)) - return result.get(); + return std::move(result.get()); return result.get().ToSubscriptedList(subscipt, false); } @@ -131,125 +313,382 @@ template class ByRef { public: - ByRef(const T& val) + explicit ByRef(const T& val) : m_val(&val) - {} + { + } - const T& Get() const {return *m_val;} - T& Get() {return *const_cast(m_val);} + const T& Get() const { return *m_val; } + T& Get() { return *const_cast(m_val); } + bool ShouldExtendLifetime() const { return false; } + bool operator==(const ByRef& other) const + { + if (m_val && other.m_val && m_val != other.m_val) + return false; + if ((m_val && !other.m_val) || (!m_val && other.m_val)) + return false; + return true; + } + bool operator!=(const ByRef& other) const + { + return !(*this == other); + } private: - const T* m_val; + const T* m_val{}; }; template class ByVal { public: - ByVal(T&& val) + explicit ByVal(T&& val) : m_val(std::move(val)) - {} + { + } + ~ByVal() = default; - const T& Get() const {return m_val;} - T& Get() {return m_val;} + const T& Get() const { return m_val; } + T& Get() { return m_val; } + bool ShouldExtendLifetime() const { return false; } + bool operator==(const ByVal& other) const + { + return m_val == other.m_val; + } + bool operator!=(const ByVal& other) const + { + return !(*this == other); + } private: T m_val; }; +template +class BySharedVal +{ +public: + explicit BySharedVal(T&& val) + : m_val(std::make_shared(std::move(val))) + { + } + ~BySharedVal() = default; + + const T& Get() const { return *m_val; } + T& Get() { return *m_val; } + bool ShouldExtendLifetime() const { return true; } + + bool operator==(const BySharedVal& other) const + { + return m_val == other.m_val; + } + bool operator!=(const BySharedVal& other) const + { + return !(*this == other); + } +private: + std::shared_ptr m_val; +}; + template class Holder> class GenericListAdapter : public IListAccessor { public: + struct Enumerator : public IListAccessorEnumerator + { + ListEnumeratorPtr m_enum; + + explicit Enumerator(ListEnumeratorPtr e) + : m_enum(std::move(e)) + { + } + + // Inherited via IListAccessorEnumerator + void Reset() override + { + if (m_enum) + m_enum->Reset(); + } + bool MoveNext() override { return !m_enum ? false : m_enum->MoveNext(); } + InternalValue GetCurrent() const override { return !m_enum ? InternalValue() : Value2IntValue(m_enum->GetCurrent()); } + IListAccessorEnumerator* Clone() const override { return !m_enum ? new Enumerator(MakeEmptyListEnumeratorPtr()) : new Enumerator(m_enum->Clone()); } + IListAccessorEnumerator* Transfer() override { return new Enumerator(std::move(m_enum)); } + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_enum && val->m_enum && !m_enum->IsEqual(*val->m_enum)) + return false; + if ((m_enum && !val->m_enum) || (!m_enum && val->m_enum)) + return false; + return true; + } + }; + template - GenericListAdapter(U&& values) : m_values(std::forward(values)) {} + GenericListAdapter(U&& values) + : m_values(std::forward(values)) + { + } - size_t GetSize() const override {return m_values.Get().GetSize();} - InternalValue GetValueByIndex(int64_t idx) const override + nonstd::optional GetSize() const override { return m_values.Get().GetSize(); } + nonstd::optional GetItem(int64_t idx) const override + { + const IListItemAccessor* accessor = m_values.Get().GetAccessor(); + auto indexer = accessor->GetIndexer(); + if (!indexer) + return nonstd::optional(); + + auto val = indexer->GetItemByIndex(idx); + return visit(visitors::InputValueConvertor(true, false), std::move(val.data())).get(); + } + bool ShouldExtendLifetime() const override { return m_values.ShouldExtendLifetime(); } + ListAccessorEnumeratorPtr CreateListAccessorEnumerator() const override { - auto val = m_values.Get().GetValueByIndex(idx); - return boost::apply_visitor(visitors::InputValueConvertor(true), val.data()).get(); + const IListItemAccessor* accessor = m_values.Get().GetAccessor(); + if (!accessor) + return ListAccessorEnumeratorPtr(new Enumerator(MakeEmptyListEnumeratorPtr())); + return ListAccessorEnumeratorPtr(new Enumerator(m_values.Get().GetAccessor()->CreateEnumerator())); } + GenericList CreateGenericList() const override + { + // return m_values.Get(); + return GenericList([list = m_values]() -> const IListItemAccessor* { return list.Get().GetAccessor(); }); + } + private: Holder m_values; }; template class Holder> -class ValuesListAdapter : public IListAccessor +class ValuesListAdapter : public IndexedListAccessorImpl> { public: template - ValuesListAdapter(U&& values) : m_values(std::forward(values)) {} + ValuesListAdapter(U&& values) + : m_values(std::forward(values)) + { + } - size_t GetSize() const override {return m_values.Get().size();} - InternalValue GetValueByIndex(int64_t idx) const override + size_t GetItemsCountImpl() const { return m_values.Get().size(); } + nonstd::optional GetItem(int64_t idx) const override { - auto val = m_values.Get()[idx]; - return boost::apply_visitor(visitors::InputValueConvertor(false), val.data()).get(); + const auto& val = m_values.Get()[static_cast(idx)]; + return visit(visitors::InputValueConvertor(false, true), val.data()).get(); } + bool ShouldExtendLifetime() const override { return m_values.ShouldExtendLifetime(); } + GenericList CreateGenericList() const override + { + // return m_values.Get(); + return GenericList([list = *this]() -> const IListItemAccessor* { return &list; }); + } + private: Holder m_values; }; - ListAdapter ListAdapter::CreateAdapter(InternalValueList&& values) { - class Adapter : public IListAccessor + class Adapter : public IndexedListAccessorImpl { public: - explicit Adapter(InternalValueList&& values) : m_values(std::move(values)) {} + explicit Adapter(InternalValueList&& values) + : m_values(std::move(values)) + { + } + + size_t GetItemsCountImpl() const { return m_values.size(); } + nonstd::optional GetItem(int64_t idx) const override { return m_values[static_cast(idx)]; } + bool ShouldExtendLifetime() const override { return false; } + GenericList CreateGenericList() const override + { + return GenericList([adapter = *this]() -> const IListItemAccessor* { return &adapter; }); + } - size_t GetSize() const override {return m_values.size();} - InternalValue GetValueByIndex(int64_t idx) const override {return m_values[static_cast(idx)];} private: InternalValueList m_values; }; - return ListAdapter([accessor = Adapter(std::move(values))]() {return &accessor;}); + return ListAdapter([accessor = Adapter(std::move(values))]() { return &accessor; }); } ListAdapter ListAdapter::CreateAdapter(const GenericList& values) { - return ListAdapter([accessor = GenericListAdapter(values)]() {return &accessor;}); + return ListAdapter([accessor = GenericListAdapter(values)]() { return &accessor; }); } ListAdapter ListAdapter::CreateAdapter(const ValuesList& values) { - return ListAdapter([accessor = ValuesListAdapter(values)]() {return &accessor;}); + return ListAdapter([accessor = ValuesListAdapter(values)]() { return &accessor; }); } ListAdapter ListAdapter::CreateAdapter(GenericList&& values) { - return ListAdapter([accessor = GenericListAdapter(std::move(values))]() {return &accessor;}); + return ListAdapter([accessor = GenericListAdapter(std::move(values))]() { return &accessor; }); } ListAdapter ListAdapter::CreateAdapter(ValuesList&& values) { - return ListAdapter([accessor = ValuesListAdapter(std::move(values))]() {return &accessor;}); + return ListAdapter([accessor = ValuesListAdapter(std::move(values))]() { return &accessor; }); } -template class Holder> -class SubscriptedListAdapter : public IListAccessor +ListAdapter ListAdapter::CreateAdapter(std::function()> fn) { -public: - template - SubscriptedListAdapter(U&& values, const InternalValue& subscript) : m_values(std::forward(values)), m_subscript(subscript) {} + using GenFn = std::function()>; - size_t GetSize() const override {return m_values.Get().GetSize();} - InternalValue GetValueByIndex(int64_t idx) const override + class Adapter : public IListAccessor { - return Subscript(m_values.Get().GetValueByIndex(idx), m_subscript); - } -private: - Holder m_values; - InternalValue m_subscript; -}; + public: + class Enumerator : public IListAccessorEnumerator + { + public: + explicit Enumerator(const GenFn* fn) + : m_fn(fn) + { + if (!fn) + throw std::runtime_error("List enumerator couldn't be created without element accessor function!"); + } + + void Reset() override {} + + bool MoveNext() override + { + if (m_isFinished) + return false; + + auto res = (*m_fn)(); + if (!res) + return false; + + m_current = *res; + + return true; + } + + InternalValue GetCurrent() const override { return m_current; } + + IListAccessorEnumerator* Clone() const override + { + auto result = new Enumerator(*this); + return result; + } + + IListAccessorEnumerator* Transfer() override + { + auto result = new Enumerator(std::move(*this)); + return result; + } + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_isFinished != val->m_isFinished) + return false; + if (m_current != val->m_current) + return false; + // TODO: compare fn? + if (m_fn != val->m_fn) + return false; + return true; + } + + protected: + const GenFn* m_fn{}; + InternalValue m_current; + bool m_isFinished = false; + }; + + explicit Adapter(std::function()>&& fn) + : m_fn(std::move(fn)) + { + } + + nonstd::optional GetSize() const override { return nonstd::optional(); } + nonstd::optional GetItem(int64_t /*idx*/) const override { return nonstd::optional(); } + bool ShouldExtendLifetime() const override { return false; } + ListAccessorEnumeratorPtr CreateListAccessorEnumerator() const override { return ListAccessorEnumeratorPtr(new Enumerator(&m_fn)); } + + GenericList CreateGenericList() const override + { + return GenericList(); // return GenericList([adapter = *this]() -> const ListItemAccessor* {return &adapter; }); + } + + private: + std::function()> m_fn; + }; + + return ListAdapter([accessor = Adapter(std::move(fn))]() { return &accessor; }); +} + +ListAdapter ListAdapter::CreateAdapter(size_t listSize, std::function fn) +{ + using GenFn = std::function; + + class Adapter : public IndexedListAccessorImpl + { + public: + explicit Adapter(size_t listSize, GenFn&& fn) + : m_listSize(listSize) + , m_fn(std::move(fn)) + { + } + + size_t GetItemsCountImpl() const { return m_listSize; } + nonstd::optional GetItem(int64_t idx) const override { return m_fn(static_cast(idx)); } + bool ShouldExtendLifetime() const override { return false; } + GenericList CreateGenericList() const override + { + return GenericList([adapter = *this]() -> const IListItemAccessor* { return &adapter; }); + } + + private: + size_t m_listSize; + GenFn m_fn; + }; + + return ListAdapter([accessor = Adapter(listSize, std::move(fn))]() { return &accessor; }); +} + +template +auto CreateIndexedSubscribedList(Holder&& holder, const InternalValue& subscript, size_t size) +{ + return ListAdapter::CreateAdapter( + size, [h = std::forward(holder), subscript](size_t idx) -> InternalValue { return Subscript(h.Get().GetValueByIndex(idx), subscript, nullptr); }); +} + +template +auto CreateGenericSubscribedList(Holder&& holder, const InternalValue& subscript) +{ + return ListAdapter::CreateAdapter([h = std::forward(holder), e = ListAccessorEnumeratorPtr(), isFirst = true, isLast = false, subscript]() mutable { + using ResultType = nonstd::optional; + if (isFirst) + { + e = h.Get().GetEnumerator(); + isLast = !e->MoveNext(); + isFirst = false; + } + if (isLast) + return ResultType(); + + return ResultType(Subscript(e->GetCurrent(), subscript, nullptr)); + }); +} ListAdapter ListAdapter::ToSubscriptedList(const InternalValue& subscript, bool asRef) const { + auto listSize = GetSize(); if (asRef) - return ListAdapter([accessor = SubscriptedListAdapter(*this, subscript)]() {return &accessor;}); - - ListAdapter tmp(*this); - return ListAdapter([accessor = SubscriptedListAdapter(std::move(tmp), subscript)]() {return &accessor;}); + { + ByRef holder(*this); + return listSize ? CreateIndexedSubscribedList(holder, subscript, *listSize) : CreateGenericSubscribedList(holder, subscript); + } + else + { + ListAdapter tmp(*this); + BySharedVal holder(std::move(tmp)); + return listSize ? CreateIndexedSubscribedList(std::move(holder), subscript, *listSize) : CreateGenericSubscribedList(std::move(holder), subscript); + } } InternalValueList ListAdapter::ToValueList() const @@ -260,29 +699,18 @@ InternalValueList ListAdapter::ToValueList() const } template class Holder, bool CanModify> -class InternalValueMapAdapter : public IMapAccessor +class InternalValueMapAdapter : public MapAccessorImpl> { public: template - InternalValueMapAdapter(U&& values) : m_values(std::forward(values)) {} - - size_t GetSize() const override {return m_values.Get().size();} - InternalValue GetValueByIndex(int64_t idx) const override - { - KeyValuePair result; - auto p = m_values.Get().begin(); - std::advance(p, idx); - - result.key = p->first; - result.value = p->second; - - return InternalValue(std::move(result)); - } - bool HasValue(const std::string& name) const override + InternalValueMapAdapter(U&& values) + : m_values(std::forward(values)) { - return m_values.Get().count(name) != 0; } - InternalValue GetValueByName(const std::string& name) const override + + size_t GetSize() const override { return m_values.Get().size(); } + bool HasValue(const std::string& name) const override { return m_values.Get().count(name) != 0; } + InternalValue GetItem(const std::string& name) const override { auto& vals = m_values.Get(); auto p = vals.find(name); @@ -310,13 +738,25 @@ class InternalValueMapAdapter : public IMapAccessor } return false; } + bool ShouldExtendLifetime() const override { return m_values.ShouldExtendLifetime(); } + GenericMap CreateGenericMap() const override + { + return GenericMap([accessor = *this]() -> const IMapItemAccessor* { return &accessor; }); + } + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return m_values == val->m_values; + } private: Holder m_values; }; InternalValue Value2IntValue(const Value& val) { - auto result = boost::apply_visitor(visitors::InputValueConvertor(false), val.data()); + auto result = nonstd::visit(visitors::InputValueConvertor(false, true), val.data()); if (result) return result.get(); @@ -325,7 +765,7 @@ InternalValue Value2IntValue(const Value& val) InternalValue Value2IntValue(Value&& val) { - auto result = boost::apply_visitor(visitors::InputValueConvertor(true), val.data()); + auto result = nonstd::visit(visitors::InputValueConvertor(true, false), val.data()); if (result) return result.get(); @@ -333,27 +773,18 @@ InternalValue Value2IntValue(Value&& val) } template class Holder> -class GenericMapAdapter : public IMapAccessor +class GenericMapAdapter : public MapAccessorImpl> { public: template - GenericMapAdapter(U&& values) : m_values(std::forward(values)) {} - - size_t GetSize() const override {return m_values.Get().GetSize();} - InternalValue GetValueByIndex(int64_t idx) const override - { - auto val = m_values.Get().GetValueByIndex(idx); - KeyValuePair result; - auto& map = val.asMap(); - result.key = map["key"].asString(); - result.value = Value2IntValue(std::move(map["value"])); - return result; - } - bool HasValue(const std::string& name) const override + GenericMapAdapter(U&& values) + : m_values(std::forward(values)) { - return m_values.Get().HasValue(name); } - InternalValue GetValueByName(const std::string& name) const override + + size_t GetSize() const override { return m_values.Get().GetSize(); } + bool HasValue(const std::string& name) const override { return m_values.Get().HasValue(name); } + InternalValue GetItem(const std::string& name) const override { auto val = m_values.Get().GetValueByName(name); if (val.isEmpty()) @@ -361,41 +792,36 @@ class GenericMapAdapter : public IMapAccessor return Value2IntValue(std::move(val)); } - std::vector GetKeys() const override + std::vector GetKeys() const override { return m_values.Get().GetKeys(); } + bool ShouldExtendLifetime() const override { return m_values.ShouldExtendLifetime(); } + GenericMap CreateGenericMap() const override { - return m_values.Get().GetKeys(); + return GenericMap([accessor = *this]() -> const IMapItemAccessor* { return accessor.m_values.Get().GetAccessor(); }); + } + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return m_values == val->m_values; } - - private: Holder m_values; }; - template class Holder> -class ValuesMapAdapter : public IMapAccessor +class ValuesMapAdapter : public MapAccessorImpl> { public: template - ValuesMapAdapter(U&& values) : m_values(std::forward(values)) {} - - size_t GetSize() const override {return m_values.Get().size();} - InternalValue GetValueByIndex(int64_t idx) const override - { - KeyValuePair result; - auto p = m_values.Get().begin(); - std::advance(p, idx); - - result.key = p->first; - result.value = Value2IntValue(p->second); - - return result; - } - bool HasValue(const std::string& name) const override + ValuesMapAdapter(U&& values) + : m_values(std::forward(values)) { - return m_values.Get().count(name) != 0; } - InternalValue GetValueByName(const std::string& name) const override + + size_t GetSize() const override { return m_values.Get().size(); } + bool HasValue(const std::string& name) const override { return m_values.Get().count(name) != 0; } + InternalValue GetItem(const std::string& name) const override { auto& vals = m_values.Get(); auto p = vals.find(name); @@ -413,39 +839,215 @@ class ValuesMapAdapter : public IMapAccessor return result; } + bool ShouldExtendLifetime() const override { return m_values.ShouldExtendLifetime(); } + GenericMap CreateGenericMap() const override + { + return GenericMap([accessor = *this]() -> const IMapItemAccessor* { return &accessor; }); + } + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return m_values == val->m_values; + } private: Holder m_values; }; +MapAdapter CreateMapAdapter(InternalValueMap&& values) +{ + return MapAdapter([accessor = InternalValueMapAdapter(std::move(values))]() mutable { return &accessor; }); +} -MapAdapter MapAdapter::CreateAdapter(InternalValueMap&& values) +MapAdapter CreateMapAdapter(const InternalValueMap* values) { - return MapAdapter([accessor = InternalValueMapAdapter(std::move(values))]() mutable {return &accessor;}); + return MapAdapter([accessor = InternalValueMapAdapter(*values)]() mutable { return &accessor; }); } -MapAdapter MapAdapter::CreateAdapter(const InternalValueMap* values) +MapAdapter CreateMapAdapter(const GenericMap& values) { - return MapAdapter([accessor = InternalValueMapAdapter(*values)]() mutable {return &accessor;}); + return MapAdapter([accessor = GenericMapAdapter(values)]() mutable { return &accessor; }); } -MapAdapter MapAdapter::CreateAdapter(const GenericMap& values) +MapAdapter CreateMapAdapter(GenericMap&& values) { - return MapAdapter([accessor = GenericMapAdapter(values)]() mutable {return &accessor;}); + return MapAdapter([accessor = GenericMapAdapter(std::move(values))]() mutable { return &accessor; }); } -MapAdapter MapAdapter::CreateAdapter(GenericMap&& values) +MapAdapter CreateMapAdapter(const ValuesMap& values) { - return MapAdapter([accessor = GenericMapAdapter(std::move(values))]() mutable {return &accessor;}); + return MapAdapter([accessor = ValuesMapAdapter(values)]() mutable { return &accessor; }); } -MapAdapter MapAdapter::CreateAdapter(const ValuesMap& values) +MapAdapter CreateMapAdapter(ValuesMap&& values) { - return MapAdapter([accessor = ValuesMapAdapter(values)]() mutable {return &accessor;}); + return MapAdapter([accessor = ValuesMapAdapter(std::move(values))]() mutable { return &accessor; }); } -MapAdapter MapAdapter::CreateAdapter(ValuesMap&& values) +struct OutputValueConvertor { - return MapAdapter([accessor = ValuesMapAdapter(std::move(values))]() mutable {return &accessor;}); + using result_t = Value; + + result_t operator()(const EmptyValue&) const { return result_t(); } + result_t operator()(const MapAdapter& adapter) const { return result_t(adapter.CreateGenericMap()); } + result_t operator()(const ListAdapter& adapter) const { return result_t(adapter.CreateGenericList()); } + result_t operator()(const ValueRef& ref) const { return ref.get(); } + result_t operator()(const TargetString& str) const + { + switch (str.index()) + { + case 0: + return nonstd::get(str); + default: + return nonstd::get(str); + } + } + result_t operator()(const TargetStringView& str) const + { + switch (str.index()) + { + case 0: + return nonstd::get(str); + default: + return nonstd::get(str); + } + } + result_t operator()(const KeyValuePair& pair) const { return ValuesMap{ { "key", Value(pair.key) }, { "value", IntValue2Value(pair.value) } }; } + result_t operator()(const Callable&) const { return result_t(); } + result_t operator()(const UserCallable&) const { return result_t(); } + result_t operator()(const std::shared_ptr&) const { return result_t(); } + + template + result_t operator()(const RecWrapper& val) const + { + return this->operator()(const_cast(*val)); + } + + template + result_t operator()(RecWrapper& val) const + { + return this->operator()(*val); + } + + template + result_t operator()(T&& val) const + { + return result_t(std::forward(val)); + } + + bool m_byValue; +}; + +Value OptIntValue2Value(nonstd::optional val) +{ + if (val) + return Apply(val.value()); + + return Value(); +} + +Value IntValue2Value(const InternalValue& val) +{ + return Apply(val); } -} // jinja2 +class ContextMapper : public IMapItemAccessor +{ +public: + explicit ContextMapper(RenderContext* context) + : m_context(context) + { + } + + size_t GetSize() const override { return std::numeric_limits::max(); } + bool HasValue(const std::string& name) const override + { + bool found = false; + m_context->FindValue(name, found); + return found; + } + Value GetValueByName(const std::string& name) const override + { + bool found = false; + auto p = m_context->FindValue(name, found); + return found ? IntValue2Value(p->second) : Value(); + } + std::vector GetKeys() const override { return std::vector(); } + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_context && val->m_context && !m_context->IsEqual(*val->m_context)) + { + return false; + } + if ((m_context && !val->m_context) || (!m_context && val->m_context)) + return false; + return true; + } + +private: + RenderContext* m_context; +}; + +UserCallableParams PrepareUserCallableParams(const CallParams& params, RenderContext& context, const std::vector& argsInfo) +{ + UserCallableParams result; + + ParsedArguments args = helpers::ParseCallParams(argsInfo, params, result.paramsParsed); + if (!result.paramsParsed) + return result; + + for (auto& argInfo : argsInfo) + { + if (argInfo.name.size() > 1 && argInfo.name[0] == '*') + continue; + + auto p = args.args.find(argInfo.name); + if (p == args.args.end()) + { + result.args[argInfo.name] = IntValue2Value(argInfo.defaultVal); + continue; + } + + const auto& v = p->second; + result.args[argInfo.name] = IntValue2Value(v); + } + + ValuesMap extraKwArgs; + for (auto& p : args.extraKwArgs) + extraKwArgs[p.first] = IntValue2Value(p.second); + result.extraKwArgs = Value(std::move(extraKwArgs)); + + ValuesList extraPosArgs; + for (auto& p : args.extraPosArgs) + extraPosArgs.push_back(IntValue2Value(p)); + result.extraPosArgs = Value(std::move(extraPosArgs)); + result.context = GenericMap([accessor = ContextMapper(&context)]() -> const IMapItemAccessor* { return &accessor; }); + + return result; +} + +namespace visitors +{ + +InputValueConvertor::result_t InputValueConvertor::ConvertUserCallable(const UserCallable& val) +{ + std::vector args; + for (auto& pi : val.argsInfo) + { + args.emplace_back(pi.paramName, pi.isMandatory, Value2IntValue(pi.defValue)); + } + + return InternalValue(Callable(Callable::UserCallable, [val, argsInfo = std::move(args)](const CallParams& params, RenderContext& context) -> InternalValue { + auto ucParams = PrepareUserCallableParams(params, context, argsInfo); + return Value2IntValue(val.callable(ucParams)); + })); +} + +} // namespace visitors + +} // namespace jinja2 diff --git a/src/internal_value.h b/src/internal_value.h index 25fabf44..b674db1c 100644 --- a/src/internal_value.h +++ b/src/internal_value.h @@ -1,9 +1,26 @@ -#ifndef INTERNAL_VALUE_H -#define INTERNAL_VALUE_H +#ifndef JINJA2CPP_SRC_INTERNAL_VALUE_H +#define JINJA2CPP_SRC_INTERNAL_VALUE_H #include -#include +//#include + #include +#include +#include + +#include + +#if defined(_MSC_VER) && _MSC_VER <= 1900 // robin_hood hash map doesn't compatible with MSVC 14.0 +#include +#else +#include "robin_hood.h" +#endif + + +#include +#include + +#include namespace jinja2 { @@ -12,31 +29,76 @@ template class ReferenceWrapper { public: - using type = T; + using type = T; + + ReferenceWrapper(T& ref) noexcept + : m_ptr(std::addressof(ref)) + { + } - ReferenceWrapper(T& ref) noexcept - : m_ptr(std::addressof(ref)) - { - } + ReferenceWrapper(T&&) = delete; + ReferenceWrapper(const ReferenceWrapper&) noexcept = default; - ReferenceWrapper(T&&) = delete; - ReferenceWrapper(const ReferenceWrapper&) noexcept = default; + // assignment + ReferenceWrapper& operator=(const ReferenceWrapper& x) noexcept = default; + + // access + T& get() const noexcept + { + return *m_ptr; + } + +private: + T* m_ptr; +}; + +template +class RecursiveWrapper +{ +public: + RecursiveWrapper() = default; + + RecursiveWrapper(const T& value) + : m_data(value) + {} - // assignment - ReferenceWrapper& operator=(const ReferenceWrapper& x) noexcept = default; + RecursiveWrapper(T&& value) + : m_data(std::move(value)) + {} - // access - T& get() const noexcept - { - return *m_ptr; - } + const T& GetValue() const {return m_data.get();} + T& GetValue() {return m_data.get();} private: - T* m_ptr; + boost::recursive_wrapper m_data; + +#if 0 + enum class State + { + Undefined, + Inplace, + Ptr + }; + + State m_state; + + union + { + uint64_t dummy; + nonstd::value_ptr ptr; + } m_data; +#endif }; +template +auto MakeWrapped(T&& val) +{ + return RecursiveWrapper>(std::forward(val)); +} + using ValueRef = ReferenceWrapper; -using TargetString = boost::variant; +using TargetString = nonstd::variant; +using TargetStringView = nonstd::variant; class ListAdapter; class MapAdapter; @@ -45,29 +107,134 @@ class OutStream; class Callable; struct CallParams; struct KeyValuePair; -class RendererBase; +class IRendererBase; + +class InternalValue; +using InternalValueData = nonstd::variant< + EmptyValue, + bool, + std::string, + TargetString, + TargetStringView, + int64_t, + double, + ValueRef, + ListAdapter, + MapAdapter, + RecursiveWrapper, + RecursiveWrapper, + std::shared_ptr>; + -using InternalValue = boost::variant, boost::recursive_wrapper, RendererBase*>; using InternalValueRef = ReferenceWrapper; -using InternalValueMap = std::unordered_map; using InternalValueList = std::vector; +template +struct ValueGetter +{ + template + static auto& Get(V&& val) + { + return nonstd::get(std::forward(val).GetData()); + } + + static auto GetPtr(const InternalValue* val); + + static auto GetPtr(InternalValue* val); + + template + static auto GetPtr(V* val, std::enable_if_t::value>* = nullptr) + { + return nonstd::get_if(val); + } +}; + +template +struct ValueGetter +{ + template + static auto& Get(V&& val) + { + auto& ref = nonstd::get>(std::forward(val)); + return ref.GetValue(); + } + + static auto GetPtr(const InternalValue* val); + + static auto GetPtr(InternalValue* val); + + template + static auto GetPtr(V* val, std::enable_if_t::value>* = nullptr) + { + auto ref = nonstd::get_if>(val); + return !ref ? nullptr : &ref->GetValue(); + } +}; + +template +struct IsRecursive : std::false_type {}; + +template<> +struct IsRecursive : std::true_type {}; + +template<> +struct IsRecursive : std::true_type {}; + +struct IListAccessorEnumerator : virtual IComparable +{ + virtual ~IListAccessorEnumerator() {} + + virtual void Reset() = 0; + + virtual bool MoveNext() = 0; + virtual InternalValue GetCurrent() const = 0; + + virtual IListAccessorEnumerator* Clone() const = 0; + virtual IListAccessorEnumerator* Transfer() = 0; +/* + struct Cloner + { + Cloner() = default; + + IListAccessorEnumerator* operator()(const IListAccessorEnumerator &x) const + { + return x.Clone(); + } + + IListAccessorEnumerator* operator()(IListAccessorEnumerator &&x) const + { + return x.Transfer(); + } + }; + */ +}; + +using ListAccessorEnumeratorPtr = types::ValuePtr; + struct IListAccessor { virtual ~IListAccessor() {} - virtual size_t GetSize() const = 0; - virtual InternalValue GetValueByIndex(int64_t idx) const = 0; + virtual nonstd::optional GetSize() const = 0; + virtual nonstd::optional GetItem(int64_t idx) const = 0; + virtual ListAccessorEnumeratorPtr CreateListAccessorEnumerator() const = 0; + virtual GenericList CreateGenericList() const = 0; + virtual bool ShouldExtendLifetime() const = 0; }; + using ListAccessorProvider = std::function; -struct IMapAccessor : public IListAccessor +struct IMapAccessor { + virtual ~IMapAccessor() = default; + virtual size_t GetSize() const = 0; virtual bool HasValue(const std::string& name) const = 0; - virtual InternalValue GetValueByName(const std::string& name) const = 0; + virtual InternalValue GetItem(const std::string& name) const = 0; virtual std::vector GetKeys() const = 0; - virtual bool SetValue(std::string name, const InternalValue& val) {return false;} + virtual bool SetValue(std::string, const InternalValue&) {return false;} + virtual GenericMap CreateGenericMap() const = 0; + virtual bool ShouldExtendLifetime() const = 0; }; using MapAccessorProvider = std::function; @@ -85,11 +252,13 @@ class ListAdapter static ListAdapter CreateAdapter(const ValuesList& values); static ListAdapter CreateAdapter(GenericList&& values); static ListAdapter CreateAdapter(ValuesList&& values); + static ListAdapter CreateAdapter(std::function ()> fn); + static ListAdapter CreateAdapter(size_t listSize, std::function fn); ListAdapter& operator = (const ListAdapter&) = default; ListAdapter& operator = (ListAdapter&&) = default; - size_t GetSize() const + nonstd::optional GetSize() const { if (m_accessorProvider && m_accessorProvider()) { @@ -99,9 +268,26 @@ class ListAdapter return 0; } InternalValue GetValueByIndex(int64_t idx) const; + bool ShouldExtendLifetime() const + { + if (m_accessorProvider && m_accessorProvider()) + { + return m_accessorProvider()->ShouldExtendLifetime(); + } + + return false; + } ListAdapter ToSubscriptedList(const InternalValue& subscript, bool asRef = false) const; InternalValueList ToValueList() const; + GenericList CreateGenericList() const + { + if (m_accessorProvider && m_accessorProvider()) + return m_accessorProvider()->CreateGenericList(); + + return GenericList(); + } + ListAccessorEnumeratorPtr GetEnumerator() const; class Iterator; @@ -115,16 +301,9 @@ class ListAdapter class MapAdapter { public: - MapAdapter() {} + MapAdapter() = default; explicit MapAdapter(MapAccessorProvider prov) : m_accessorProvider(std::move(prov)) {} - static MapAdapter CreateAdapter(InternalValueMap&& values); - static MapAdapter CreateAdapter(const InternalValueMap* values); - static MapAdapter CreateAdapter(const GenericMap& values); - static MapAdapter CreateAdapter(GenericMap&& values); - static MapAdapter CreateAdapter(const ValuesMap& values); - static MapAdapter CreateAdapter(ValuesMap&& values); - size_t GetSize() const { if (m_accessorProvider && m_accessorProvider()) @@ -134,7 +313,7 @@ class MapAdapter return 0; } - InternalValue GetValueByIndex(int64_t idx) const; + // InternalValue GetValueByIndex(int64_t idx) const; bool HasValue(const std::string& name) const { if (m_accessorProvider && m_accessorProvider()) @@ -158,32 +337,108 @@ class MapAdapter { if (m_accessorProvider && m_accessorProvider()) { - return m_accessorProvider()->SetValue(name, val); + return m_accessorProvider()->SetValue(std::move(name), val); } return false; } + bool ShouldExtendLifetime() const + { + if (m_accessorProvider && m_accessorProvider()) + { + return m_accessorProvider()->ShouldExtendLifetime(); + } + + return false; + } + + GenericMap CreateGenericMap() const + { + if (m_accessorProvider && m_accessorProvider()) + return m_accessorProvider()->CreateGenericMap(); + + return GenericMap(); + } private: MapAccessorProvider m_accessorProvider; }; + +class InternalValue +{ +public: + InternalValue() = default; + + template + InternalValue(T&& val, typename std::enable_if, InternalValue>::value>::type* = nullptr) + : m_data(InternalValueData(std::forward(val))) + { + } + + auto& GetData() const {return m_data;} + auto& GetData() {return m_data;} + + auto& GetParentData() {return m_parentData;} + auto& GetParentData() const {return m_parentData;} + + void SetParentData(const InternalValue& val) + { + m_parentData = val.GetData(); + } + + void SetParentData(InternalValue&& val) + { + m_parentData = std::move(val.GetData()); + } + + bool ShouldExtendLifetime() const + { + if (m_parentData.index() != 0) + return true; + + const MapAdapter* ma = nonstd::get_if(&m_data); + if (ma != nullptr) + return ma->ShouldExtendLifetime(); + + const ListAdapter* la = nonstd::get_if(&m_data); + if (la != nullptr) + return la->ShouldExtendLifetime(); + + return false; + } + + bool IsEmpty() const {return m_data.index() == 0;} + + bool IsEqual(const InternalValue& other) const; + +private: + InternalValueData m_data; + InternalValueData m_parentData; +}; + +inline bool operator==(const InternalValue& lhs, const InternalValue& rhs) +{ + return lhs.IsEqual(rhs); +} +inline bool operator!=(const InternalValue& lhs, const InternalValue& rhs) +{ + return !(lhs == rhs); +} + class ListAdapter::Iterator : public boost::iterator_facade< Iterator, const InternalValue, - boost::single_pass_traversal_tag> + boost::forward_traversal_tag> { public: - Iterator() - : m_current(0) - , m_list(nullptr) - {} + Iterator() = default; - explicit Iterator(const ListAdapter& list) - : m_current(0) - , m_list(&list) - , m_currentVal(list.GetSize() == 0 ? InternalValue() : list.GetValueByIndex(0)) + explicit Iterator(ListAccessorEnumeratorPtr&& iter) + : m_iterator(std::move(iter)) + , m_isFinished(!m_iterator->MoveNext()) + , m_currentVal(m_isFinished ? InternalValue() : m_iterator->GetCurrent()) {} private: @@ -191,19 +446,24 @@ class ListAdapter::Iterator void increment() { - ++ m_current; - m_currentVal = m_current == m_list->GetSize() ? InternalValue() : m_list->GetValueByIndex(m_current); + m_isFinished = !m_iterator->MoveNext(); + ++ m_currentIndex; + m_currentVal = m_isFinished ? InternalValue() : m_iterator->GetCurrent(); } bool equal(const Iterator& other) const { - if (m_list == nullptr) - return other.m_list == nullptr ? true : other.equal(*this); - - if (other.m_list == nullptr) - return m_current == m_list->GetSize(); - - return this->m_list == other.m_list && this->m_current == other.m_current; + if (!this->m_iterator) + return !other.m_iterator ? true : other.equal(*this); + + if (!other.m_iterator) + return this->m_isFinished; +// return true; + //const InternalValue& lhs = *(this->m_iterator); + //const InternalValue& rhs = *(other.m_iterator); + //return lhs == rhs; + return this->m_iterator->GetCurrent() == other.m_iterator->GetCurrent() && this->m_currentIndex == other.m_currentIndex; + ///return *(this->m_iterator) == *(other.m_iterator) && this->m_currentIndex == other.m_currentIndex; } const InternalValue& dereference() const @@ -211,42 +471,101 @@ class ListAdapter::Iterator return m_currentVal; } - int64_t m_current = 0; - const ListAdapter* m_list; + ListAccessorEnumeratorPtr m_iterator; + bool m_isFinished = true; + mutable uint64_t m_currentIndex = 0; mutable InternalValue m_currentVal; }; -inline InternalValue ListAdapter::GetValueByIndex(int64_t idx) const +#if defined(_MSC_VER) && _MSC_VER <= 1900 // robin_hood hash map doesn't compatible with MSVC 14.0 +typedef std::unordered_map InternalValueMap; +#else +typedef robin_hood::unordered_map InternalValueMap; +#endif + + +MapAdapter CreateMapAdapter(InternalValueMap&& values); +MapAdapter CreateMapAdapter(const InternalValueMap* values); +MapAdapter CreateMapAdapter(const GenericMap& values); +MapAdapter CreateMapAdapter(GenericMap&& values); +MapAdapter CreateMapAdapter(const ValuesMap& values); +MapAdapter CreateMapAdapter(ValuesMap&& values); + +template +inline auto ValueGetter::GetPtr(const InternalValue* val) { - if (m_accessorProvider && m_accessorProvider()) - { - return m_accessorProvider()->GetValueByIndex(idx); - } + return nonstd::get_if(&val->GetData()); +} - return InternalValue(); +template +inline auto ValueGetter::GetPtr(InternalValue* val) +{ + return nonstd::get_if(&val->GetData()); +} + +template +inline auto ValueGetter::GetPtr(const InternalValue* val) +{ + auto ref = nonstd::get_if>(&val->GetData()); + return !ref ? nullptr : &ref->GetValue(); +} + +template +inline auto ValueGetter::GetPtr(InternalValue* val) +{ + auto ref = nonstd::get_if>(&val->GetData()); + return !ref ? nullptr : &ref->GetValue(); +} + +template +auto& Get(V&& val) +{ + return ValueGetter::value>::Get(std::forward(val).GetData()); } -inline InternalValue MapAdapter::GetValueByIndex(int64_t idx) const +template +auto GetIf(V* val) +{ + return ValueGetter::value>::GetPtr(val); +} + + +inline InternalValue ListAdapter::GetValueByIndex(int64_t idx) const { if (m_accessorProvider && m_accessorProvider()) { - return m_accessorProvider()->GetValueByIndex(idx); + const auto& val = m_accessorProvider()->GetItem(idx); + if (val) + return std::move(val.value()); + + return InternalValue(); } return InternalValue(); } +//inline InternalValue MapAdapter::GetValueByIndex(int64_t idx) const +//{ +// if (m_accessorProvider && m_accessorProvider()) +// { +// return static_cast(m_accessorProvider())->GetItem(idx); +// } + +// return InternalValue(); +//} + inline InternalValue MapAdapter::GetValueByName(const std::string& name) const { if (m_accessorProvider && m_accessorProvider()) { - return m_accessorProvider()->GetValueByName(name); + return m_accessorProvider()->GetItem(name); } return InternalValue(); } -inline ListAdapter::Iterator ListAdapter::begin() const {return Iterator(*this);} +inline ListAccessorEnumeratorPtr ListAdapter::GetEnumerator() const {return m_accessorProvider()->CreateListAccessorEnumerator();} +inline ListAdapter::Iterator ListAdapter::begin() const {return Iterator(m_accessorProvider()->CreateListAccessorEnumerator());} inline ListAdapter::Iterator ListAdapter::end() const {return Iterator();} @@ -260,10 +579,17 @@ struct KeyValuePair class Callable { public: + enum Kind + { + GlobalFunc, + SpecialFunc, + Macro, + UserCallable + }; using ExpressionCallable = std::function; using StatementCallable = std::function; - using CallableHolder = boost::variant; + using CallableHolder = nonstd::variant; enum class Type { @@ -271,19 +597,26 @@ class Callable Statement }; - Callable(ExpressionCallable&& callable) - : m_callable(std::move(callable)) + Callable(Kind kind, ExpressionCallable&& callable) + : m_kind(kind) + , m_callable(std::move(callable)) { } - Callable(StatementCallable&& callable) - : m_callable(std::move(callable)) + Callable(Kind kind, StatementCallable&& callable) + : m_kind(kind) + , m_callable(std::move(callable)) { } auto GetType() const { - return m_callable.which() == 0 ? Type::Expression : Type::Statement; + return m_callable.index() == 0 ? Type::Expression : Type::Statement; + } + + auto GetKind() const + { + return m_kind; } auto& GetCallable() const @@ -293,29 +626,48 @@ class Callable auto& GetExpressionCallable() const { - return boost::get(m_callable); + return nonstd::get(m_callable); } auto& GetStatementCallable() const { - return boost::get(m_callable); + return nonstd::get(m_callable); } private: + Kind m_kind; CallableHolder m_callable; }; inline bool IsEmpty(const InternalValue& val) { - return boost::get(&val) != nullptr; + return val.IsEmpty() || nonstd::get_if(&val.GetData()) != nullptr; +} + +class RenderContext; + +template +auto MakeDynamicProperty(Fn&& fn) +{ + return CreateMapAdapter(InternalValueMap{ + {"value()", Callable(Callable::GlobalFunc, std::forward(fn))} + }); +} + +template +auto sv_to_string(const nonstd::basic_string_view& sv) +{ + return std::basic_string(sv.begin(), sv.end()); } -InternalValue Subscript(const InternalValue& val, const InternalValue& subscript); -InternalValue Subscript(const InternalValue& val, const std::string& subscript); +InternalValue Subscript(const InternalValue& val, const InternalValue& subscript, RenderContext* values); +InternalValue Subscript(const InternalValue& val, const std::string& subscript, RenderContext* values); std::string AsString(const InternalValue& val); -ListAdapter ConvertToList(const InternalValue& val, bool& isConverted); -ListAdapter ConvertToList(const InternalValue& val, InternalValue subscipt, bool& isConverted); +ListAdapter ConvertToList(const InternalValue& val, bool& isConverted, bool strictConversion = true); +ListAdapter ConvertToList(const InternalValue& val, InternalValue subscipt, bool& isConverted, bool strictConversion = true); +Value IntValue2Value(const InternalValue& val); +Value OptIntValue2Value(nonstd::optional val); -} // jinja2 +} // namespace jinja2 -#endif // INTERNAL_VALUE_H +#endif // JINJA2CPP_SRC_INTERNAL_VALUE_H diff --git a/src/lexer.cpp b/src/lexer.cpp index e963f619..dfd33eb3 100644 --- a/src/lexer.cpp +++ b/src/lexer.cpp @@ -80,12 +80,30 @@ bool Lexer::ProcessNumber(const lexertk::token&, Token& newToken) bool Lexer::ProcessSymbolOrKeyword(const lexertk::token&, Token& newToken) { - Token::Type tokType = m_helper->GetKeyword(newToken.range); + Keyword kwType = m_helper->GetKeyword(newToken.range); + Token::Type tokType = Token::Unknown; + + switch (kwType) + { + case Keyword::None: + tokType = Token::None; + break; + case Keyword::True: + tokType = Token::True; + break; + case Keyword::False: + tokType = Token::False; + break; + default: + tokType = Token::Unknown; + break; + } + if (tokType == Token::Unknown) { newToken.type = Token::Identifier; auto id = m_helper->GetAsString(newToken.range); - newToken.value = id; + newToken.value = InternalValue(id); } else { @@ -101,4 +119,4 @@ bool Lexer::ProcessString(const lexertk::token&, Token& newToken) return true; } -} // jinja2 +} // namespace jinja2 diff --git a/src/lexer.h b/src/lexer.h index 426910b5..e644de2f 100644 --- a/src/lexer.h +++ b/src/lexer.h @@ -1,5 +1,5 @@ -#ifndef LEXER_H -#define LEXER_H +#ifndef JINJA2CPP_SRC_LEXER_H +#define JINJA2CPP_SRC_LEXER_H #include "lexertk.h" #include "internal_value.h" @@ -52,18 +52,18 @@ struct Token GreaterEqual, StarStar, DashDash, - LogicalOr, - LogicalAnd, - LogicalNot, MulMul, DivDiv, True, False, None, - In, - Is, // Keywords + LogicalOr, + LogicalAnd, + LogicalNot, + In, + Is, For, Endfor, If, @@ -83,16 +83,31 @@ struct Token EndSet, Include, Import, + Recursive, + Scoped, + With, + EndWith, + Without, + Ignore, + Missing, + Context, + From, + As, + Do, // Template control CommentBegin, CommentEnd, + RawBegin, + RawEnd, + MetaBegin, + MetaEnd, StmtBegin, StmtEnd, ExprBegin, ExprEnd, }; - + Type type = Unknown; CharRange range = {0, 0}; InternalValue value; @@ -119,11 +134,56 @@ struct Token } }; +enum class Keyword +{ + Unknown, + + // Keywords + LogicalOr, + LogicalAnd, + LogicalNot, + True, + False, + None, + In, + Is, + For, + Endfor, + If, + Else, + ElIf, + EndIf, + Block, + EndBlock, + Extends, + Macro, + EndMacro, + Call, + EndCall, + Filter, + EndFilter, + Set, + EndSet, + Include, + Import, + Recursive, + Scoped, + With, + EndWith, + Without, + Ignore, + Missing, + Context, + From, + As, + Do, +}; + struct LexerHelper { virtual std::string GetAsString(const CharRange& range) = 0; virtual InternalValue GetAsValue(const CharRange& range, Token::Type type) = 0; - virtual Token::Type GetKeyword(const CharRange& range) = 0; + virtual Keyword GetKeyword(const CharRange& range) = 0; virtual char GetCharAt(size_t pos) = 0; }; @@ -142,6 +202,8 @@ class Lexer { return m_tokens; } + + auto GetHelper() const {return m_helper;} private: bool ProcessNumber(const lexertk::token& token, Token& newToken); @@ -188,6 +250,7 @@ class LexScanner }; LexScanner(const Lexer& lexer) + : m_helper(lexer.GetHelper()) { m_state.m_begin = lexer.GetTokens().begin(); m_state.m_end = lexer.GetTokens().end(); @@ -252,7 +315,27 @@ class LexScanner return type == Token::Type::Eof; } - if (m_state.m_cur->type == type) + return EatIfEqualImpl(tok, [type](const Token& t) {return t.type == type;}); + } + + auto GetAsKeyword(const Token& tok) const + { + return m_helper->GetKeyword(tok.range); + } + + bool EatIfEqual(Keyword kwType, Token* tok = nullptr) + { + if (m_state.m_cur == m_state.m_end) + return false; + + return EatIfEqualImpl(tok, [this, kwType](const Token& t) {return GetAsKeyword(t) == kwType;}); + } + +private: + template + bool EatIfEqualImpl(Token* tok, Fn&& predicate) + { + if (predicate(*m_state.m_cur)) { if (tok) *tok = *m_state.m_cur; @@ -265,6 +348,8 @@ class LexScanner private: State m_state; + LexerHelper* m_helper; + static const Token& EofToken() { static Token eof; @@ -273,6 +358,18 @@ class LexScanner } }; -} // jinja2 +} // namespace jinja2 + +namespace std +{ +template<> +struct hash +{ + size_t operator()(jinja2::Keyword kw) const + { + return std::hash{}(static_cast(kw)); + } +}; +} // namespace std -#endif // LEXER_H +#endif // JINJA2CPP_SRC_LEXER_H diff --git a/src/lexertk.h b/src/lexertk.h index 06a1df9e..c318cc7f 100644 --- a/src/lexertk.h +++ b/src/lexertk.h @@ -38,8 +38,8 @@ */ -#ifndef INCLUDE_LEXERTK_HPP -#define INCLUDE_LEXERTK_HPP +#ifndef JINJA2CPP_SRC_LEXERTK_H +#define JINJA2CPP_SRC_LEXERTK_H #include #include @@ -281,51 +281,7 @@ namespace lexertk } }; -#if 0 - inline void cleanup_escapes(std::string& s) - { - typedef std::string::iterator str_itr_t; - - str_itr_t itr1 = s.begin(); - str_itr_t itr2 = s.begin(); - str_itr_t end = s.end (); - - std::size_t removal_count = 0; - - while (end != itr1) - { - if ('\\' == (*itr1)) - { - ++removal_count; - - if (end == ++itr1) - break; - else if ('\\' != (*itr1)) - { - switch (*itr1) - { - case 'n' : (*itr1) = '\n'; break; - case 'r' : (*itr1) = '\r'; break; - case 't' : (*itr1) = '\t'; break; - } - - continue; - } - } - - if (itr1 != itr2) - { - (*itr2) = (*itr1); - } - - ++itr1; - ++itr2; - } - - s.resize(s.size() - removal_count); - } -#endif - } + } // namespace details struct token { @@ -349,7 +305,8 @@ namespace lexertk token() : type(e_none), - position(std::numeric_limits::max()) + position(std::numeric_limits::max()), + length(0) {} void clear() @@ -498,7 +455,7 @@ namespace lexertk s_end_ = 0; token_list_.clear(); token_itr_ = token_list_.end(); - store_token_itr_ = token_list_.end(); + store_token_itr_ = token_itr_; } inline bool process(const std::basic_string& str) @@ -542,7 +499,7 @@ namespace lexertk inline void begin() { token_itr_ = token_list_.begin(); - store_token_itr_ = token_list_.begin(); + store_token_itr_ = token_itr_; } inline void store() @@ -634,7 +591,7 @@ namespace lexertk scan_operator(); return; } - else if (traits::is_letter(*s_itr_)) + else if (traits::is_letter(*s_itr_) || ('_' == (*s_itr_))) { scan_symbol(); return; @@ -851,14 +808,12 @@ namespace lexertk ++s_itr_; - bool escaped_found = false; bool escaped = false; while (!is_end(s_itr_)) { if (!escaped && ('\\' == *s_itr_)) { - escaped_found = true; escaped = true; ++s_itr_; @@ -869,7 +824,7 @@ namespace lexertk if (endChar == *s_itr_) break; } - else if (escaped) + else escaped = false; ++s_itr_; @@ -1882,6 +1837,6 @@ namespace lexertk token_t current_token_; }; #endif -} +} // namespace lexertk #endif diff --git a/src/out_stream.h b/src/out_stream.h index b667deea..8e3f001b 100644 --- a/src/out_stream.h +++ b/src/out_stream.h @@ -1,34 +1,43 @@ -#ifndef OUT_STREAM_H -#define OUT_STREAM_H +#ifndef JINJA2CPP_SRC_OUT_STREAM_H +#define JINJA2CPP_SRC_OUT_STREAM_H #include "internal_value.h" + #include +#include +#include namespace jinja2 { class OutStream { public: - OutStream(std::function chunkWriter, std::function valueWriter) - : m_bufferWriter(std::move(chunkWriter)) - , m_valueWriter(valueWriter) + struct StreamWriter { - } + virtual ~StreamWriter() {} + + virtual void WriteBuffer(const void* ptr, size_t length) = 0; + virtual void WriteValue(const InternalValue &val) = 0; + }; + + OutStream(std::function writerGetter) + : m_writerGetter(std::move(writerGetter)) + {} void WriteBuffer(const void* ptr, size_t length) { - m_bufferWriter(ptr, length); + m_writerGetter()->WriteBuffer(ptr, length); } void WriteValue(const InternalValue& val) { - m_valueWriter(val); + m_writerGetter()->WriteValue(val); } private: - std::function m_bufferWriter; - std::function m_valueWriter; + std::function m_writerGetter; }; -} // jinja2 -#endif // OUT_STREAM_H +} // namespace jinja2 + +#endif // JINJA2CPP_SRC_OUT_STREAM_H diff --git a/src/render_context.h b/src/render_context.h index 988fa9e2..0239efda 100644 --- a/src/render_context.h +++ b/src/render_context.h @@ -1,35 +1,54 @@ -#ifndef RENDER_CONTEXT_H -#define RENDER_CONTEXT_H +#ifndef JINJA2CPP_SRC_RENDER_CONTEXT_H +#define JINJA2CPP_SRC_RENDER_CONTEXT_H #include "internal_value.h" +#include +#include #include -#include #include +#include namespace jinja2 { template class TemplateImpl; -struct IRendererCallback +struct IRendererCallback : IComparable { + virtual ~IRendererCallback() {} virtual TargetString GetAsTargetString(const InternalValue& val) = 0; - virtual boost::variant>, ErrorInfo>, - nonstd::expected>, ErrorInfoW>> LoadTemplate(const std::string& fileName) const = 0; + virtual OutStream GetStreamOnString(TargetString& str) = 0; + virtual nonstd::variant>, ErrorInfo>, + nonstd::expected>, ErrorInfoW>> LoadTemplate(const std::string& fileName) const = 0; + virtual nonstd::variant>, ErrorInfo>, + nonstd::expected>, ErrorInfoW>> LoadTemplate(const InternalValue& fileName) const = 0; + virtual void ThrowRuntimeError(ErrorCode code, ValuesList extraParams) = 0; }; class RenderContext { public: - RenderContext(const InternalValueMap& extValues, IRendererCallback* rendererCallback) + RenderContext(const InternalValueMap& extValues, const InternalValueMap& globalValues, IRendererCallback* rendererCallback) : m_rendererCallback(rendererCallback) { m_externalScope = &extValues; + m_globalScope = &globalValues; EnterScope(); - (*m_currentScope)["self"] = MapAdapter::CreateAdapter(InternalValueMap()); + (*m_currentScope)["self"] = CreateMapAdapter(InternalValueMap()); + } + + RenderContext(const RenderContext& other) + : m_rendererCallback(other.m_rendererCallback) + , m_externalScope(other.m_externalScope) + , m_globalScope(other.m_globalScope) + , m_boundScope(other.m_boundScope) + , m_scopes(other.m_scopes) + { + m_currentScope = &m_scopes.back(); } InternalValueMap& EnterScope() @@ -50,25 +69,34 @@ class RenderContext auto FindValue(const std::string& val, bool& found) const { - for (auto p = m_scopes.rbegin(); p != m_scopes.rend(); ++ p) + auto finder = [&val, &found](auto& map) mutable { - auto& map = *p; - auto valP = map.find(val); - if (valP != map.end()) - { + auto p = map.find(val); + if (p != map.end()) found = true; + + return p; + }; + + if (m_boundScope) + { + auto valP = finder(*m_boundScope); + if (found) return valP; - } } - auto valP = m_externalScope->find(val); - if (valP != m_externalScope->end()) + + for (auto p = m_scopes.rbegin(); p != m_scopes.rend(); ++ p) { - found = true; - return valP; + auto valP = finder(*p); + if (found) + return valP; } - found = false; - return m_externalScope->end(); + auto valP = finder(*m_externalScope); + if (found) + return valP; + + return finder(*m_globalScope); } auto& GetCurrentScope() const @@ -91,17 +119,64 @@ class RenderContext RenderContext Clone(bool includeCurrentContext) const { if (!includeCurrentContext) - return RenderContext(*m_externalScope, m_rendererCallback); + return RenderContext(m_emptyScope, *m_globalScope, m_rendererCallback); return RenderContext(*this); } + + void BindScope(InternalValueMap* scope) + { + m_boundScope = scope; + } + + bool IsEqual(const RenderContext& other) const + { + if (!IsEqual(m_rendererCallback, other.m_rendererCallback)) + return false; + if (!IsEqual(this->m_currentScope, other.m_currentScope)) + return false; + if (!IsEqual(m_externalScope, other.m_externalScope)) + return false; + if (!IsEqual(m_globalScope, other.m_globalScope)) + return false; + if (!IsEqual(m_boundScope, other.m_boundScope)) + return false; + if (m_emptyScope != other.m_emptyScope) + return false; + if (m_scopes != other.m_scopes) + return false; + return true; + } + private: - InternalValueMap* m_currentScope; - const InternalValueMap* m_externalScope; - std::list m_scopes; - IRendererCallback* m_rendererCallback; + bool IsEqual(const IRendererCallback* lhs, const IRendererCallback* rhs) const + { + if (lhs && rhs) + return lhs->IsEqual(*rhs); + if ((!lhs && rhs) || (lhs && !rhs)) + return false; + return true; + } + + bool IsEqual(const InternalValueMap* lhs, const InternalValueMap* rhs) const + { + if (lhs && rhs) + return *lhs == *rhs; + if ((!lhs && rhs) || (lhs && !rhs)) + return false; + return true; + } + +private: + IRendererCallback* m_rendererCallback{}; + InternalValueMap* m_currentScope{}; + const InternalValueMap* m_externalScope{}; + const InternalValueMap* m_globalScope{}; + const InternalValueMap* m_boundScope{}; + InternalValueMap m_emptyScope; + std::deque m_scopes; }; -} // jinja2 +} // namespace jinja2 -#endif // RENDER_CONTEXT_H +#endif // JINJA2CPP_SRC_RENDER_CONTEXT_H diff --git a/src/renderer.h b/src/renderer.h index e3b55779..9c08d237 100644 --- a/src/renderer.h +++ b/src/renderer.h @@ -1,11 +1,14 @@ -#ifndef RENDERER_H -#define RENDERER_H +#ifndef JINJA2CPP_SRC_RENDERER_H +#define JINJA2CPP_SRC_RENDERER_H -#include "jinja2cpp/value.h" #include "out_stream.h" #include "lexertk.h" #include "expression_evaluator.h" #include "render_context.h" +#include "ast_visitor.h" + +#include +#include #include #include @@ -14,21 +17,41 @@ namespace jinja2 { -class RendererBase +class IRendererBase : public virtual IComparable { public: - virtual ~RendererBase() {} + virtual ~IRendererBase() = default; virtual void Render(OutStream& os, RenderContext& values) = 0; }; -using RendererPtr = std::shared_ptr; +class VisitableRendererBase : public IRendererBase, public VisitableStatement +{ +}; + +using RendererPtr = std::shared_ptr; + +inline bool operator==(const RendererPtr& lhs, const RendererPtr& rhs) +{ + if (lhs && rhs && !lhs->IsEqual(*rhs)) + return false; + if ((lhs && !rhs) || (!lhs && rhs)) + return false; + return true; +} + +inline bool operator!=(const RendererPtr& lhs, const RendererPtr& rhs) +{ + return !(lhs == rhs); +} -class ComposedRenderer : public RendererBase +class ComposedRenderer : public VisitableRendererBase { public: + VISITABLE_STATEMENT(); + void AddRenderer(RendererPtr r) { - m_renderers.push_back(r); + m_renderers.push_back(std::move(r)); } void Render(OutStream& os, RenderContext& values) override { @@ -36,13 +59,23 @@ class ComposedRenderer : public RendererBase r->Render(os, values); } + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return m_renderers == val->m_renderers; + } + private: std::vector m_renderers; }; -class RawTextRenderer : public RendererBase +class RawTextRenderer : public VisitableRendererBase { public: + VISITABLE_STATEMENT(); + RawTextRenderer(const void* ptr, size_t len) : m_ptr(ptr) , m_length(len) @@ -53,16 +86,28 @@ class RawTextRenderer : public RendererBase { os.WriteBuffer(m_ptr, m_length); } + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_ptr != val->m_ptr) + return false; + return m_length == val->m_length; + } private: - const void* m_ptr; - size_t m_length; + const void* m_ptr{}; + size_t m_length{}; }; -class ExpressionRenderer : public RendererBase +class ExpressionRenderer : public VisitableRendererBase { public: - ExpressionRenderer(ExpressionEvaluatorPtr<> expr) - : m_expression(expr) + VISITABLE_STATEMENT(); + + explicit ExpressionRenderer(ExpressionEvaluatorPtr<> expr) + : m_expression(std::move(expr)) { } @@ -70,9 +115,17 @@ class ExpressionRenderer : public RendererBase { m_expression->Render(os, values); } + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return m_expression == val->m_expression; + } private: ExpressionEvaluatorPtr<> m_expression; }; -} // jinja2 +} // namespace jinja2 -#endif // RENDERER_H +#endif // JINJA2CPP_SRC_RENDERER_H diff --git a/src/robin_hood.h b/src/robin_hood.h new file mode 100644 index 00000000..b4e0fbc5 --- /dev/null +++ b/src/robin_hood.h @@ -0,0 +1,2544 @@ +// ______ _____ ______ _________ +// ______________ ___ /_ ___(_)_______ ___ /_ ______ ______ ______ / +// __ ___/_ __ \__ __ \__ / __ __ \ __ __ \_ __ \_ __ \_ __ / +// _ / / /_/ /_ /_/ /_ / _ / / / _ / / // /_/ // /_/ // /_/ / +// /_/ \____/ /_.___/ /_/ /_/ /_/ ________/_/ /_/ \____/ \____/ \__,_/ +// _/_____/ +// +// Fast & memory efficient hashtable based on robin hood hashing for C++11/14/17/20 +// https://github.com/martinus/robin-hood-hashing +// +// Licensed under the MIT License . +// SPDX-License-Identifier: MIT +// Copyright (c) 2018-2021 Martin Ankerl +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef ROBIN_HOOD_H_INCLUDED +#define ROBIN_HOOD_H_INCLUDED + +// see https://semver.org/ +#define ROBIN_HOOD_VERSION_MAJOR 3 // for incompatible API changes +#define ROBIN_HOOD_VERSION_MINOR 11 // for adding functionality in a backwards-compatible manner +#define ROBIN_HOOD_VERSION_PATCH 5 // for backwards-compatible bug fixes + +#include +#include +#include +#include +#include +#include // only to support hash of smart pointers +#include +#include +#include +#include +#if __cplusplus >= 201703L +# include +#endif + +// #define ROBIN_HOOD_LOG_ENABLED +#ifdef ROBIN_HOOD_LOG_ENABLED +# include +# define ROBIN_HOOD_LOG(...) \ + std::cout << __FUNCTION__ << "@" << __LINE__ << ": " << __VA_ARGS__ << std::endl; +#else +# define ROBIN_HOOD_LOG(x) +#endif + +// #define ROBIN_HOOD_TRACE_ENABLED +#ifdef ROBIN_HOOD_TRACE_ENABLED +# include +# define ROBIN_HOOD_TRACE(...) \ + std::cout << __FUNCTION__ << "@" << __LINE__ << ": " << __VA_ARGS__ << std::endl; +#else +# define ROBIN_HOOD_TRACE(x) +#endif + +// #define ROBIN_HOOD_COUNT_ENABLED +#ifdef ROBIN_HOOD_COUNT_ENABLED +# include +# define ROBIN_HOOD_COUNT(x) ++counts().x; +namespace robin_hood { +struct Counts { + uint64_t shiftUp{}; + uint64_t shiftDown{}; +}; +inline std::ostream& operator<<(std::ostream& os, Counts const& c) { + return os << c.shiftUp << " shiftUp" << std::endl << c.shiftDown << " shiftDown" << std::endl; +} + +static Counts& counts() { + static Counts counts{}; + return counts; +} +} // namespace robin_hood +#else +# define ROBIN_HOOD_COUNT(x) +#endif + +// all non-argument macros should use this facility. See +// https://www.fluentcpp.com/2019/05/28/better-macros-better-flags/ +#define ROBIN_HOOD(x) ROBIN_HOOD_PRIVATE_DEFINITION_##x() + +// mark unused members with this macro +#define ROBIN_HOOD_UNUSED(identifier) + +// bitness +#if SIZE_MAX == UINT32_MAX +# define ROBIN_HOOD_PRIVATE_DEFINITION_BITNESS() 32 +#elif SIZE_MAX == UINT64_MAX +# define ROBIN_HOOD_PRIVATE_DEFINITION_BITNESS() 64 +#else +# error Unsupported bitness +#endif + +// endianess +#ifdef _MSC_VER +# define ROBIN_HOOD_PRIVATE_DEFINITION_LITTLE_ENDIAN() 1 +# define ROBIN_HOOD_PRIVATE_DEFINITION_BIG_ENDIAN() 0 +#else +# define ROBIN_HOOD_PRIVATE_DEFINITION_LITTLE_ENDIAN() \ + (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) +# define ROBIN_HOOD_PRIVATE_DEFINITION_BIG_ENDIAN() (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) +#endif + +// inline +#ifdef _MSC_VER +# define ROBIN_HOOD_PRIVATE_DEFINITION_NOINLINE() __declspec(noinline) +#else +# define ROBIN_HOOD_PRIVATE_DEFINITION_NOINLINE() __attribute__((noinline)) +#endif + +// exceptions +#if !defined(__cpp_exceptions) && !defined(__EXCEPTIONS) && !defined(_CPPUNWIND) +# define ROBIN_HOOD_PRIVATE_DEFINITION_HAS_EXCEPTIONS() 0 +#else +# define ROBIN_HOOD_PRIVATE_DEFINITION_HAS_EXCEPTIONS() 1 +#endif + +// count leading/trailing bits +#if !defined(ROBIN_HOOD_DISABLE_INTRINSICS) +# ifdef _MSC_VER +# if ROBIN_HOOD(BITNESS) == 32 +# define ROBIN_HOOD_PRIVATE_DEFINITION_BITSCANFORWARD() _BitScanForward +# else +# define ROBIN_HOOD_PRIVATE_DEFINITION_BITSCANFORWARD() _BitScanForward64 +# endif +# include +# pragma intrinsic(ROBIN_HOOD(BITSCANFORWARD)) +# define ROBIN_HOOD_COUNT_TRAILING_ZEROES(x) \ + [](size_t mask) noexcept -> int { \ + unsigned long index; \ + return ROBIN_HOOD(BITSCANFORWARD)(&index, mask) ? static_cast(index) \ + : ROBIN_HOOD(BITNESS); \ + }(x) +# else +# if ROBIN_HOOD(BITNESS) == 32 +# define ROBIN_HOOD_PRIVATE_DEFINITION_CTZ() __builtin_ctzl +# define ROBIN_HOOD_PRIVATE_DEFINITION_CLZ() __builtin_clzl +# else +# define ROBIN_HOOD_PRIVATE_DEFINITION_CTZ() __builtin_ctzll +# define ROBIN_HOOD_PRIVATE_DEFINITION_CLZ() __builtin_clzll +# endif +# define ROBIN_HOOD_COUNT_LEADING_ZEROES(x) ((x) ? ROBIN_HOOD(CLZ)(x) : ROBIN_HOOD(BITNESS)) +# define ROBIN_HOOD_COUNT_TRAILING_ZEROES(x) ((x) ? ROBIN_HOOD(CTZ)(x) : ROBIN_HOOD(BITNESS)) +# endif +#endif + +// fallthrough +#ifndef __has_cpp_attribute // For backwards compatibility +# define __has_cpp_attribute(x) 0 +#endif +#if __has_cpp_attribute(clang::fallthrough) +# define ROBIN_HOOD_PRIVATE_DEFINITION_FALLTHROUGH() [[clang::fallthrough]] +#elif __has_cpp_attribute(gnu::fallthrough) +# define ROBIN_HOOD_PRIVATE_DEFINITION_FALLTHROUGH() [[gnu::fallthrough]] +#else +# define ROBIN_HOOD_PRIVATE_DEFINITION_FALLTHROUGH() +#endif + +// likely/unlikely +#ifdef _MSC_VER +# define ROBIN_HOOD_LIKELY(condition) condition +# define ROBIN_HOOD_UNLIKELY(condition) condition +#else +# define ROBIN_HOOD_LIKELY(condition) __builtin_expect(condition, 1) +# define ROBIN_HOOD_UNLIKELY(condition) __builtin_expect(condition, 0) +#endif + +// detect if native wchar_t type is availiable in MSVC +#ifdef _MSC_VER +# ifdef _NATIVE_WCHAR_T_DEFINED +# define ROBIN_HOOD_PRIVATE_DEFINITION_HAS_NATIVE_WCHART() 1 +# else +# define ROBIN_HOOD_PRIVATE_DEFINITION_HAS_NATIVE_WCHART() 0 +# endif +#else +# define ROBIN_HOOD_PRIVATE_DEFINITION_HAS_NATIVE_WCHART() 1 +#endif + +// detect if MSVC supports the pair(std::piecewise_construct_t,...) consructor being constexpr +#ifdef _MSC_VER +# if _MSC_VER <= 1900 +# define ROBIN_HOOD_PRIVATE_DEFINITION_BROKEN_CONSTEXPR() 1 +# else +# define ROBIN_HOOD_PRIVATE_DEFINITION_BROKEN_CONSTEXPR() 0 +# endif +#else +# define ROBIN_HOOD_PRIVATE_DEFINITION_BROKEN_CONSTEXPR() 0 +#endif + +// workaround missing "is_trivially_copyable" in g++ < 5.0 +// See https://stackoverflow.com/a/31798726/48181 +#if defined(__GNUC__) && __GNUC__ < 5 && !defined(__clang__) +# define ROBIN_HOOD_IS_TRIVIALLY_COPYABLE(...) __has_trivial_copy(__VA_ARGS__) +#else +# define ROBIN_HOOD_IS_TRIVIALLY_COPYABLE(...) std::is_trivially_copyable<__VA_ARGS__>::value +#endif + +// helpers for C++ versions, see https://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html +#define ROBIN_HOOD_PRIVATE_DEFINITION_CXX() __cplusplus +#define ROBIN_HOOD_PRIVATE_DEFINITION_CXX98() 199711L +#define ROBIN_HOOD_PRIVATE_DEFINITION_CXX11() 201103L +#define ROBIN_HOOD_PRIVATE_DEFINITION_CXX14() 201402L +#define ROBIN_HOOD_PRIVATE_DEFINITION_CXX17() 201703L + +#if ROBIN_HOOD(CXX) >= ROBIN_HOOD(CXX17) +# define ROBIN_HOOD_PRIVATE_DEFINITION_NODISCARD() [[nodiscard]] +#else +# define ROBIN_HOOD_PRIVATE_DEFINITION_NODISCARD() +#endif + +namespace robin_hood { + +#if ROBIN_HOOD(CXX) >= ROBIN_HOOD(CXX14) +# define ROBIN_HOOD_STD std +#else + +// c++11 compatibility layer +namespace ROBIN_HOOD_STD { +template +struct alignment_of + : std::integral_constant::type)> {}; + +template +class integer_sequence { +public: + using value_type = T; + static_assert(std::is_integral::value, "not integral type"); + static constexpr std::size_t size() noexcept { + return sizeof...(Ints); + } +}; +template +using index_sequence = integer_sequence; + +namespace detail_ { +template +struct IntSeqImpl { + using TValue = T; + static_assert(std::is_integral::value, "not integral type"); + static_assert(Begin >= 0 && Begin < End, "unexpected argument (Begin<0 || Begin<=End)"); + + template + struct IntSeqCombiner; + + template + struct IntSeqCombiner, integer_sequence> { + using TResult = integer_sequence; + }; + + using TResult = + typename IntSeqCombiner::TResult, + typename IntSeqImpl::TResult>::TResult; +}; + +template +struct IntSeqImpl { + using TValue = T; + static_assert(std::is_integral::value, "not integral type"); + static_assert(Begin >= 0, "unexpected argument (Begin<0)"); + using TResult = integer_sequence; +}; + +template +struct IntSeqImpl { + using TValue = T; + static_assert(std::is_integral::value, "not integral type"); + static_assert(Begin >= 0, "unexpected argument (Begin<0)"); + using TResult = integer_sequence; +}; +} // namespace detail_ + +template +using make_integer_sequence = typename detail_::IntSeqImpl::TResult; + +template +using make_index_sequence = make_integer_sequence; + +template +using index_sequence_for = make_index_sequence; + +} // namespace ROBIN_HOOD_STD + +#endif + +namespace detail { + +// make sure we static_cast to the correct type for hash_int +#if ROBIN_HOOD(BITNESS) == 64 +using SizeT = uint64_t; +#else +using SizeT = uint32_t; +#endif + +template +T rotr(T x, unsigned k) { + return (x >> k) | (x << (8U * sizeof(T) - k)); +} + +// This cast gets rid of warnings like "cast from 'uint8_t*' {aka 'unsigned char*'} to +// 'uint64_t*' {aka 'long unsigned int*'} increases required alignment of target type". Use with +// care! +template +inline T reinterpret_cast_no_cast_align_warning(void* ptr) noexcept { + return reinterpret_cast(ptr); +} + +template +inline T reinterpret_cast_no_cast_align_warning(void const* ptr) noexcept { + return reinterpret_cast(ptr); +} + +// make sure this is not inlined as it is slow and dramatically enlarges code, thus making other +// inlinings more difficult. Throws are also generally the slow path. +template +[[noreturn]] ROBIN_HOOD(NOINLINE) +#if ROBIN_HOOD(HAS_EXCEPTIONS) + void doThrow(Args&&... args) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay) + throw E(std::forward(args)...); +} +#else + void doThrow(Args&&... ROBIN_HOOD_UNUSED(args) /*unused*/) { + abort(); +} +#endif + +template +T* assertNotNull(T* t, Args&&... args) { + if (ROBIN_HOOD_UNLIKELY(nullptr == t)) { + doThrow(std::forward(args)...); + } + return t; +} + +template +inline T unaligned_load(void const* ptr) noexcept { + // using memcpy so we don't get into unaligned load problems. + // compiler should optimize this very well anyways. + T t; + std::memcpy(&t, ptr, sizeof(T)); + return t; +} + +// Allocates bulks of memory for objects of type T. This deallocates the memory in the destructor, +// and keeps a linked list of the allocated memory around. Overhead per allocation is the size of a +// pointer. +template +class BulkPoolAllocator { +public: + BulkPoolAllocator() noexcept = default; + + // does not copy anything, just creates a new allocator. + BulkPoolAllocator(const BulkPoolAllocator& ROBIN_HOOD_UNUSED(o) /*unused*/) noexcept + : mHead(nullptr) + , mListForFree(nullptr) {} + + BulkPoolAllocator(BulkPoolAllocator&& o) noexcept + : mHead(o.mHead) + , mListForFree(o.mListForFree) { + o.mListForFree = nullptr; + o.mHead = nullptr; + } + + BulkPoolAllocator& operator=(BulkPoolAllocator&& o) noexcept { + reset(); + mHead = o.mHead; + mListForFree = o.mListForFree; + o.mListForFree = nullptr; + o.mHead = nullptr; + return *this; + } + + BulkPoolAllocator& + // NOLINTNEXTLINE(bugprone-unhandled-self-assignment,cert-oop54-cpp) + operator=(const BulkPoolAllocator& ROBIN_HOOD_UNUSED(o) /*unused*/) noexcept { + // does not do anything + return *this; + } + + ~BulkPoolAllocator() noexcept { + reset(); + } + + // Deallocates all allocated memory. + void reset() noexcept { + while (mListForFree) { + T* tmp = *mListForFree; + ROBIN_HOOD_LOG("std::free") + std::free(mListForFree); + mListForFree = reinterpret_cast_no_cast_align_warning(tmp); + } + mHead = nullptr; + } + + // allocates, but does NOT initialize. Use in-place new constructor, e.g. + // T* obj = pool.allocate(); + // ::new (static_cast(obj)) T(); + T* allocate() { + T* tmp = mHead; + if (!tmp) { + tmp = performAllocation(); + } + + mHead = *reinterpret_cast_no_cast_align_warning(tmp); + return tmp; + } + + // does not actually deallocate but puts it in store. + // make sure you have already called the destructor! e.g. with + // obj->~T(); + // pool.deallocate(obj); + void deallocate(T* obj) noexcept { + *reinterpret_cast_no_cast_align_warning(obj) = mHead; + mHead = obj; + } + + // Adds an already allocated block of memory to the allocator. This allocator is from now on + // responsible for freeing the data (with free()). If the provided data is not large enough to + // make use of, it is immediately freed. Otherwise it is reused and freed in the destructor. + void addOrFree(void* ptr, const size_t numBytes) noexcept { + // calculate number of available elements in ptr + if (numBytes < ALIGNMENT + ALIGNED_SIZE) { + // not enough data for at least one element. Free and return. + ROBIN_HOOD_LOG("std::free") + std::free(ptr); + } else { + ROBIN_HOOD_LOG("add to buffer") + add(ptr, numBytes); + } + } + + void swap(BulkPoolAllocator& other) noexcept { + using std::swap; + swap(mHead, other.mHead); + swap(mListForFree, other.mListForFree); + } + +private: + // iterates the list of allocated memory to calculate how many to alloc next. + // Recalculating this each time saves us a size_t member. + // This ignores the fact that memory blocks might have been added manually with addOrFree. In + // practice, this should not matter much. + ROBIN_HOOD(NODISCARD) size_t calcNumElementsToAlloc() const noexcept { + auto tmp = mListForFree; + size_t numAllocs = MinNumAllocs; + + while (numAllocs * 2 <= MaxNumAllocs && tmp) { + auto x = reinterpret_cast(tmp); + tmp = *x; + numAllocs *= 2; + } + + return numAllocs; + } + + // WARNING: Underflow if numBytes < ALIGNMENT! This is guarded in addOrFree(). + void add(void* ptr, const size_t numBytes) noexcept { + const size_t numElements = (numBytes - ALIGNMENT) / ALIGNED_SIZE; + + auto data = reinterpret_cast(ptr); + + // link free list + auto x = reinterpret_cast(data); + *x = mListForFree; + mListForFree = data; + + // create linked list for newly allocated data + auto* const headT = + reinterpret_cast_no_cast_align_warning(reinterpret_cast(ptr) + ALIGNMENT); + + auto* const head = reinterpret_cast(headT); + + // Visual Studio compiler automatically unrolls this loop, which is pretty cool + for (size_t i = 0; i < numElements; ++i) { + *reinterpret_cast_no_cast_align_warning(head + i * ALIGNED_SIZE) = + head + (i + 1) * ALIGNED_SIZE; + } + + // last one points to 0 + *reinterpret_cast_no_cast_align_warning(head + (numElements - 1) * ALIGNED_SIZE) = + mHead; + mHead = headT; + } + + // Called when no memory is available (mHead == 0). + // Don't inline this slow path. + ROBIN_HOOD(NOINLINE) T* performAllocation() { + size_t const numElementsToAlloc = calcNumElementsToAlloc(); + + // alloc new memory: [prev |T, T, ... T] + size_t const bytes = ALIGNMENT + ALIGNED_SIZE * numElementsToAlloc; + ROBIN_HOOD_LOG("std::malloc " << bytes << " = " << ALIGNMENT << " + " << ALIGNED_SIZE + << " * " << numElementsToAlloc) + add(assertNotNull(std::malloc(bytes)), bytes); + return mHead; + } + + // enforce byte alignment of the T's +#if ROBIN_HOOD(CXX) >= ROBIN_HOOD(CXX14) + static constexpr size_t ALIGNMENT = + (std::max)(std::alignment_of::value, std::alignment_of::value); +#else + static const size_t ALIGNMENT = + (ROBIN_HOOD_STD::alignment_of::value > ROBIN_HOOD_STD::alignment_of::value) + ? ROBIN_HOOD_STD::alignment_of::value + : +ROBIN_HOOD_STD::alignment_of::value; // the + is for walkarround +#endif + + static constexpr size_t ALIGNED_SIZE = ((sizeof(T) - 1) / ALIGNMENT + 1) * ALIGNMENT; + + static_assert(MinNumAllocs >= 1, "MinNumAllocs"); + static_assert(MaxNumAllocs >= MinNumAllocs, "MaxNumAllocs"); + static_assert(ALIGNED_SIZE >= sizeof(T*), "ALIGNED_SIZE"); + static_assert(0 == (ALIGNED_SIZE % sizeof(T*)), "ALIGNED_SIZE mod"); + static_assert(ALIGNMENT >= sizeof(T*), "ALIGNMENT"); + + T* mHead{nullptr}; + T** mListForFree{nullptr}; +}; + +template +struct NodeAllocator; + +// dummy allocator that does nothing +template +struct NodeAllocator { + + // we are not using the data, so just free it. + void addOrFree(void* ptr, size_t ROBIN_HOOD_UNUSED(numBytes) /*unused*/) noexcept { + ROBIN_HOOD_LOG("std::free") + std::free(ptr); + } +}; + +template +struct NodeAllocator : public BulkPoolAllocator {}; + +// c++14 doesn't have is_nothrow_swappable, and clang++ 6.0.1 doesn't like it either, so I'm making +// my own here. +namespace swappable { +#if ROBIN_HOOD(CXX) < ROBIN_HOOD(CXX17) +using std::swap; +template +struct nothrow { + static const bool value = noexcept(swap(std::declval(), std::declval())); +}; +#else +template +struct nothrow { + static const bool value = std::is_nothrow_swappable::value; +}; +#endif +} // namespace swappable + +} // namespace detail + +struct is_transparent_tag {}; + +// A custom pair implementation is used in the map because std::pair is not is_trivially_copyable, +// which means it would not be allowed to be used in std::memcpy. This struct is copyable, which is +// also tested. +template +struct pair { + using first_type = T1; + using second_type = T2; + + template ::value && + std::is_default_constructible::value>::type> + constexpr pair() noexcept(noexcept(U1()) && noexcept(U2())) + : first() + , second() {} + + // pair constructors are explicit so we don't accidentally call this ctor when we don't have to. + explicit constexpr pair(std::pair const& o) noexcept( + noexcept(T1(std::declval())) && noexcept(T2(std::declval()))) + : first(o.first) + , second(o.second) {} + + // pair constructors are explicit so we don't accidentally call this ctor when we don't have to. + explicit constexpr pair(std::pair&& o) noexcept(noexcept( + T1(std::move(std::declval()))) && noexcept(T2(std::move(std::declval())))) + : first(std::move(o.first)) + , second(std::move(o.second)) {} + + constexpr pair(T1&& a, T2&& b) noexcept(noexcept( + T1(std::move(std::declval()))) && noexcept(T2(std::move(std::declval())))) + : first(std::move(a)) + , second(std::move(b)) {} + + template + constexpr pair(U1&& a, U2&& b) noexcept(noexcept(T1(std::forward( + std::declval()))) && noexcept(T2(std::forward(std::declval())))) + : first(std::forward(a)) + , second(std::forward(b)) {} + + template + // MSVC 2015 produces error "C2476: ‘constexpr’ constructor does not initialize all members" + // if this constructor is constexpr +#if !ROBIN_HOOD(BROKEN_CONSTEXPR) + constexpr +#endif + pair(std::piecewise_construct_t /*unused*/, std::tuple a, + std::tuple + b) noexcept(noexcept(pair(std::declval&>(), + std::declval&>(), + ROBIN_HOOD_STD::index_sequence_for(), + ROBIN_HOOD_STD::index_sequence_for()))) + : pair(a, b, ROBIN_HOOD_STD::index_sequence_for(), + ROBIN_HOOD_STD::index_sequence_for()) { + } + + // constructor called from the std::piecewise_construct_t ctor + template + pair(std::tuple& a, std::tuple& b, ROBIN_HOOD_STD::index_sequence /*unused*/, ROBIN_HOOD_STD::index_sequence /*unused*/) noexcept( + noexcept(T1(std::forward(std::get( + std::declval&>()))...)) && noexcept(T2(std:: + forward(std::get( + std::declval&>()))...))) + : first(std::forward(std::get(a))...) + , second(std::forward(std::get(b))...) { + // make visual studio compiler happy about warning about unused a & b. + // Visual studio's pair implementation disables warning 4100. + (void)a; + (void)b; + } + + void swap(pair& o) noexcept((detail::swappable::nothrow::value) && + (detail::swappable::nothrow::value)) { + using std::swap; + swap(first, o.first); + swap(second, o.second); + } + + T1 first; // NOLINT(misc-non-private-member-variables-in-classes) + T2 second; // NOLINT(misc-non-private-member-variables-in-classes) +}; + +template +inline void swap(pair& a, pair& b) noexcept( + noexcept(std::declval&>().swap(std::declval&>()))) { + a.swap(b); +} + +template +inline constexpr bool operator==(pair const& x, pair const& y) { + return (x.first == y.first) && (x.second == y.second); +} +template +inline constexpr bool operator!=(pair const& x, pair const& y) { + return !(x == y); +} +template +inline constexpr bool operator<(pair const& x, pair const& y) noexcept(noexcept( + std::declval() < std::declval()) && noexcept(std::declval() < + std::declval())) { + return x.first < y.first || (!(y.first < x.first) && x.second < y.second); +} +template +inline constexpr bool operator>(pair const& x, pair const& y) { + return y < x; +} +template +inline constexpr bool operator<=(pair const& x, pair const& y) { + return !(x > y); +} +template +inline constexpr bool operator>=(pair const& x, pair const& y) { + return !(x < y); +} + +inline size_t hash_bytes(void const* ptr, size_t len) noexcept { + static constexpr uint64_t m = UINT64_C(0xc6a4a7935bd1e995); + static constexpr uint64_t seed = UINT64_C(0xe17a1465); + static constexpr unsigned int r = 47; + + auto const* const data64 = static_cast(ptr); + uint64_t h = seed ^ (len * m); + + size_t const n_blocks = len / 8; + for (size_t i = 0; i < n_blocks; ++i) { + auto k = detail::unaligned_load(data64 + i); + + k *= m; + k ^= k >> r; + k *= m; + + h ^= k; + h *= m; + } + + auto const* const data8 = reinterpret_cast(data64 + n_blocks); + switch (len & 7U) { + case 7: + h ^= static_cast(data8[6]) << 48U; + ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH + case 6: + h ^= static_cast(data8[5]) << 40U; + ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH + case 5: + h ^= static_cast(data8[4]) << 32U; + ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH + case 4: + h ^= static_cast(data8[3]) << 24U; + ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH + case 3: + h ^= static_cast(data8[2]) << 16U; + ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH + case 2: + h ^= static_cast(data8[1]) << 8U; + ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH + case 1: + h ^= static_cast(data8[0]); + h *= m; + ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH + default: + break; + } + + h ^= h >> r; + + // not doing the final step here, because this will be done by keyToIdx anyways + // h *= m; + // h ^= h >> r; + return static_cast(h); +} + +inline size_t hash_int(uint64_t x) noexcept { + // tried lots of different hashes, let's stick with murmurhash3. It's simple, fast, well tested, + // and doesn't need any special 128bit operations. + x ^= x >> 33U; + x *= UINT64_C(0xff51afd7ed558ccd); + x ^= x >> 33U; + + // not doing the final step here, because this will be done by keyToIdx anyways + // x *= UINT64_C(0xc4ceb9fe1a85ec53); + // x ^= x >> 33U; + return static_cast(x); +} + +// A thin wrapper around std::hash, performing an additional simple mixing step of the result. +template +struct hash : public std::hash { + size_t operator()(T const& obj) const + noexcept(noexcept(std::declval>().operator()(std::declval()))) { + // call base hash + auto result = std::hash::operator()(obj); + // return mixed of that, to be save against identity has + return hash_int(static_cast(result)); + } +}; + +template +struct hash> { + size_t operator()(std::basic_string const& str) const noexcept { + return hash_bytes(str.data(), sizeof(CharT) * str.size()); + } +}; + +#if ROBIN_HOOD(CXX) >= ROBIN_HOOD(CXX17) +template +struct hash> { + size_t operator()(std::basic_string_view const& sv) const noexcept { + return hash_bytes(sv.data(), sizeof(CharT) * sv.size()); + } +}; +#endif + +template +struct hash { + size_t operator()(T* ptr) const noexcept { + return hash_int(reinterpret_cast(ptr)); + } +}; + +template +struct hash> { + size_t operator()(std::unique_ptr const& ptr) const noexcept { + return hash_int(reinterpret_cast(ptr.get())); + } +}; + +template +struct hash> { + size_t operator()(std::shared_ptr const& ptr) const noexcept { + return hash_int(reinterpret_cast(ptr.get())); + } +}; + +template +struct hash::value>::type> { + size_t operator()(Enum e) const noexcept { + using Underlying = typename std::underlying_type::type; + return hash{}(static_cast(e)); + } +}; + +#define ROBIN_HOOD_HASH_INT(T) \ + template <> \ + struct hash { \ + size_t operator()(T const& obj) const noexcept { \ + return hash_int(static_cast(obj)); \ + } \ + } + +#if defined(__GNUC__) && !defined(__clang__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wuseless-cast" +#endif +// see https://en.cppreference.com/w/cpp/utility/hash +ROBIN_HOOD_HASH_INT(bool); +ROBIN_HOOD_HASH_INT(char); +ROBIN_HOOD_HASH_INT(signed char); +ROBIN_HOOD_HASH_INT(unsigned char); +ROBIN_HOOD_HASH_INT(char16_t); +ROBIN_HOOD_HASH_INT(char32_t); +#if ROBIN_HOOD(HAS_NATIVE_WCHART) +ROBIN_HOOD_HASH_INT(wchar_t); +#endif +ROBIN_HOOD_HASH_INT(short); +ROBIN_HOOD_HASH_INT(unsigned short); +ROBIN_HOOD_HASH_INT(int); +ROBIN_HOOD_HASH_INT(unsigned int); +ROBIN_HOOD_HASH_INT(long); +ROBIN_HOOD_HASH_INT(long long); +ROBIN_HOOD_HASH_INT(unsigned long); +ROBIN_HOOD_HASH_INT(unsigned long long); +#if defined(__GNUC__) && !defined(__clang__) +# pragma GCC diagnostic pop +#endif +namespace detail { + +template +struct void_type { + using type = void; +}; + +template +struct has_is_transparent : public std::false_type {}; + +template +struct has_is_transparent::type> + : public std::true_type {}; + +// using wrapper classes for hash and key_equal prevents the diamond problem when the same type +// is used. see https://stackoverflow.com/a/28771920/48181 +template +struct WrapHash : public T { + WrapHash() = default; + explicit WrapHash(T const& o) noexcept(noexcept(T(std::declval()))) + : T(o) {} +}; + +template +struct WrapKeyEqual : public T { + WrapKeyEqual() = default; + explicit WrapKeyEqual(T const& o) noexcept(noexcept(T(std::declval()))) + : T(o) {} +}; + +// A highly optimized hashmap implementation, using the Robin Hood algorithm. +// +// In most cases, this map should be usable as a drop-in replacement for std::unordered_map, but +// be about 2x faster in most cases and require much less allocations. +// +// This implementation uses the following memory layout: +// +// [Node, Node, ... Node | info, info, ... infoSentinel ] +// +// * Node: either a DataNode that directly has the std::pair as member, +// or a DataNode with a pointer to std::pair. Which DataNode representation to use +// depends on how fast the swap() operation is. Heuristically, this is automatically choosen +// based on sizeof(). there are always 2^n Nodes. +// +// * info: Each Node in the map has a corresponding info byte, so there are 2^n info bytes. +// Each byte is initialized to 0, meaning the corresponding Node is empty. Set to 1 means the +// corresponding node contains data. Set to 2 means the corresponding Node is filled, but it +// actually belongs to the previous position and was pushed out because that place is already +// taken. +// +// * infoSentinel: Sentinel byte set to 1, so that iterator's ++ can stop at end() without the +// need for a idx variable. +// +// According to STL, order of templates has effect on throughput. That's why I've moved the +// boolean to the front. +// https://www.reddit.com/r/cpp/comments/ahp6iu/compile_time_binary_size_reductions_and_cs_future/eeguck4/ +template +class Table + : public WrapHash, + public WrapKeyEqual, + detail::NodeAllocator< + typename std::conditional< + std::is_void::value, Key, + robin_hood::pair::type, T>>::type, + 4, 16384, IsFlat> { +public: + static constexpr bool is_flat = IsFlat; + static constexpr bool is_map = !std::is_void::value; + static constexpr bool is_set = !is_map; + static constexpr bool is_transparent = + has_is_transparent::value && has_is_transparent::value; + + using key_type = Key; + using mapped_type = T; + using value_type = typename std::conditional< + is_set, Key, + robin_hood::pair::type, T>>::type; + using size_type = size_t; + using hasher = Hash; + using key_equal = KeyEqual; + using Self = Table; + +private: + static_assert(MaxLoadFactor100 > 10 && MaxLoadFactor100 < 100, + "MaxLoadFactor100 needs to be >10 && < 100"); + + using WHash = WrapHash; + using WKeyEqual = WrapKeyEqual; + + // configuration defaults + + // make sure we have 8 elements, needed to quickly rehash mInfo + static constexpr size_t InitialNumElements = sizeof(uint64_t); + static constexpr uint32_t InitialInfoNumBits = 5; + static constexpr uint8_t InitialInfoInc = 1U << InitialInfoNumBits; + static constexpr size_t InfoMask = InitialInfoInc - 1U; + static constexpr uint8_t InitialInfoHashShift = 0; + using DataPool = detail::NodeAllocator; + + // type needs to be wider than uint8_t. + using InfoType = uint32_t; + + // DataNode //////////////////////////////////////////////////////// + + // Primary template for the data node. We have special implementations for small and big + // objects. For large objects it is assumed that swap() is fairly slow, so we allocate these + // on the heap so swap merely swaps a pointer. + template + class DataNode {}; + + // Small: just allocate on the stack. + template + class DataNode final { + public: + template + explicit DataNode(M& ROBIN_HOOD_UNUSED(map) /*unused*/, Args&&... args) noexcept( + noexcept(value_type(std::forward(args)...))) + : mData(std::forward(args)...) {} + + DataNode(M& ROBIN_HOOD_UNUSED(map) /*unused*/, DataNode&& n) noexcept( + std::is_nothrow_move_constructible::value) + : mData(std::move(n.mData)) {} + + // doesn't do anything + void destroy(M& ROBIN_HOOD_UNUSED(map) /*unused*/) noexcept {} + void destroyDoNotDeallocate() noexcept {} + + value_type const* operator->() const noexcept { + return &mData; + } + value_type* operator->() noexcept { + return &mData; + } + + const value_type& operator*() const noexcept { + return mData; + } + + value_type& operator*() noexcept { + return mData; + } + + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::type getFirst() noexcept { + return mData.first; + } + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::type getFirst() noexcept { + return mData; + } + + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::type + getFirst() const noexcept { + return mData.first; + } + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::type getFirst() const noexcept { + return mData; + } + + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::type getSecond() noexcept { + return mData.second; + } + + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::type getSecond() const noexcept { + return mData.second; + } + + void swap(DataNode& o) noexcept( + noexcept(std::declval().swap(std::declval()))) { + mData.swap(o.mData); + } + + private: + value_type mData; + }; + + // big object: allocate on heap. + template + class DataNode { + public: + template + explicit DataNode(M& map, Args&&... args) + : mData(map.allocate()) { + ::new (static_cast(mData)) value_type(std::forward(args)...); + } + + DataNode(M& ROBIN_HOOD_UNUSED(map) /*unused*/, DataNode&& n) noexcept + : mData(std::move(n.mData)) {} + + void destroy(M& map) noexcept { + // don't deallocate, just put it into list of datapool. + mData->~value_type(); + map.deallocate(mData); + } + + void destroyDoNotDeallocate() noexcept { + mData->~value_type(); + } + + value_type const* operator->() const noexcept { + return mData; + } + + value_type* operator->() noexcept { + return mData; + } + + const value_type& operator*() const { + return *mData; + } + + value_type& operator*() { + return *mData; + } + + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::type getFirst() noexcept { + return mData->first; + } + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::type getFirst() noexcept { + return *mData; + } + + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::type + getFirst() const noexcept { + return mData->first; + } + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::type getFirst() const noexcept { + return *mData; + } + + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::type getSecond() noexcept { + return mData->second; + } + + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::type getSecond() const noexcept { + return mData->second; + } + + void swap(DataNode& o) noexcept { + using std::swap; + swap(mData, o.mData); + } + + private: + value_type* mData; + }; + + using Node = DataNode; + + // helpers for insertKeyPrepareEmptySpot: extract first entry (only const required) + ROBIN_HOOD(NODISCARD) key_type const& getFirstConst(Node const& n) const noexcept { + return n.getFirst(); + } + + // in case we have void mapped_type, we are not using a pair, thus we just route k through. + // No need to disable this because it's just not used if not applicable. + ROBIN_HOOD(NODISCARD) key_type const& getFirstConst(key_type const& k) const noexcept { + return k; + } + + // in case we have non-void mapped_type, we have a standard robin_hood::pair + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::value, key_type const&>::type + getFirstConst(value_type const& vt) const noexcept { + return vt.first; + } + + // Cloner ////////////////////////////////////////////////////////// + + template + struct Cloner; + + // fast path: Just copy data, without allocating anything. + template + struct Cloner { + void operator()(M const& source, M& target) const { + auto const* const src = reinterpret_cast(source.mKeyVals); + auto* tgt = reinterpret_cast(target.mKeyVals); + auto const numElementsWithBuffer = target.calcNumElementsWithBuffer(target.mMask + 1); + std::copy(src, src + target.calcNumBytesTotal(numElementsWithBuffer), tgt); + } + }; + + template + struct Cloner { + void operator()(M const& s, M& t) const { + auto const numElementsWithBuffer = t.calcNumElementsWithBuffer(t.mMask + 1); + std::copy(s.mInfo, s.mInfo + t.calcNumBytesInfo(numElementsWithBuffer), t.mInfo); + + for (size_t i = 0; i < numElementsWithBuffer; ++i) { + if (t.mInfo[i]) { + ::new (static_cast(t.mKeyVals + i)) Node(t, *s.mKeyVals[i]); + } + } + } + }; + + // Destroyer /////////////////////////////////////////////////////// + + template + struct Destroyer {}; + + template + struct Destroyer { + void nodes(M& m) const noexcept { + m.mNumElements = 0; + } + + void nodesDoNotDeallocate(M& m) const noexcept { + m.mNumElements = 0; + } + }; + + template + struct Destroyer { + void nodes(M& m) const noexcept { + m.mNumElements = 0; + // clear also resets mInfo to 0, that's sometimes not necessary. + auto const numElementsWithBuffer = m.calcNumElementsWithBuffer(m.mMask + 1); + + for (size_t idx = 0; idx < numElementsWithBuffer; ++idx) { + if (0 != m.mInfo[idx]) { + Node& n = m.mKeyVals[idx]; + n.destroy(m); + n.~Node(); + } + } + } + + void nodesDoNotDeallocate(M& m) const noexcept { + m.mNumElements = 0; + // clear also resets mInfo to 0, that's sometimes not necessary. + auto const numElementsWithBuffer = m.calcNumElementsWithBuffer(m.mMask + 1); + for (size_t idx = 0; idx < numElementsWithBuffer; ++idx) { + if (0 != m.mInfo[idx]) { + Node& n = m.mKeyVals[idx]; + n.destroyDoNotDeallocate(); + n.~Node(); + } + } + } + }; + + // Iter //////////////////////////////////////////////////////////// + + struct fast_forward_tag {}; + + // generic iterator for both const_iterator and iterator. + template + // NOLINTNEXTLINE(hicpp-special-member-functions,cppcoreguidelines-special-member-functions) + class Iter { + private: + using NodePtr = typename std::conditional::type; + + public: + using difference_type = std::ptrdiff_t; + using value_type = typename Self::value_type; + using reference = typename std::conditional::type; + using pointer = typename std::conditional::type; + using iterator_category = std::forward_iterator_tag; + + // default constructed iterator can be compared to itself, but WON'T return true when + // compared to end(). + Iter() = default; + + // Rule of zero: nothing specified. The conversion constructor is only enabled for + // iterator to const_iterator, so it doesn't accidentally work as a copy ctor. + + // Conversion constructor from iterator to const_iterator. + template ::type> + // NOLINTNEXTLINE(hicpp-explicit-conversions) + Iter(Iter const& other) noexcept + : mKeyVals(other.mKeyVals) + , mInfo(other.mInfo) {} + + Iter(NodePtr valPtr, uint8_t const* infoPtr) noexcept + : mKeyVals(valPtr) + , mInfo(infoPtr) {} + + Iter(NodePtr valPtr, uint8_t const* infoPtr, + fast_forward_tag ROBIN_HOOD_UNUSED(tag) /*unused*/) noexcept + : mKeyVals(valPtr) + , mInfo(infoPtr) { + fastForward(); + } + + template ::type> + Iter& operator=(Iter const& other) noexcept { + mKeyVals = other.mKeyVals; + mInfo = other.mInfo; + return *this; + } + + // prefix increment. Undefined behavior if we are at end()! + Iter& operator++() noexcept { + mInfo++; + mKeyVals++; + fastForward(); + return *this; + } + + Iter operator++(int) noexcept { + Iter tmp = *this; + ++(*this); + return tmp; + } + + reference operator*() const { + return **mKeyVals; + } + + pointer operator->() const { + return &**mKeyVals; + } + + template + bool operator==(Iter const& o) const noexcept { + return mKeyVals == o.mKeyVals; + } + + template + bool operator!=(Iter const& o) const noexcept { + return mKeyVals != o.mKeyVals; + } + + private: + // fast forward to the next non-free info byte + // I've tried a few variants that don't depend on intrinsics, but unfortunately they are + // quite a bit slower than this one. So I've reverted that change again. See map_benchmark. + void fastForward() noexcept { + size_t n = 0; + while (0U == (n = detail::unaligned_load(mInfo))) { + mInfo += sizeof(size_t); + mKeyVals += sizeof(size_t); + } +#if defined(ROBIN_HOOD_DISABLE_INTRINSICS) + // we know for certain that within the next 8 bytes we'll find a non-zero one. + if (ROBIN_HOOD_UNLIKELY(0U == detail::unaligned_load(mInfo))) { + mInfo += 4; + mKeyVals += 4; + } + if (ROBIN_HOOD_UNLIKELY(0U == detail::unaligned_load(mInfo))) { + mInfo += 2; + mKeyVals += 2; + } + if (ROBIN_HOOD_UNLIKELY(0U == *mInfo)) { + mInfo += 1; + mKeyVals += 1; + } +#else +# if ROBIN_HOOD(LITTLE_ENDIAN) + auto inc = ROBIN_HOOD_COUNT_TRAILING_ZEROES(n) / 8; +# else + auto inc = ROBIN_HOOD_COUNT_LEADING_ZEROES(n) / 8; +# endif + mInfo += inc; + mKeyVals += inc; +#endif + } + + friend class Table; + NodePtr mKeyVals{nullptr}; + uint8_t const* mInfo{nullptr}; + }; + + //////////////////////////////////////////////////////////////////// + + // highly performance relevant code. + // Lower bits are used for indexing into the array (2^n size) + // The upper 1-5 bits need to be a reasonable good hash, to save comparisons. + template + void keyToIdx(HashKey&& key, size_t* idx, InfoType* info) const { + // In addition to whatever hash is used, add another mul & shift so we get better hashing. + // This serves as a bad hash prevention, if the given data is + // badly mixed. + auto h = static_cast(WHash::operator()(key)); + + h *= mHashMultiplier; + h ^= h >> 33U; + + // the lower InitialInfoNumBits are reserved for info. + *info = mInfoInc + static_cast((h & InfoMask) >> mInfoHashShift); + *idx = (static_cast(h) >> InitialInfoNumBits) & mMask; + } + + // forwards the index by one, wrapping around at the end + void next(InfoType* info, size_t* idx) const noexcept { + *idx = *idx + 1; + *info += mInfoInc; + } + + void nextWhileLess(InfoType* info, size_t* idx) const noexcept { + // unrolling this by hand did not bring any speedups. + while (*info < mInfo[*idx]) { + next(info, idx); + } + } + + // Shift everything up by one element. Tries to move stuff around. + void + shiftUp(size_t startIdx, + size_t const insertion_idx) noexcept(std::is_nothrow_move_assignable::value) { + auto idx = startIdx; + ::new (static_cast(mKeyVals + idx)) Node(std::move(mKeyVals[idx - 1])); + while (--idx != insertion_idx) { + mKeyVals[idx] = std::move(mKeyVals[idx - 1]); + } + + idx = startIdx; + while (idx != insertion_idx) { + ROBIN_HOOD_COUNT(shiftUp) + mInfo[idx] = static_cast(mInfo[idx - 1] + mInfoInc); + if (ROBIN_HOOD_UNLIKELY(mInfo[idx] + mInfoInc > 0xFF)) { + mMaxNumElementsAllowed = 0; + } + --idx; + } + } + + void shiftDown(size_t idx) noexcept(std::is_nothrow_move_assignable::value) { + // until we find one that is either empty or has zero offset. + // TODO(martinus) we don't need to move everything, just the last one for the same + // bucket. + mKeyVals[idx].destroy(*this); + + // until we find one that is either empty or has zero offset. + while (mInfo[idx + 1] >= 2 * mInfoInc) { + ROBIN_HOOD_COUNT(shiftDown) + mInfo[idx] = static_cast(mInfo[idx + 1] - mInfoInc); + mKeyVals[idx] = std::move(mKeyVals[idx + 1]); + ++idx; + } + + mInfo[idx] = 0; + // don't destroy, we've moved it + // mKeyVals[idx].destroy(*this); + mKeyVals[idx].~Node(); + } + + // copy of find(), except that it returns iterator instead of const_iterator. + template + ROBIN_HOOD(NODISCARD) + size_t findIdx(Other const& key) const { + size_t idx{}; + InfoType info{}; + keyToIdx(key, &idx, &info); + + do { + // unrolling this twice gives a bit of a speedup. More unrolling did not help. + if (info == mInfo[idx] && + ROBIN_HOOD_LIKELY(WKeyEqual::operator()(key, mKeyVals[idx].getFirst()))) { + return idx; + } + next(&info, &idx); + if (info == mInfo[idx] && + ROBIN_HOOD_LIKELY(WKeyEqual::operator()(key, mKeyVals[idx].getFirst()))) { + return idx; + } + next(&info, &idx); + } while (info <= mInfo[idx]); + + // nothing found! + return mMask == 0 ? 0 + : static_cast(std::distance( + mKeyVals, reinterpret_cast_no_cast_align_warning(mInfo))); + } + + void cloneData(const Table& o) { + Cloner()(o, *this); + } + + // inserts a keyval that is guaranteed to be new, e.g. when the hashmap is resized. + // @return True on success, false if something went wrong + void insert_move(Node&& keyval) { + // we don't retry, fail if overflowing + // don't need to check max num elements + if (0 == mMaxNumElementsAllowed && !try_increase_info()) { + throwOverflowError(); + } + + size_t idx{}; + InfoType info{}; + keyToIdx(keyval.getFirst(), &idx, &info); + + // skip forward. Use <= because we are certain that the element is not there. + while (info <= mInfo[idx]) { + idx = idx + 1; + info += mInfoInc; + } + + // key not found, so we are now exactly where we want to insert it. + auto const insertion_idx = idx; + auto const insertion_info = static_cast(info); + if (ROBIN_HOOD_UNLIKELY(insertion_info + mInfoInc > 0xFF)) { + mMaxNumElementsAllowed = 0; + } + + // find an empty spot + while (0 != mInfo[idx]) { + next(&info, &idx); + } + + auto& l = mKeyVals[insertion_idx]; + if (idx == insertion_idx) { + ::new (static_cast(&l)) Node(std::move(keyval)); + } else { + shiftUp(idx, insertion_idx); + l = std::move(keyval); + } + + // put at empty spot + mInfo[insertion_idx] = insertion_info; + + ++mNumElements; + } + +public: + using iterator = Iter; + using const_iterator = Iter; + + Table() noexcept(noexcept(Hash()) && noexcept(KeyEqual())) + : WHash() + , WKeyEqual() { + ROBIN_HOOD_TRACE(this) + } + + // Creates an empty hash map. Nothing is allocated yet, this happens at the first insert. + // This tremendously speeds up ctor & dtor of a map that never receives an element. The + // penalty is payed at the first insert, and not before. Lookup of this empty map works + // because everybody points to DummyInfoByte::b. parameter bucket_count is dictated by the + // standard, but we can ignore it. + explicit Table( + size_t ROBIN_HOOD_UNUSED(bucket_count) /*unused*/, const Hash& h = Hash{}, + const KeyEqual& equal = KeyEqual{}) noexcept(noexcept(Hash(h)) && noexcept(KeyEqual(equal))) + : WHash(h) + , WKeyEqual(equal) { + ROBIN_HOOD_TRACE(this) + } + + template + Table(Iter first, Iter last, size_t ROBIN_HOOD_UNUSED(bucket_count) /*unused*/ = 0, + const Hash& h = Hash{}, const KeyEqual& equal = KeyEqual{}) + : WHash(h) + , WKeyEqual(equal) { + ROBIN_HOOD_TRACE(this) + insert(first, last); + } + + Table(std::initializer_list initlist, + size_t ROBIN_HOOD_UNUSED(bucket_count) /*unused*/ = 0, const Hash& h = Hash{}, + const KeyEqual& equal = KeyEqual{}) + : WHash(h) + , WKeyEqual(equal) { + ROBIN_HOOD_TRACE(this) + insert(initlist.begin(), initlist.end()); + } + + Table(Table&& o) noexcept + : WHash(std::move(static_cast(o))) + , WKeyEqual(std::move(static_cast(o))) + , DataPool(std::move(static_cast(o))) { + ROBIN_HOOD_TRACE(this) + if (o.mMask) { + mHashMultiplier = std::move(o.mHashMultiplier); + mKeyVals = std::move(o.mKeyVals); + mInfo = std::move(o.mInfo); + mNumElements = std::move(o.mNumElements); + mMask = std::move(o.mMask); + mMaxNumElementsAllowed = std::move(o.mMaxNumElementsAllowed); + mInfoInc = std::move(o.mInfoInc); + mInfoHashShift = std::move(o.mInfoHashShift); + // set other's mask to 0 so its destructor won't do anything + o.init(); + } + } + + Table& operator=(Table&& o) noexcept { + ROBIN_HOOD_TRACE(this) + if (&o != this) { + if (o.mMask) { + // only move stuff if the other map actually has some data + destroy(); + mHashMultiplier = std::move(o.mHashMultiplier); + mKeyVals = std::move(o.mKeyVals); + mInfo = std::move(o.mInfo); + mNumElements = std::move(o.mNumElements); + mMask = std::move(o.mMask); + mMaxNumElementsAllowed = std::move(o.mMaxNumElementsAllowed); + mInfoInc = std::move(o.mInfoInc); + mInfoHashShift = std::move(o.mInfoHashShift); + WHash::operator=(std::move(static_cast(o))); + WKeyEqual::operator=(std::move(static_cast(o))); + DataPool::operator=(std::move(static_cast(o))); + + o.init(); + + } else { + // nothing in the other map => just clear us. + clear(); + } + } + return *this; + } + + Table(const Table& o) + : WHash(static_cast(o)) + , WKeyEqual(static_cast(o)) + , DataPool(static_cast(o)) { + ROBIN_HOOD_TRACE(this) + if (!o.empty()) { + // not empty: create an exact copy. it is also possible to just iterate through all + // elements and insert them, but copying is probably faster. + + auto const numElementsWithBuffer = calcNumElementsWithBuffer(o.mMask + 1); + auto const numBytesTotal = calcNumBytesTotal(numElementsWithBuffer); + + ROBIN_HOOD_LOG("std::malloc " << numBytesTotal << " = calcNumBytesTotal(" + << numElementsWithBuffer << ")") + mHashMultiplier = o.mHashMultiplier; + mKeyVals = static_cast( + detail::assertNotNull(std::malloc(numBytesTotal))); + // no need for calloc because clonData does memcpy + mInfo = reinterpret_cast(mKeyVals + numElementsWithBuffer); + mNumElements = o.mNumElements; + mMask = o.mMask; + mMaxNumElementsAllowed = o.mMaxNumElementsAllowed; + mInfoInc = o.mInfoInc; + mInfoHashShift = o.mInfoHashShift; + cloneData(o); + } + } + + // Creates a copy of the given map. Copy constructor of each entry is used. + // Not sure why clang-tidy thinks this doesn't handle self assignment, it does + // NOLINTNEXTLINE(bugprone-unhandled-self-assignment,cert-oop54-cpp) + Table& operator=(Table const& o) { + ROBIN_HOOD_TRACE(this) + if (&o == this) { + // prevent assigning of itself + return *this; + } + + // we keep using the old allocator and not assign the new one, because we want to keep + // the memory available. when it is the same size. + if (o.empty()) { + if (0 == mMask) { + // nothing to do, we are empty too + return *this; + } + + // not empty: destroy what we have there + // clear also resets mInfo to 0, that's sometimes not necessary. + destroy(); + init(); + WHash::operator=(static_cast(o)); + WKeyEqual::operator=(static_cast(o)); + DataPool::operator=(static_cast(o)); + + return *this; + } + + // clean up old stuff + Destroyer::value>{}.nodes(*this); + + if (mMask != o.mMask) { + // no luck: we don't have the same array size allocated, so we need to realloc. + if (0 != mMask) { + // only deallocate if we actually have data! + ROBIN_HOOD_LOG("std::free") + std::free(mKeyVals); + } + + auto const numElementsWithBuffer = calcNumElementsWithBuffer(o.mMask + 1); + auto const numBytesTotal = calcNumBytesTotal(numElementsWithBuffer); + ROBIN_HOOD_LOG("std::malloc " << numBytesTotal << " = calcNumBytesTotal(" + << numElementsWithBuffer << ")") + mKeyVals = static_cast( + detail::assertNotNull(std::malloc(numBytesTotal))); + + // no need for calloc here because cloneData performs a memcpy. + mInfo = reinterpret_cast(mKeyVals + numElementsWithBuffer); + // sentinel is set in cloneData + } + WHash::operator=(static_cast(o)); + WKeyEqual::operator=(static_cast(o)); + DataPool::operator=(static_cast(o)); + mHashMultiplier = o.mHashMultiplier; + mNumElements = o.mNumElements; + mMask = o.mMask; + mMaxNumElementsAllowed = o.mMaxNumElementsAllowed; + mInfoInc = o.mInfoInc; + mInfoHashShift = o.mInfoHashShift; + cloneData(o); + + return *this; + } + + // Swaps everything between the two maps. + void swap(Table& o) { + ROBIN_HOOD_TRACE(this) + using std::swap; + swap(o, *this); + } + + // Clears all data, without resizing. + void clear() { + ROBIN_HOOD_TRACE(this) + if (empty()) { + // don't do anything! also important because we don't want to write to + // DummyInfoByte::b, even though we would just write 0 to it. + return; + } + + Destroyer::value>{}.nodes(*this); + + auto const numElementsWithBuffer = calcNumElementsWithBuffer(mMask + 1); + // clear everything, then set the sentinel again + uint8_t const z = 0; + std::fill(mInfo, mInfo + calcNumBytesInfo(numElementsWithBuffer), z); + mInfo[numElementsWithBuffer] = 1; + + mInfoInc = InitialInfoInc; + mInfoHashShift = InitialInfoHashShift; + } + + // Destroys the map and all it's contents. + ~Table() { + ROBIN_HOOD_TRACE(this) + destroy(); + } + + // Checks if both tables contain the same entries. Order is irrelevant. + bool operator==(const Table& other) const { + ROBIN_HOOD_TRACE(this) + if (other.size() != size()) { + return false; + } + for (auto const& otherEntry : other) { + if (!has(otherEntry)) { + return false; + } + } + + return true; + } + + bool operator!=(const Table& other) const { + ROBIN_HOOD_TRACE(this) + return !operator==(other); + } + + template + typename std::enable_if::value, Q&>::type operator[](const key_type& key) { + ROBIN_HOOD_TRACE(this) + auto idxAndState = insertKeyPrepareEmptySpot(key); + switch (idxAndState.second) { + case InsertionState::key_found: + break; + + case InsertionState::new_node: + ::new (static_cast(&mKeyVals[idxAndState.first])) + Node(*this, std::piecewise_construct, std::forward_as_tuple(key), + std::forward_as_tuple()); + break; + + case InsertionState::overwrite_node: + mKeyVals[idxAndState.first] = Node(*this, std::piecewise_construct, + std::forward_as_tuple(key), std::forward_as_tuple()); + break; + + case InsertionState::overflow_error: + throwOverflowError(); + } + + return mKeyVals[idxAndState.first].getSecond(); + } + + template + typename std::enable_if::value, Q&>::type operator[](key_type&& key) { + ROBIN_HOOD_TRACE(this) + auto idxAndState = insertKeyPrepareEmptySpot(key); + switch (idxAndState.second) { + case InsertionState::key_found: + break; + + case InsertionState::new_node: + ::new (static_cast(&mKeyVals[idxAndState.first])) + Node(*this, std::piecewise_construct, std::forward_as_tuple(std::move(key)), + std::forward_as_tuple()); + break; + + case InsertionState::overwrite_node: + mKeyVals[idxAndState.first] = + Node(*this, std::piecewise_construct, std::forward_as_tuple(std::move(key)), + std::forward_as_tuple()); + break; + + case InsertionState::overflow_error: + throwOverflowError(); + } + + return mKeyVals[idxAndState.first].getSecond(); + } + + template + void insert(Iter first, Iter last) { + for (; first != last; ++first) { + // value_type ctor needed because this might be called with std::pair's + insert(value_type(*first)); + } + } + + void insert(std::initializer_list ilist) { + for (auto&& vt : ilist) { + insert(std::move(vt)); + } + } + + template + std::pair emplace(Args&&... args) { + ROBIN_HOOD_TRACE(this) + Node n{*this, std::forward(args)...}; + auto idxAndState = insertKeyPrepareEmptySpot(getFirstConst(n)); + switch (idxAndState.second) { + case InsertionState::key_found: + n.destroy(*this); + break; + + case InsertionState::new_node: + ::new (static_cast(&mKeyVals[idxAndState.first])) Node(*this, std::move(n)); + break; + + case InsertionState::overwrite_node: + mKeyVals[idxAndState.first] = std::move(n); + break; + + case InsertionState::overflow_error: + n.destroy(*this); + throwOverflowError(); + break; + } + + return std::make_pair(iterator(mKeyVals + idxAndState.first, mInfo + idxAndState.first), + InsertionState::key_found != idxAndState.second); + } + + template + iterator emplace_hint(const_iterator position, Args&&... args) { + (void)position; + return emplace(std::forward(args)...).first; + } + + template + std::pair try_emplace(const key_type& key, Args&&... args) { + return try_emplace_impl(key, std::forward(args)...); + } + + template + std::pair try_emplace(key_type&& key, Args&&... args) { + return try_emplace_impl(std::move(key), std::forward(args)...); + } + + template + iterator try_emplace(const_iterator hint, const key_type& key, Args&&... args) { + (void)hint; + return try_emplace_impl(key, std::forward(args)...).first; + } + + template + iterator try_emplace(const_iterator hint, key_type&& key, Args&&... args) { + (void)hint; + return try_emplace_impl(std::move(key), std::forward(args)...).first; + } + + template + std::pair insert_or_assign(const key_type& key, Mapped&& obj) { + return insertOrAssignImpl(key, std::forward(obj)); + } + + template + std::pair insert_or_assign(key_type&& key, Mapped&& obj) { + return insertOrAssignImpl(std::move(key), std::forward(obj)); + } + + template + iterator insert_or_assign(const_iterator hint, const key_type& key, Mapped&& obj) { + (void)hint; + return insertOrAssignImpl(key, std::forward(obj)).first; + } + + template + iterator insert_or_assign(const_iterator hint, key_type&& key, Mapped&& obj) { + (void)hint; + return insertOrAssignImpl(std::move(key), std::forward(obj)).first; + } + + std::pair insert(const value_type& keyval) { + ROBIN_HOOD_TRACE(this) + return emplace(keyval); + } + + iterator insert(const_iterator hint, const value_type& keyval) { + (void)hint; + return emplace(keyval).first; + } + + std::pair insert(value_type&& keyval) { + return emplace(std::move(keyval)); + } + + iterator insert(const_iterator hint, value_type&& keyval) { + (void)hint; + return emplace(std::move(keyval)).first; + } + + // Returns 1 if key is found, 0 otherwise. + size_t count(const key_type& key) const { // NOLINT(modernize-use-nodiscard) + ROBIN_HOOD_TRACE(this) + auto kv = mKeyVals + findIdx(key); + if (kv != reinterpret_cast_no_cast_align_warning(mInfo)) { + return 1; + } + return 0; + } + + template + // NOLINTNEXTLINE(modernize-use-nodiscard) + typename std::enable_if::type count(const OtherKey& key) const { + ROBIN_HOOD_TRACE(this) + auto kv = mKeyVals + findIdx(key); + if (kv != reinterpret_cast_no_cast_align_warning(mInfo)) { + return 1; + } + return 0; + } + + bool contains(const key_type& key) const { // NOLINT(modernize-use-nodiscard) + return 1U == count(key); + } + + template + // NOLINTNEXTLINE(modernize-use-nodiscard) + typename std::enable_if::type contains(const OtherKey& key) const { + return 1U == count(key); + } + + // Returns a reference to the value found for key. + // Throws std::out_of_range if element cannot be found + template + // NOLINTNEXTLINE(modernize-use-nodiscard) + typename std::enable_if::value, Q&>::type at(key_type const& key) { + ROBIN_HOOD_TRACE(this) + auto kv = mKeyVals + findIdx(key); + if (kv == reinterpret_cast_no_cast_align_warning(mInfo)) { + doThrow("key not found"); + } + return kv->getSecond(); + } + + // Returns a reference to the value found for key. + // Throws std::out_of_range if element cannot be found + template + // NOLINTNEXTLINE(modernize-use-nodiscard) + typename std::enable_if::value, Q const&>::type at(key_type const& key) const { + ROBIN_HOOD_TRACE(this) + auto kv = mKeyVals + findIdx(key); + if (kv == reinterpret_cast_no_cast_align_warning(mInfo)) { + doThrow("key not found"); + } + return kv->getSecond(); + } + + const_iterator find(const key_type& key) const { // NOLINT(modernize-use-nodiscard) + ROBIN_HOOD_TRACE(this) + const size_t idx = findIdx(key); + return const_iterator{mKeyVals + idx, mInfo + idx}; + } + + template + const_iterator find(const OtherKey& key, is_transparent_tag /*unused*/) const { + ROBIN_HOOD_TRACE(this) + const size_t idx = findIdx(key); + return const_iterator{mKeyVals + idx, mInfo + idx}; + } + + template + typename std::enable_if::type // NOLINT(modernize-use-nodiscard) + find(const OtherKey& key) const { // NOLINT(modernize-use-nodiscard) + ROBIN_HOOD_TRACE(this) + const size_t idx = findIdx(key); + return const_iterator{mKeyVals + idx, mInfo + idx}; + } + + iterator find(const key_type& key) { + ROBIN_HOOD_TRACE(this) + const size_t idx = findIdx(key); + return iterator{mKeyVals + idx, mInfo + idx}; + } + + template + iterator find(const OtherKey& key, is_transparent_tag /*unused*/) { + ROBIN_HOOD_TRACE(this) + const size_t idx = findIdx(key); + return iterator{mKeyVals + idx, mInfo + idx}; + } + + template + typename std::enable_if::type find(const OtherKey& key) { + ROBIN_HOOD_TRACE(this) + const size_t idx = findIdx(key); + return iterator{mKeyVals + idx, mInfo + idx}; + } + + iterator begin() { + ROBIN_HOOD_TRACE(this) + if (empty()) { + return end(); + } + return iterator(mKeyVals, mInfo, fast_forward_tag{}); + } + const_iterator begin() const { // NOLINT(modernize-use-nodiscard) + ROBIN_HOOD_TRACE(this) + return cbegin(); + } + const_iterator cbegin() const { // NOLINT(modernize-use-nodiscard) + ROBIN_HOOD_TRACE(this) + if (empty()) { + return cend(); + } + return const_iterator(mKeyVals, mInfo, fast_forward_tag{}); + } + + iterator end() { + ROBIN_HOOD_TRACE(this) + // no need to supply valid info pointer: end() must not be dereferenced, and only node + // pointer is compared. + return iterator{reinterpret_cast_no_cast_align_warning(mInfo), nullptr}; + } + const_iterator end() const { // NOLINT(modernize-use-nodiscard) + ROBIN_HOOD_TRACE(this) + return cend(); + } + const_iterator cend() const { // NOLINT(modernize-use-nodiscard) + ROBIN_HOOD_TRACE(this) + return const_iterator{reinterpret_cast_no_cast_align_warning(mInfo), nullptr}; + } + + iterator erase(const_iterator pos) { + ROBIN_HOOD_TRACE(this) + // its safe to perform const cast here + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) + return erase(iterator{const_cast(pos.mKeyVals), const_cast(pos.mInfo)}); + } + + // Erases element at pos, returns iterator to the next element. + iterator erase(iterator pos) { + ROBIN_HOOD_TRACE(this) + // we assume that pos always points to a valid entry, and not end(). + auto const idx = static_cast(pos.mKeyVals - mKeyVals); + + shiftDown(idx); + --mNumElements; + + if (*pos.mInfo) { + // we've backward shifted, return this again + return pos; + } + + // no backward shift, return next element + return ++pos; + } + + size_t erase(const key_type& key) { + ROBIN_HOOD_TRACE(this) + size_t idx{}; + InfoType info{}; + keyToIdx(key, &idx, &info); + + // check while info matches with the source idx + do { + if (info == mInfo[idx] && WKeyEqual::operator()(key, mKeyVals[idx].getFirst())) { + shiftDown(idx); + --mNumElements; + return 1; + } + next(&info, &idx); + } while (info <= mInfo[idx]); + + // nothing found to delete + return 0; + } + + // reserves space for the specified number of elements. Makes sure the old data fits. + // exactly the same as reserve(c). + void rehash(size_t c) { + // forces a reserve + reserve(c, true); + } + + // reserves space for the specified number of elements. Makes sure the old data fits. + // Exactly the same as rehash(c). Use rehash(0) to shrink to fit. + void reserve(size_t c) { + // reserve, but don't force rehash + reserve(c, false); + } + + // If possible reallocates the map to a smaller one. This frees the underlying table. + // Does not do anything if load_factor is too large for decreasing the table's size. + void compact() { + ROBIN_HOOD_TRACE(this) + auto newSize = InitialNumElements; + while (calcMaxNumElementsAllowed(newSize) < mNumElements && newSize != 0) { + newSize *= 2; + } + if (ROBIN_HOOD_UNLIKELY(newSize == 0)) { + throwOverflowError(); + } + + ROBIN_HOOD_LOG("newSize > mMask + 1: " << newSize << " > " << mMask << " + 1") + + // only actually do anything when the new size is bigger than the old one. This prevents to + // continuously allocate for each reserve() call. + if (newSize < mMask + 1) { + rehashPowerOfTwo(newSize, true); + } + } + + size_type size() const noexcept { // NOLINT(modernize-use-nodiscard) + ROBIN_HOOD_TRACE(this) + return mNumElements; + } + + size_type max_size() const noexcept { // NOLINT(modernize-use-nodiscard) + ROBIN_HOOD_TRACE(this) + return static_cast(-1); + } + + ROBIN_HOOD(NODISCARD) bool empty() const noexcept { + ROBIN_HOOD_TRACE(this) + return 0 == mNumElements; + } + + float max_load_factor() const noexcept { // NOLINT(modernize-use-nodiscard) + ROBIN_HOOD_TRACE(this) + return MaxLoadFactor100 / 100.0F; + } + + // Average number of elements per bucket. Since we allow only 1 per bucket + float load_factor() const noexcept { // NOLINT(modernize-use-nodiscard) + ROBIN_HOOD_TRACE(this) + return static_cast(size()) / static_cast(mMask + 1); + } + + ROBIN_HOOD(NODISCARD) size_t mask() const noexcept { + ROBIN_HOOD_TRACE(this) + return mMask; + } + + ROBIN_HOOD(NODISCARD) size_t calcMaxNumElementsAllowed(size_t maxElements) const noexcept { + if (ROBIN_HOOD_LIKELY(maxElements <= (std::numeric_limits::max)() / 100)) { + return maxElements * MaxLoadFactor100 / 100; + } + + // we might be a bit inprecise, but since maxElements is quite large that doesn't matter + return (maxElements / 100) * MaxLoadFactor100; + } + + ROBIN_HOOD(NODISCARD) size_t calcNumBytesInfo(size_t numElements) const noexcept { + // we add a uint64_t, which houses the sentinel (first byte) and padding so we can load + // 64bit types. + return numElements + sizeof(uint64_t); + } + + ROBIN_HOOD(NODISCARD) + size_t calcNumElementsWithBuffer(size_t numElements) const noexcept { + auto maxNumElementsAllowed = calcMaxNumElementsAllowed(numElements); + return numElements + (std::min)(maxNumElementsAllowed, (static_cast(0xFF))); + } + + // calculation only allowed for 2^n values + ROBIN_HOOD(NODISCARD) size_t calcNumBytesTotal(size_t numElements) const { +#if ROBIN_HOOD(BITNESS) == 64 + return numElements * sizeof(Node) + calcNumBytesInfo(numElements); +#else + // make sure we're doing 64bit operations, so we are at least safe against 32bit overflows. + auto const ne = static_cast(numElements); + auto const s = static_cast(sizeof(Node)); + auto const infos = static_cast(calcNumBytesInfo(numElements)); + + auto const total64 = ne * s + infos; + auto const total = static_cast(total64); + + if (ROBIN_HOOD_UNLIKELY(static_cast(total) != total64)) { + throwOverflowError(); + } + return total; +#endif + } + +private: + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::value, bool>::type has(const value_type& e) const { + ROBIN_HOOD_TRACE(this) + auto it = find(e.first); + return it != end() && it->second == e.second; + } + + template + ROBIN_HOOD(NODISCARD) + typename std::enable_if::value, bool>::type has(const value_type& e) const { + ROBIN_HOOD_TRACE(this) + return find(e) != end(); + } + + void reserve(size_t c, bool forceRehash) { + ROBIN_HOOD_TRACE(this) + auto const minElementsAllowed = (std::max)(c, mNumElements); + auto newSize = InitialNumElements; + while (calcMaxNumElementsAllowed(newSize) < minElementsAllowed && newSize != 0) { + newSize *= 2; + } + if (ROBIN_HOOD_UNLIKELY(newSize == 0)) { + throwOverflowError(); + } + + ROBIN_HOOD_LOG("newSize > mMask + 1: " << newSize << " > " << mMask << " + 1") + + // only actually do anything when the new size is bigger than the old one. This prevents to + // continuously allocate for each reserve() call. + if (forceRehash || newSize > mMask + 1) { + rehashPowerOfTwo(newSize, false); + } + } + + // reserves space for at least the specified number of elements. + // only works if numBuckets if power of two + // True on success, false otherwise + void rehashPowerOfTwo(size_t numBuckets, bool forceFree) { + ROBIN_HOOD_TRACE(this) + + Node* const oldKeyVals = mKeyVals; + uint8_t const* const oldInfo = mInfo; + + const size_t oldMaxElementsWithBuffer = calcNumElementsWithBuffer(mMask + 1); + + // resize operation: move stuff + initData(numBuckets); + if (oldMaxElementsWithBuffer > 1) { + for (size_t i = 0; i < oldMaxElementsWithBuffer; ++i) { + if (oldInfo[i] != 0) { + // might throw an exception, which is really bad since we are in the middle of + // moving stuff. + insert_move(std::move(oldKeyVals[i])); + // destroy the node but DON'T destroy the data. + oldKeyVals[i].~Node(); + } + } + + // this check is not necessary as it's guarded by the previous if, but it helps + // silence g++'s overeager "attempt to free a non-heap object 'map' + // [-Werror=free-nonheap-object]" warning. + if (oldKeyVals != reinterpret_cast_no_cast_align_warning(&mMask)) { + // don't destroy old data: put it into the pool instead + if (forceFree) { + std::free(oldKeyVals); + } else { + DataPool::addOrFree(oldKeyVals, calcNumBytesTotal(oldMaxElementsWithBuffer)); + } + } + } + } + + ROBIN_HOOD(NOINLINE) void throwOverflowError() const { +#if ROBIN_HOOD(HAS_EXCEPTIONS) + throw std::overflow_error("robin_hood::map overflow"); +#else + abort(); +#endif + } + + template + std::pair try_emplace_impl(OtherKey&& key, Args&&... args) { + ROBIN_HOOD_TRACE(this) + auto idxAndState = insertKeyPrepareEmptySpot(key); + switch (idxAndState.second) { + case InsertionState::key_found: + break; + + case InsertionState::new_node: + ::new (static_cast(&mKeyVals[idxAndState.first])) Node( + *this, std::piecewise_construct, std::forward_as_tuple(std::forward(key)), + std::forward_as_tuple(std::forward(args)...)); + break; + + case InsertionState::overwrite_node: + mKeyVals[idxAndState.first] = Node(*this, std::piecewise_construct, + std::forward_as_tuple(std::forward(key)), + std::forward_as_tuple(std::forward(args)...)); + break; + + case InsertionState::overflow_error: + throwOverflowError(); + break; + } + + return std::make_pair(iterator(mKeyVals + idxAndState.first, mInfo + idxAndState.first), + InsertionState::key_found != idxAndState.second); + } + + template + std::pair insertOrAssignImpl(OtherKey&& key, Mapped&& obj) { + ROBIN_HOOD_TRACE(this) + auto idxAndState = insertKeyPrepareEmptySpot(key); + switch (idxAndState.second) { + case InsertionState::key_found: + mKeyVals[idxAndState.first].getSecond() = std::forward(obj); + break; + + case InsertionState::new_node: + ::new (static_cast(&mKeyVals[idxAndState.first])) Node( + *this, std::piecewise_construct, std::forward_as_tuple(std::forward(key)), + std::forward_as_tuple(std::forward(obj))); + break; + + case InsertionState::overwrite_node: + mKeyVals[idxAndState.first] = Node(*this, std::piecewise_construct, + std::forward_as_tuple(std::forward(key)), + std::forward_as_tuple(std::forward(obj))); + break; + + case InsertionState::overflow_error: + throwOverflowError(); + break; + } + + return std::make_pair(iterator(mKeyVals + idxAndState.first, mInfo + idxAndState.first), + InsertionState::key_found != idxAndState.second); + } + + void initData(size_t max_elements) { + mNumElements = 0; + mMask = max_elements - 1; + mMaxNumElementsAllowed = calcMaxNumElementsAllowed(max_elements); + + auto const numElementsWithBuffer = calcNumElementsWithBuffer(max_elements); + + // malloc & zero mInfo. Faster than calloc everything. + auto const numBytesTotal = calcNumBytesTotal(numElementsWithBuffer); + ROBIN_HOOD_LOG("std::calloc " << numBytesTotal << " = calcNumBytesTotal(" + << numElementsWithBuffer << ")") + mKeyVals = reinterpret_cast( + detail::assertNotNull(std::malloc(numBytesTotal))); + mInfo = reinterpret_cast(mKeyVals + numElementsWithBuffer); + std::memset(mInfo, 0, numBytesTotal - numElementsWithBuffer * sizeof(Node)); + + // set sentinel + mInfo[numElementsWithBuffer] = 1; + + mInfoInc = InitialInfoInc; + mInfoHashShift = InitialInfoHashShift; + } + + enum class InsertionState { overflow_error, key_found, new_node, overwrite_node }; + + // Finds key, and if not already present prepares a spot where to pot the key & value. + // This potentially shifts nodes out of the way, updates mInfo and number of inserted + // elements, so the only operation left to do is create/assign a new node at that spot. + template + std::pair insertKeyPrepareEmptySpot(OtherKey&& key) { + for (int i = 0; i < 256; ++i) { + size_t idx{}; + InfoType info{}; + keyToIdx(key, &idx, &info); + nextWhileLess(&info, &idx); + + // while we potentially have a match + while (info == mInfo[idx]) { + if (WKeyEqual::operator()(key, mKeyVals[idx].getFirst())) { + // key already exists, do NOT insert. + // see http://en.cppreference.com/w/cpp/container/unordered_map/insert + return std::make_pair(idx, InsertionState::key_found); + } + next(&info, &idx); + } + + // unlikely that this evaluates to true + if (ROBIN_HOOD_UNLIKELY(mNumElements >= mMaxNumElementsAllowed)) { + if (!increase_size()) { + return std::make_pair(size_t(0), InsertionState::overflow_error); + } + continue; + } + + // key not found, so we are now exactly where we want to insert it. + auto const insertion_idx = idx; + auto const insertion_info = info; + if (ROBIN_HOOD_UNLIKELY(insertion_info + mInfoInc > 0xFF)) { + mMaxNumElementsAllowed = 0; + } + + // find an empty spot + while (0 != mInfo[idx]) { + next(&info, &idx); + } + + if (idx != insertion_idx) { + shiftUp(idx, insertion_idx); + } + // put at empty spot + mInfo[insertion_idx] = static_cast(insertion_info); + ++mNumElements; + return std::make_pair(insertion_idx, idx == insertion_idx + ? InsertionState::new_node + : InsertionState::overwrite_node); + } + + // enough attempts failed, so finally give up. + return std::make_pair(size_t(0), InsertionState::overflow_error); + } + + bool try_increase_info() { + ROBIN_HOOD_LOG("mInfoInc=" << mInfoInc << ", numElements=" << mNumElements + << ", maxNumElementsAllowed=" + << calcMaxNumElementsAllowed(mMask + 1)) + if (mInfoInc <= 2) { + // need to be > 2 so that shift works (otherwise undefined behavior!) + return false; + } + // we got space left, try to make info smaller + mInfoInc = static_cast(mInfoInc >> 1U); + + // remove one bit of the hash, leaving more space for the distance info. + // This is extremely fast because we can operate on 8 bytes at once. + ++mInfoHashShift; + auto const numElementsWithBuffer = calcNumElementsWithBuffer(mMask + 1); + + for (size_t i = 0; i < numElementsWithBuffer; i += 8) { + auto val = unaligned_load(mInfo + i); + val = (val >> 1U) & UINT64_C(0x7f7f7f7f7f7f7f7f); + std::memcpy(mInfo + i, &val, sizeof(val)); + } + // update sentinel, which might have been cleared out! + mInfo[numElementsWithBuffer] = 1; + + mMaxNumElementsAllowed = calcMaxNumElementsAllowed(mMask + 1); + return true; + } + + // True if resize was possible, false otherwise + bool increase_size() { + // nothing allocated yet? just allocate InitialNumElements + if (0 == mMask) { + initData(InitialNumElements); + return true; + } + + auto const maxNumElementsAllowed = calcMaxNumElementsAllowed(mMask + 1); + if (mNumElements < maxNumElementsAllowed && try_increase_info()) { + return true; + } + + ROBIN_HOOD_LOG("mNumElements=" << mNumElements << ", maxNumElementsAllowed=" + << maxNumElementsAllowed << ", load=" + << (static_cast(mNumElements) * 100.0 / + (static_cast(mMask) + 1))) + + if (mNumElements * 2 < calcMaxNumElementsAllowed(mMask + 1)) { + // we have to resize, even though there would still be plenty of space left! + // Try to rehash instead. Delete freed memory so we don't steadyily increase mem in case + // we have to rehash a few times + nextHashMultiplier(); + rehashPowerOfTwo(mMask + 1, true); + } else { + // we've reached the capacity of the map, so the hash seems to work nice. Keep using it. + rehashPowerOfTwo((mMask + 1) * 2, false); + } + return true; + } + + void nextHashMultiplier() { + // adding an *even* number, so that the multiplier will always stay odd. This is necessary + // so that the hash stays a mixing function (and thus doesn't have any information loss). + mHashMultiplier += UINT64_C(0xc4ceb9fe1a85ec54); + } + + void destroy() { + if (0 == mMask) { + // don't deallocate! + return; + } + + Destroyer::value>{} + .nodesDoNotDeallocate(*this); + + // This protection against not deleting mMask shouldn't be needed as it's sufficiently + // protected with the 0==mMask check, but I have this anyways because g++ 7 otherwise + // reports a compile error: attempt to free a non-heap object 'fm' + // [-Werror=free-nonheap-object] + if (mKeyVals != reinterpret_cast_no_cast_align_warning(&mMask)) { + ROBIN_HOOD_LOG("std::free") + std::free(mKeyVals); + } + } + + void init() noexcept { + mKeyVals = reinterpret_cast_no_cast_align_warning(&mMask); + mInfo = reinterpret_cast(&mMask); + mNumElements = 0; + mMask = 0; + mMaxNumElementsAllowed = 0; + mInfoInc = InitialInfoInc; + mInfoHashShift = InitialInfoHashShift; + } + + // members are sorted so no padding occurs + uint64_t mHashMultiplier = UINT64_C(0xc4ceb9fe1a85ec53); // 8 byte 8 + Node* mKeyVals = reinterpret_cast_no_cast_align_warning(&mMask); // 8 byte 16 + uint8_t* mInfo = reinterpret_cast(&mMask); // 8 byte 24 + size_t mNumElements = 0; // 8 byte 32 + size_t mMask = 0; // 8 byte 40 + size_t mMaxNumElementsAllowed = 0; // 8 byte 48 + InfoType mInfoInc = InitialInfoInc; // 4 byte 52 + InfoType mInfoHashShift = InitialInfoHashShift; // 4 byte 56 + // 16 byte 56 if NodeAllocator +}; + +} // namespace detail + +// map + +template , + typename KeyEqual = std::equal_to, size_t MaxLoadFactor100 = 80> +using unordered_flat_map = detail::Table; + +template , + typename KeyEqual = std::equal_to, size_t MaxLoadFactor100 = 80> +using unordered_node_map = detail::Table; + +template , + typename KeyEqual = std::equal_to, size_t MaxLoadFactor100 = 80> +using unordered_map = + detail::Table) <= sizeof(size_t) * 6 && + std::is_nothrow_move_constructible>::value && + std::is_nothrow_move_assignable>::value, + MaxLoadFactor100, Key, T, Hash, KeyEqual>; + +// set + +template , typename KeyEqual = std::equal_to, + size_t MaxLoadFactor100 = 80> +using unordered_flat_set = detail::Table; + +template , typename KeyEqual = std::equal_to, + size_t MaxLoadFactor100 = 80> +using unordered_node_set = detail::Table; + +template , typename KeyEqual = std::equal_to, + size_t MaxLoadFactor100 = 80> +using unordered_set = detail::Table::value && + std::is_nothrow_move_assignable::value, + MaxLoadFactor100, Key, void, Hash, KeyEqual>; + +} // namespace robin_hood + +#endif diff --git a/src/serialize_filters.cpp b/src/serialize_filters.cpp new file mode 100644 index 00000000..a8e4363a --- /dev/null +++ b/src/serialize_filters.cpp @@ -0,0 +1,442 @@ +#include "filters.h" +#include "generic_adapters.h" +#include "out_stream.h" +#include "testers.h" +#include "value_helpers.h" +#include "value_visitors.h" + +#include + +#include +#include +#include +#include +#include + +#ifdef JINJA2CPP_WITH_JSON_BINDINGS_BOOST +#include "binding/boost_json_serializer.h" +using DocumentWrapper = jinja2::boost_json_serializer::DocumentWrapper; +#else +#include "binding/rapid_json_serializer.h" +using DocumentWrapper = jinja2::rapidjson_serializer::DocumentWrapper; +#endif + + +using namespace std::string_literals; + +namespace jinja2 +{ +namespace filters +{ +struct PrettyPrinter : visitors::BaseVisitor +{ + using BaseVisitor::operator(); + + PrettyPrinter(const RenderContext* context) + : m_context(context) + { + } + + std::string operator()(const ListAdapter& list) const + { + std::string str; + auto os = std::back_inserter(str); + + fmt::format_to(os, "["); + bool isFirst = true; + + for (auto& v : list) + { + if (isFirst) + isFirst = false; + else + fmt::format_to(os, ", "); + fmt::format_to(os, "{}", Apply(v, m_context)); + } + fmt::format_to(os, "]"); + + return str; + } + + std::string operator()(const MapAdapter& map) const + { + std::string str; + auto os = std::back_inserter(str); + + fmt::format_to(os, "{{"); + + const auto& keys = map.GetKeys(); + + bool isFirst = true; + for (auto& k : keys) + { + if (isFirst) + isFirst = false; + else + fmt::format_to(os, ", "); + + fmt::format_to(os, "'{}': ", k); + fmt::format_to(os, "{}", Apply(map.GetValueByName(k), m_context)); + } + + fmt::format_to(os, "}}"); + + return str; + } + + std::string operator()(const KeyValuePair& kwPair) const + { + std::string str; + auto os = std::back_inserter(str); + + fmt::format_to(os, "'{}': ", kwPair.key); + fmt::format_to(os, "{}", Apply(kwPair.value, m_context)); + + return str; + } + + std::string operator()(const std::string& str) const { return fmt::format("'{}'", str); } + + std::string operator()(const nonstd::string_view& str) const { return fmt::format("'{}'", fmt::basic_string_view(str.data(), str.size())); } + + std::string operator()(const std::wstring& str) const { return fmt::format("'{}'", ConvertString(str)); } + + std::string operator()(const nonstd::wstring_view& str) const { return fmt::format("'{}'", ConvertString(str)); } + + std::string operator()(bool val) const { return val ? "true"s : "false"s; } + + std::string operator()(EmptyValue) const { return "none"s; } + + std::string operator()(const Callable&) const { return ""s; } + + std::string operator()(double val) const + { + std::string str; + auto os = std::back_inserter(str); + + fmt::format_to(os, "{:.8g}", val); + + return str; + } + + std::string operator()(int64_t val) const { return fmt::format("{}", val); } + + const RenderContext* m_context; +}; + +PrettyPrint::PrettyPrint(FilterParams params) {} + +InternalValue PrettyPrint::Filter(const InternalValue& baseVal, RenderContext& context) +{ + return Apply(baseVal, &context); +} + +Serialize::Serialize(const FilterParams params, const Serialize::Mode mode) + : m_mode(mode) +{ + switch (mode) + { + case JsonMode: + ParseParams({ { "indent", false, static_cast(0) } }, params); + break; + default: + break; + } +} + +InternalValue Serialize::Filter(const InternalValue& value, RenderContext& context) +{ + if (m_mode == JsonMode) + { + const auto indent = ConvertToInt(this->GetArgumentValue("indent", context)); + DocumentWrapper jsonDoc; + const auto jsonValue = jsonDoc.CreateValue(value); + const auto jsonString = jsonValue.AsString(static_cast(indent)); + std::string result = ""s; + result.reserve(jsonString.size()); + for (char c : jsonString) { + if (c == '<') { + result.append("\\u003c"); + } else if (c == '>') { + result.append("\\u003e"); + } else if (c == '&') { + result.append("\\u0026"); + } else if (c == '\'') { + result.append("\\u0027"); + } else { + result.push_back(c); + } + } + return result; + } + + return InternalValue(); +} + +namespace +{ + +using FormatContext = fmt::format_context; +using FormatArgument = fmt::basic_format_arg; +using FormatDynamicArgsStore = fmt::dynamic_format_arg_store; + +struct FormatArgumentConverter : visitors::BaseVisitor +{ + using result_t = FormatArgument; + + using BaseVisitor::operator(); + + FormatArgumentConverter(const RenderContext* context, FormatDynamicArgsStore& store) + : m_context(context) + , m_store(store) + { + } + + FormatArgumentConverter(const RenderContext* context, FormatDynamicArgsStore& store, const std::string& name) + : m_context(context) + , m_store(store) + , m_name(name) + , m_named(true) + { + } + + result_t operator()(const ListAdapter& list) const { return make_result(Apply(list, m_context)); } + + result_t operator()(const MapAdapter& map) const { return make_result(Apply(map, m_context)); } + + result_t operator()(const std::string& str) const { return make_result(str); } + + result_t operator()(const nonstd::string_view& str) const { return make_result(std::string(str.data(), str.size())); } + + result_t operator()(const std::wstring& str) const { return make_result(ConvertString(str)); } + + result_t operator()(const nonstd::wstring_view& str) const { return make_result(ConvertString(str)); } + + result_t operator()(double val) const { return make_result(val); } + + result_t operator()(int64_t val) const { return make_result(val); } + + result_t operator()(bool val) const { return make_result(val ? "true"s : "false"s); } + + result_t operator()(EmptyValue) const { return make_result("none"s); } + + result_t operator()(const Callable&) const { return make_result(""s); } + + template + result_t make_result(const T& t) const + { + if (!m_named) + { + m_store.push_back(t); + } + else + { + m_store.push_back(fmt::arg(m_name.c_str(), t)); + } + return fmt::detail::make_arg(t); + } + + const RenderContext* m_context; + FormatDynamicArgsStore& m_store; + const std::string m_name; + bool m_named = false; +}; + +} // namespace + +InternalValue StringFormat::Filter(const InternalValue& baseVal, RenderContext& context) +{ + // Format library internally likes using non-owning views to complex arguments. + // In order to ensure proper lifetime of values and named args, + // helper buffer is created and passed to visitors. + FormatDynamicArgsStore store; + for (auto& arg : m_params.posParams) + { + Apply(arg->Evaluate(context), &context, store); + } + + for (auto& arg : m_params.kwParams) + { + Apply(arg.second->Evaluate(context), &context, store, arg.first); + } + + return InternalValue(fmt::vformat(AsString(baseVal), store)); +} + +class XmlAttrPrinter : public visitors::BaseVisitor +{ +public: + using BaseVisitor::operator(); + + explicit XmlAttrPrinter(RenderContext* context, bool isFirstLevel = false) + : m_context(context) + , m_isFirstLevel(isFirstLevel) + { + } + + std::string operator()(const ListAdapter& list) const + { + EnforceThatNested(); + + return EscapeHtml(Apply(list, m_context)); + } + + std::string operator()(const MapAdapter& map) const + { + if (!m_isFirstLevel) + { + return EscapeHtml(Apply(map, m_context)); + } + + std::string str; + auto os = std::back_inserter(str); + + const auto& keys = map.GetKeys(); + + bool isFirst = true; + for (auto& k : keys) + { + const auto& v = map.GetValueByName(k); + const auto item = Apply(v, m_context, false); + if (item.length() > 0) + { + if (isFirst) + isFirst = false; + else + fmt::format_to(os, " "); + + fmt::format_to(os, "{}=\"{}\"", k, item); + } + } + + return str; + } + + std::string operator()(const KeyValuePair& kwPair) const + { + EnforceThatNested(); + + return EscapeHtml(Apply(kwPair, m_context)); + } + + std::string operator()(const std::string& str) const + { + EnforceThatNested(); + + return EscapeHtml(str); + } + + std::string operator()(const nonstd::string_view& str) const + { + EnforceThatNested(); + + const auto result = fmt::format("{}", fmt::basic_string_view(str.data(), str.size())); + return EscapeHtml(result); + } + + std::string operator()(const std::wstring& str) const + { + EnforceThatNested(); + + return EscapeHtml(ConvertString(str)); + } + + std::string operator()(const nonstd::wstring_view& str) const + { + EnforceThatNested(); + + const auto result = fmt::format("{}", ConvertString(str)); + return EscapeHtml(result); + } + + std::string operator()(bool val) const + { + EnforceThatNested(); + + return val ? "true"s : "false"s; + } + + std::string operator()(EmptyValue) const + { + EnforceThatNested(); + + return ""s; + } + + std::string operator()(const Callable&) const + { + EnforceThatNested(); + + return ""s; + } + + std::string operator()(double val) const + { + EnforceThatNested(); + + std::string str; + auto os = std::back_inserter(str); + + fmt::format_to(os, "{:.8g}", val); + + return str; + } + + std::string operator()(int64_t val) const + { + EnforceThatNested(); + + return fmt::format("{}", val); + } + +private: + void EnforceThatNested() const + { + if (m_isFirstLevel) + m_context->GetRendererCallback()->ThrowRuntimeError(ErrorCode::InvalidValueType, ValuesList{}); + } + + std::string EscapeHtml(const std::string &str) const + { + const auto result = std::accumulate(str.begin(), str.end(), ""s, [](const auto &str, const auto &c) + { + switch (c) + { + case '<': + return str + "<"; + break; + case '>': + return str +">"; + break; + case '&': + return str +"&"; + break; + case '\'': + return str +"'"; + break; + case '\"': + return str +"""; + break; + default: + return str + c; + break; + } + }); + + return result; + } + +private: + RenderContext* m_context; + bool m_isFirstLevel; +}; + +XmlAttrFilter::XmlAttrFilter(FilterParams) {} + +InternalValue XmlAttrFilter::Filter(const InternalValue& baseVal, RenderContext& context) +{ + return Apply(baseVal, &context, true); +} + +} // namespace filters +} // namespace jinja2 diff --git a/src/statements.cpp b/src/statements.cpp index 04c1027b..25bc11cc 100644 --- a/src/statements.cpp +++ b/src/statements.cpp @@ -1,10 +1,14 @@ -#include "expression_evaluator.h" #include "statements.h" + +#include "expression_evaluator.h" #include "template_impl.h" #include "value_visitors.h" -#include +#include + +#include +using namespace std::string_literals; namespace jinja2 { @@ -13,37 +17,39 @@ void ForStatement::Render(OutStream& os, RenderContext& values) { InternalValue loopVal = m_value->Evaluate(values); - RenderLoop(loopVal, os, values); + RenderLoop(loopVal, os, values, 0); } -void ForStatement::RenderLoop(const InternalValue& loopVal, OutStream& os, RenderContext& values) +void ForStatement::RenderLoop(const InternalValue& loopVal, OutStream& os, RenderContext& values, int level) { auto& context = values.EnterScope(); InternalValueMap loopVar; - context["loop"] = MapAdapter::CreateAdapter(&loopVar); + context["loop"s] = CreateMapAdapter(&loopVar); if (m_isRecursive) { - loopVar["operator()"] = Callable([this](const CallParams& params, OutStream& stream, RenderContext& context) { - bool isSucceeded = false; - auto parsedParams = helpers::ParseCallParams({{"var", true}}, params, isSucceeded); - if (!isSucceeded) - { - return; - } - - auto var = parsedParams["var"]; - if (!var) - { - return; - } - - RenderLoop(var->Evaluate(context), stream, context); - }); + loopVar["operator()"s] = Callable(Callable::GlobalFunc, [this, level](const CallParams& params, OutStream& stream, RenderContext& context) { + bool isSucceeded = false; + auto parsedParams = helpers::ParseCallParams({ { "var", true } }, params, isSucceeded); + if (!isSucceeded) + return; + + auto var = parsedParams["var"]; + if (var.IsEmpty()) + return; + + RenderLoop(var, stream, context, level + 1); + }); + loopVar["depth"s] = static_cast(level + 1); + loopVar["depth0"s] = static_cast(level); } bool isConverted = false; - auto loopItems = ConvertToList(loopVal, InternalValue(), isConverted); + auto loopItems = ConvertToList(loopVal, isConverted, false); + ListAdapter filteredList; + ListAdapter indexedList; + ListAccessorEnumeratorPtr enumerator; + size_t itemIdx = 0; if (!isConverted) { if (m_elseBody) @@ -52,55 +58,103 @@ void ForStatement::RenderLoop(const InternalValue& loopVal, OutStream& os, Rende return; } + nonstd::optional listSize; if (m_ifExpr) { - auto& tempContext = values.EnterScope(); - InternalValueList newLoopItems; - for (auto& curValue : loopItems) + filteredList = CreateFilteredAdapter(loopItems, values); + enumerator = filteredList.GetEnumerator(); + } + else + { + enumerator = loopItems.GetEnumerator(); + listSize = loopItems.GetSize(); + } + + bool isLast = false; + auto makeIndexedList = [&enumerator, &listSize, &indexedList, &itemIdx, &isLast] { + if (isLast) + listSize = itemIdx; + + InternalValueList items; + do { - if (m_vars.size() > 1) - { - for (auto& varName : m_vars) - tempContext[varName] = Subscript(curValue, varName); - } - else - tempContext[m_vars[0]] = curValue; + items.push_back(enumerator->GetCurrent()); + } while (enumerator->MoveNext()); - if (ConvertToBool(m_ifExpr->Evaluate(values))) - newLoopItems.push_back(curValue); - } - values.ExitScope(); + listSize = itemIdx + items.size() + 1; + indexedList = ListAdapter::CreateAdapter(std::move(items)); + enumerator = indexedList.GetEnumerator(); + isLast = !enumerator->MoveNext(); + }; - loopItems = ListAdapter::CreateAdapter(std::move(newLoopItems)); + if (listSize) + { + int64_t itemsNum = static_cast(listSize.value()); + loopVar["length"s] = InternalValue(itemsNum); + } + else + { + loopVar["length"s] = MakeDynamicProperty([&listSize, &makeIndexedList](const CallParams& /*params*/, RenderContext & /*context*/) -> InternalValue { + if (!listSize) + makeIndexedList(); + return static_cast(listSize.value()); + }); } - - int64_t itemsNum = static_cast(loopItems.GetSize()); - loopVar["length"] = InternalValue(itemsNum); bool loopRendered = false; - for (int64_t itemIdx = 0; itemIdx != itemsNum; ++ itemIdx) + isLast = !enumerator->MoveNext(); + InternalValue prevValue; + InternalValue curValue; + InternalValue nextValue; + loopVar["cycle"s] = static_cast(LoopCycleFn); + for (; !isLast; ++itemIdx) { - loopRendered = true; - loopVar["index"] = InternalValue(itemIdx + 1); - loopVar["index0"] = InternalValue(itemIdx); - loopVar["first"] = InternalValue(itemIdx == 0); - loopVar["last"] = InternalValue(itemIdx == itemsNum - 1); + prevValue = std::move(curValue); if (itemIdx != 0) - loopVar["previtem"] = loopItems.GetValueByIndex(static_cast(itemIdx - 1)); - if (itemIdx != itemsNum - 1) - loopVar["nextitem"] = loopItems.GetValueByIndex(static_cast(itemIdx + 1)); + { + std::swap(curValue, nextValue); + loopVar["previtem"s] = prevValue; + } + else + curValue = enumerator->GetCurrent(); + + isLast = !enumerator->MoveNext(); + if (!isLast) + { + nextValue = enumerator->GetCurrent(); + loopVar["nextitem"s] = nextValue; + } else - loopVar.erase("nextitem"); + loopVar.erase("nextitem"s); + + loopRendered = true; + loopVar["index"s] = static_cast(itemIdx + 1); + loopVar["index0"s] = static_cast(itemIdx); + loopVar["first"s] = itemIdx == 0; + loopVar["last"s] = isLast; - const auto& curValue = loopItems.GetValueByIndex(static_cast(itemIdx)); if (m_vars.size() > 1) { + const auto& valList = ConvertToList(curValue, isConverted); + if (!isConverted) + continue; + + auto b = valList.begin(); + auto e = valList.end(); + for (auto& varName : m_vars) - context[varName] = Subscript(curValue, varName); + { + if (b == e) + continue; + context[varName] = *b; + ++ b; + } } else context[m_vars[0]] = curValue; + values.EnterScope(); m_mainBody->Render(os, values); + values.ExitScope(); } if (!loopRendered && m_elseBody) @@ -109,6 +163,37 @@ void ForStatement::RenderLoop(const InternalValue& loopVal, OutStream& os, Rende values.ExitScope(); } +ListAdapter ForStatement::CreateFilteredAdapter(const ListAdapter& loopItems, RenderContext& values) const +{ + return ListAdapter::CreateAdapter([e = loopItems.GetEnumerator(), this, &values]() mutable { + using ResultType = nonstd::optional; + + auto& tempContext = values.EnterScope(); + for (bool finish = !e->MoveNext(); !finish; finish = !e->MoveNext()) + { + auto curValue = e->GetCurrent(); + if (m_vars.size() > 1) + { + for (auto& varName : m_vars) + tempContext[varName] = Subscript(curValue, varName, &values); + } + else + { + tempContext[m_vars[0]] = curValue; + } + + if (ConvertToBool(m_ifExpr->Evaluate(values))) + { + values.ExitScope(); + return ResultType(std::move(curValue)); + } + } + values.ExitScope(); + + return ResultType(); + }); +} + void IfStatement::Render(OutStream& os, RenderContext& values) { InternalValue val = m_expr->Evaluate(values); @@ -143,22 +228,47 @@ void ElseBranchStatement::Render(OutStream& os, RenderContext& values) m_mainBody->Render(os, values); } -void SetStatement::Render(OutStream&, RenderContext& values) -{ - if (m_expr) - { - InternalValue val = m_expr->Evaluate(values); - if (m_fields.size() == 1) - values.GetCurrentScope()[m_fields[0]] = val; - else - { - for (auto& name : m_fields) - values.GetCurrentScope()[name] = Subscript(val, name); - } - } +void SetStatement::AssignBody(InternalValue body, RenderContext& values) +{ + auto& scope = values.GetCurrentScope(); + if (m_fields.size() == 1) + scope[m_fields.front()] = std::move(body); + else + { + for (const auto& name : m_fields) + scope[name] = Subscript(body, name, &values); + } +} + +void SetLineStatement::Render(OutStream&, RenderContext& values) +{ + if (!m_expr) + return; + AssignBody(m_expr->Evaluate(values), values); +} + +InternalValue SetBlockStatement::RenderBody(RenderContext& values) +{ + TargetString result; + auto stream = values.GetRendererCallback()->GetStreamOnString(result); + auto innerValues = values.Clone(true); + m_body->Render(stream, innerValues); + return result; +} + +void SetRawBlockStatement::Render(OutStream&, RenderContext& values) +{ + AssignBody(RenderBody(values), values); +} + +void SetFilteredBlockStatement::Render(OutStream&, RenderContext& values) +{ + if (!m_expr) + return; + AssignBody(m_expr->Evaluate(RenderBody(values), values), values); } -class BlocksRenderer : public RendererBase +class IBlocksRenderer : public IRendererBase { public: virtual bool HasBlock(const std::string& blockName) = 0; @@ -171,21 +281,24 @@ void ParentBlockStatement::Render(OutStream& os, RenderContext& values) bool found = false; auto parentTplVal = values.FindValue("$$__parent_template", found); if (!found) + { + m_mainBody->Render(os, values); return; + } bool isConverted = false; auto parentTplsList = ConvertToList(parentTplVal->second, isConverted); if (!isConverted) return; - BlocksRenderer* blockRenderer = nullptr; // static_cast(*parentTplPtr); + IBlocksRenderer* blockRenderer = nullptr; // static_cast(*parentTplPtr); for (auto& tplVal : parentTplsList) { - auto ptr = boost::get(&tplVal); + auto ptr = GetIf(&tplVal); if (!ptr) continue; - auto parentTplPtr = static_cast(*ptr); + auto parentTplPtr = static_cast(ptr->get()); if (parentTplPtr->HasBlock(m_name)) { @@ -195,14 +308,15 @@ void ParentBlockStatement::Render(OutStream& os, RenderContext& values) } if (!blockRenderer) + { + m_mainBody->Render(os, values); return; - + } auto& scope = innerContext.EnterScope(); - scope["$$__super_block"] = static_cast(this); - scope["super"] = Callable([this](const CallParams&, OutStream& stream, RenderContext& context) { - m_mainBody->Render(stream, context); - }); + scope["$$__super_block"] = RendererPtr(this, boost::null_deleter()); + scope["super"] = + Callable(Callable::SpecialFunc, [this](const CallParams&, OutStream& stream, RenderContext& context) { m_mainBody->Render(stream, context); }); if (!m_isScoped) scope["$$__parent_template"] = parentTplsList; @@ -210,11 +324,11 @@ void ParentBlockStatement::Render(OutStream& os, RenderContext& values) innerContext.ExitScope(); auto& globalScope = values.GetGlobalScope(); - auto selfMap = boost::get(&globalScope[std::string("self")]); + auto selfMap = GetIf(&globalScope[std::string("self")]); if (!selfMap->HasValue(m_name)) - selfMap->SetValue(m_name, Callable([this](const CallParams&, OutStream& stream, RenderContext& context) { - Render(stream, context); - })); + selfMap->SetValue(m_name, MakeWrapped(Callable(Callable::SpecialFunc, [this](const CallParams&, OutStream& stream, RenderContext& context) { + Render(stream, context); + }))); } void BlockStatement::Render(OutStream& os, RenderContext& values) @@ -223,7 +337,7 @@ void BlockStatement::Render(OutStream& os, RenderContext& values) } template -class ParentTemplateRenderer : public BlocksRenderer +class ParentTemplateRenderer : public IBlocksRenderer { public: ParentTemplateRenderer(std::shared_ptr> tpl, ExtendsStatement::BlocksCollection* blocks) @@ -236,7 +350,7 @@ class ParentTemplateRenderer : public BlocksRenderer { auto& scope = values.GetCurrentScope(); InternalValueList parentTemplates; - parentTemplates.push_back(InternalValue(static_cast(this))); + parentTemplates.push_back(InternalValue(RendererPtr(this, boost::null_deleter()))); bool isFound = false; auto p = values.FindValue("$$__parent_template", isFound); if (isFound) @@ -262,9 +376,20 @@ class ParentTemplateRenderer : public BlocksRenderer p->second->Render(os, values); } - bool HasBlock(const std::string &blockName) override + bool HasBlock(const std::string& blockName) override { return m_blocks->count(blockName) != 0; } + + bool IsEqual(const IComparable& other) const override { - return m_blocks->count(blockName) != 0; + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_template != val->m_template) + return false; + if (m_blocks && val->m_blocks && *m_blocks != *(val->m_blocks)) + return false; + if ((m_blocks && !val->m_blocks) || (!m_blocks && val->m_blocks)) + return false; + return true; } private: @@ -272,28 +397,48 @@ class ParentTemplateRenderer : public BlocksRenderer ExtendsStatement::BlocksCollection* m_blocks; }; -struct TemplateImplVisitor : public boost::static_visitor +template +struct TemplateImplVisitor { - ExtendsStatement::BlocksCollection* m_blocks; - - TemplateImplVisitor(ExtendsStatement::BlocksCollection* blocks) - : m_blocks(blocks) - {} + // ExtendsStatement::BlocksCollection* m_blocks; + const Fn& m_fn; + bool m_throwError{}; - template - RendererPtr operator()(nonstd::expected>, ErrorInfoTpl> tpl) const + explicit TemplateImplVisitor(const Fn& fn, bool throwError) + : m_fn(fn) + , m_throwError(throwError) { - if (!tpl) - return RendererPtr(); - return std::make_shared>(tpl.value(), m_blocks); } - RendererPtr operator()(EmptyValue) const + template + Result operator()(nonstd::expected>, ErrorInfoTpl> tpl) const { - return RendererPtr(); + if (!m_throwError && !tpl) + { + return Result{}; + } + else if (!tpl) + { + throw tpl.error(); + } + return m_fn(tpl.value()); } + + Result operator()(EmptyValue) const { return Result(); } }; +template +Result VisitTemplateImpl(Arg&& tpl, bool throwError, Fn&& fn) +{ + return visit(TemplateImplVisitor(fn, throwError), tpl); +} + +template class RendererTpl, typename CharT, typename... Args> +auto CreateTemplateRenderer(std::shared_ptr> tpl, Args&&... args) +{ + return std::make_shared>(tpl, std::forward(args)...); +} + void ExtendsStatement::Render(OutStream& os, RenderContext& values) { if (!m_isPath) @@ -302,41 +447,286 @@ void ExtendsStatement::Render(OutStream& os, RenderContext& values) return; } auto tpl = values.GetRendererCallback()->LoadTemplate(m_templateName); - auto renderer = boost::apply_visitor(TemplateImplVisitor(&m_blocks), tpl); + auto renderer = + VisitTemplateImpl(tpl, true, [this](auto tplPtr) { return CreateTemplateRenderer(tplPtr, &m_blocks); }); if (renderer) renderer->Render(os, values); } -void MacroStatement::PrepareMacroParams(RenderContext& values) +template +class IncludedTemplateRenderer : public IRendererBase +{ +public: + IncludedTemplateRenderer(std::shared_ptr> tpl, bool withContext) + : m_template(tpl) + , m_withContext(withContext) + { + } + + void Render(OutStream& os, RenderContext& values) override + { + RenderContext innerContext = values.Clone(m_withContext); + if (m_withContext) + innerContext.EnterScope(); + + m_template->GetRenderer()->Render(os, innerContext); + if (m_withContext) + { + auto& innerScope = innerContext.GetCurrentScope(); + auto& scope = values.GetCurrentScope(); + for (auto& v : innerScope) + { + scope[v.first] = std::move(v.second); + } + } + } + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast*>(&other); + if (!val) + return false; + if (m_template != val->m_template) + return false; + if (m_withContext != val->m_withContext) + return false; + return true; + } + +private: + std::shared_ptr> m_template; + bool m_withContext{}; +}; + +void IncludeStatement::Render(OutStream& os, RenderContext& values) +{ + auto templateNames = m_expr->Evaluate(values); + bool isConverted = false; + ListAdapter list = ConvertToList(templateNames, isConverted); + + auto doRender = [this, &values, &os](auto&& name) -> bool { + auto tpl = values.GetRendererCallback()->LoadTemplate(name); + + try + { + auto renderer = VisitTemplateImpl( + tpl, true, [this](auto tplPtr) { return CreateTemplateRenderer(tplPtr, m_withContext); }); + + if (renderer) + { + renderer->Render(os, values); + return true; + } + } + catch (const ErrorInfoTpl& err) + { + if (err.GetCode() != ErrorCode::FileNotFound) + throw; + } + catch (const ErrorInfoTpl& err) + { + if (err.GetCode() != ErrorCode::FileNotFound) + throw; + } + + return false; + }; + + bool rendered = false; + if (isConverted) + { + for (auto& name : list) + { + rendered = doRender(name); + if (rendered) + break; + } + } + else + { + rendered = doRender(templateNames); + } + + if (!rendered && !m_ignoreMissing) + { + InternalValueList files; + ValuesList extraParams; + if (isConverted) + { + extraParams.push_back(IntValue2Value(templateNames)); + } + else + { + files.push_back(templateNames); + extraParams.push_back(IntValue2Value(ListAdapter::CreateAdapter(std::move(files)))); + } + + values.GetRendererCallback()->ThrowRuntimeError(ErrorCode::TemplateNotFound, std::move(extraParams)); + } +} + +class ImportedMacroRenderer : public IRendererBase +{ +public: + explicit ImportedMacroRenderer(InternalValueMap&& map, bool withContext) + : m_importedContext(std::move(map)) + , m_withContext(withContext) + { + } + + void Render(OutStream& /*os*/, RenderContext& /*values*/) override {} + + void InvokeMacro(const Callable& callable, const CallParams& params, OutStream& stream, RenderContext& context) + { + auto ctx = context.Clone(m_withContext); + ctx.BindScope(&m_importedContext); + callable.GetStatementCallable()(params, stream, ctx); + } + + static void InvokeMacro(const std::string& contextName, const Callable& callable, const CallParams& params, OutStream& stream, RenderContext& context) + { + bool contextValFound = false; + auto contextVal = context.FindValue(contextName, contextValFound); + if (!contextValFound) + return; + + auto rendererPtr = GetIf(&contextVal->second); + if (!rendererPtr) + return; + + auto renderer = static_cast(rendererPtr->get()); + renderer->InvokeMacro(callable, params, stream, context); + } + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_importedContext != val->m_importedContext) + return false; + if (m_withContext != val->m_withContext) + return false; + return true; + } + +private: + InternalValueMap m_importedContext; + bool m_withContext{}; +}; + +void ImportStatement::Render(OutStream& /*os*/, RenderContext& values) +{ + auto name = m_nameExpr->Evaluate(values); + + if (!m_renderer) + { + auto tpl = values.GetRendererCallback()->LoadTemplate(name); + m_renderer = VisitTemplateImpl(tpl, true, [](auto tplPtr) { return CreateTemplateRenderer(tplPtr, true); }); + } + + if (!m_renderer) + return; + + std::string scopeName; + { + TargetString tsScopeName = values.GetRendererCallback()->GetAsTargetString(name); + scopeName = "$$_imported_" + GetAsSameString(scopeName, tsScopeName).value(); + } + + TargetString str; + auto tmpStream = values.GetRendererCallback()->GetStreamOnString(str); + + RenderContext newContext = values.Clone(m_withContext); + InternalValueMap importedScope; + { + auto& intImportedScope = newContext.EnterScope(); + m_renderer->Render(tmpStream, newContext); + importedScope = std::move(intImportedScope); + } + + ImportNames(values, importedScope, scopeName); + values.GetCurrentScope()[scopeName] = + std::static_pointer_cast(std::make_shared(std::move(importedScope), m_withContext)); +} + +void ImportStatement::ImportNames(RenderContext& values, InternalValueMap& importedScope, const std::string& scopeName) const +{ + InternalValueMap importedNs; + + for (auto& var : importedScope) + { + if (var.first.empty()) + continue; + + if (var.first[0] == '_') + continue; + + auto mappedP = m_namesToImport.find(var.first); + if (!m_namespace && mappedP == m_namesToImport.end()) + continue; + + InternalValue imported; + auto callable = GetIf(&var.second); + if (!callable) + { + imported = std::move(var.second); + } + else if (callable->GetKind() == Callable::Macro) + { + imported = Callable(Callable::Macro, [fn = std::move(*callable), scopeName](const CallParams& params, OutStream& stream, RenderContext& context) { + ImportedMacroRenderer::InvokeMacro(scopeName, fn, params, stream, context); + }); + } + else + { + continue; + } + + if (m_namespace) + importedNs[var.first] = std::move(imported); + else + values.GetCurrentScope()[mappedP->second] = std::move(imported); + } + + if (m_namespace) + values.GetCurrentScope()[m_namespace.value()] = CreateMapAdapter(std::move(importedNs)); +} + +std::vector MacroStatement::PrepareMacroParams(RenderContext& values) { + std::vector preparedParams; + 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)); + preparedParams.push_back(std::move(info)); } + + return preparedParams; } -void MacroStatement::Render(OutStream& os, RenderContext& values) +void MacroStatement::Render(OutStream&, RenderContext& values) { - PrepareMacroParams(values); + auto p = PrepareMacroParams(values); - values.GetCurrentScope()[m_name] = Callable([this](const CallParams& callParams, OutStream& stream, RenderContext& context) { - InvokeMacroRenderer(callParams, stream, context); - }); + values.GetCurrentScope()[m_name] = Callable(Callable::Macro, [this, params = std::move(p)](const CallParams& callParams, OutStream& stream, RenderContext& context) { + InvokeMacroRenderer(params, callParams, stream, context); + }); } -void MacroStatement::InvokeMacroRenderer(const CallParams& callParams, OutStream& stream, RenderContext& context) +void MacroStatement::InvokeMacroRenderer(const std::vector& params, const CallParams& callParams, OutStream& stream, RenderContext& context) { InternalValueMap callArgs; InternalValueMap kwArgs; InternalValueList varArgs; - SetupCallArgs(m_preparedParams, callParams, context, callArgs, kwArgs, varArgs); + SetupCallArgs(params, callParams, context, callArgs, kwArgs, varArgs); InternalValueList arguments; InternalValueList defaults; - for (auto& a : m_preparedParams) + for (auto& a : params) { arguments.emplace_back(a.name); defaults.emplace_back(a.defaultVal); @@ -346,34 +736,39 @@ void MacroStatement::InvokeMacroRenderer(const CallParams& callParams, OutStream 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["kwargs"s] = CreateMapAdapter(std::move(kwArgs)); + scope["varargs"s] = ListAdapter::CreateAdapter(std::move(varArgs)); - scope["name"] = m_name; - scope["arguments"] = ListAdapter::CreateAdapter(std::move(arguments)); - scope["defaults"] = ListAdapter::CreateAdapter(std::move(defaults)); + scope["name"s] = static_cast(m_name); + scope["arguments"s] = ListAdapter::CreateAdapter(std::move(arguments)); + scope["defaults"s] = 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) +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); + callArgs[a.first] = std::move(a.second); for (auto& a : args.extraKwArgs) - kwArgs[a.first] = a.second->Evaluate(context); + kwArgs[a.first] = std::move(a.second); for (auto& a : args.extraPosArgs) - varArgs.push_back(a->Evaluate(context)); + varArgs.push_back(std::move(a)); } -void MacroStatement::SetupMacroScope(InternalValueMap& scope) +void MacroStatement::SetupMacroScope(InternalValueMap&) { ; } @@ -386,11 +781,10 @@ void MacroCallStatement::Render(OutStream& os, RenderContext& values) return; auto& fnVal = macroPtr->second; - const Callable* callable = boost::get(&fnVal); + const Callable* callable = GetIf(&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(); @@ -398,11 +792,14 @@ void MacroCallStatement::Render(OutStream& os, RenderContext& values) if (hasCallerVal) prevCaller = callerP->second; - curScope["caller"] = Callable([this](const CallParams& callParams, OutStream& stream, RenderContext& context) { - InvokeMacroRenderer(callParams, stream, context); + auto p = PrepareMacroParams(values); + + curScope["caller"] = Callable(Callable::Macro, [this, params = std::move(p)](const CallParams& callParams, OutStream& stream, RenderContext& context) { + InvokeMacroRenderer(params, callParams, stream, context); }); - callable->GetStatementCallable()(m_callParams, os, values); + auto callParams = helpers::EvaluateCallParams(m_callParams, values); + callable->GetStatementCallable()(callParams, os, values); if (hasCallerVal) curScope["caller"] = prevCaller; @@ -410,10 +807,33 @@ void MacroCallStatement::Render(OutStream& os, RenderContext& values) values.GetCurrentScope().erase("caller"); } -void MacroCallStatement::SetupMacroScope(InternalValueMap& scope) -{ +void MacroCallStatement::SetupMacroScope(InternalValueMap&) {} +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); -} // jinja2 + m_mainBody->Render(os, innerValues); + + innerValues.ExitScope(); +} + +void FilterStatement::Render(OutStream& os, RenderContext& values) +{ + TargetString arg; + auto argStream = values.GetRendererCallback()->GetStreamOnString(arg); + auto innerValues = values.Clone(true); + m_body->Render(argStream, innerValues); + const auto result = m_expr->Evaluate(std::move(arg), values); + os.WriteValue(result); +} +} // namespace jinja2 diff --git a/src/statements.h b/src/statements.h index d3440366..eed6e2c0 100644 --- a/src/statements.h +++ b/src/statements.h @@ -1,5 +1,5 @@ -#ifndef STATEMENTS_H -#define STATEMENTS_H +#ifndef JINJA2CPP_SRC_STATEMENTS_H +#define JINJA2CPP_SRC_STATEMENTS_H #include "renderer.h" #include "expression_evaluator.h" @@ -9,8 +9,10 @@ namespace jinja2 { -struct Statement : public RendererBase +class Statement : public VisitableRendererBase { +public: + VISITABLE_STATEMENT(); }; template @@ -24,12 +26,22 @@ struct MacroParam std::string paramName; ExpressionEvaluatorPtr<> defaultValue; }; +inline bool operator==(const MacroParam& lhs, const MacroParam& rhs) +{ + if (lhs.paramName != rhs.paramName) + return false; + if (lhs.defaultValue != rhs.defaultValue) + return false; + return true; +} using MacroParams = std::vector; class ForStatement : public Statement { public: + VISITABLE_STATEMENT(); + ForStatement(std::vector vars, ExpressionEvaluatorPtr<> expr, ExpressionEvaluatorPtr<> ifExpr, bool isRecursive) : m_vars(std::move(vars)) , m_value(expr) @@ -40,24 +52,46 @@ class ForStatement : public Statement void SetMainBody(RendererPtr renderer) { - m_mainBody = renderer; + m_mainBody = std::move(renderer); } void SetElseBody(RendererPtr renderer) { - m_elseBody = renderer; + m_elseBody = std::move(renderer); } void Render(OutStream& os, RenderContext& values) override; + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_vars != val->m_vars) + return false; + if (m_value != val->m_value) + return false; + if (m_ifExpr != val->m_ifExpr) + return false; + if (m_isRecursive != val->m_isRecursive) + return false; + if (m_mainBody != val->m_mainBody) + return false; + if (m_elseBody != val->m_elseBody) + return false; + return true; + } + private: - void RenderLoop(const InternalValue& val, OutStream& os, RenderContext& values); + void RenderLoop(const InternalValue &loopVal, OutStream &os, + RenderContext &values, int level); + ListAdapter CreateFilteredAdapter(const ListAdapter& loopItems, RenderContext& values) const; private: std::vector m_vars; ExpressionEvaluatorPtr<> m_value; ExpressionEvaluatorPtr<> m_ifExpr; - bool m_isRecursive; + bool m_isRecursive{}; RendererPtr m_mainBody; RendererPtr m_elseBody; }; @@ -67,6 +101,8 @@ class ElseBranchStatement; class IfStatement : public Statement { public: + VISITABLE_STATEMENT(); + IfStatement(ExpressionEvaluatorPtr<> expr) : m_expr(expr) { @@ -74,7 +110,7 @@ class IfStatement : public Statement void SetMainBody(RendererPtr renderer) { - m_mainBody = renderer; + m_mainBody = std::move(renderer); } void AddElseBranch(StatementPtr branch) @@ -84,6 +120,19 @@ class IfStatement : public Statement void Render(OutStream& os, RenderContext& values) override; + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_expr != val->m_expr) + return false; + if (m_mainBody != val->m_mainBody) + return false; + if (m_elseBranches != val->m_elseBranches) + return false; + return true; + } private: ExpressionEvaluatorPtr<> m_expr; RendererPtr m_mainBody; @@ -94,6 +143,8 @@ class IfStatement : public Statement class ElseBranchStatement : public Statement { public: + VISITABLE_STATEMENT(); + ElseBranchStatement(ExpressionEvaluatorPtr<> expr) : m_expr(expr) { @@ -102,9 +153,20 @@ class ElseBranchStatement : public Statement bool ShouldRender(RenderContext& values) const; void SetMainBody(RendererPtr renderer) { - m_mainBody = renderer; + m_mainBody = std::move(renderer); } void Render(OutStream& os, RenderContext& values) override; + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_expr != val->m_expr) + return false; + if (m_mainBody != val->m_mainBody) + return false; + return true; + } private: ExpressionEvaluatorPtr<> m_expr; @@ -119,20 +181,128 @@ class SetStatement : public Statement { } - void SetAssignmentExpr(ExpressionEvaluatorPtr<> expr) + bool IsEqual(const IComparable& other) const override { - m_expr = expr; + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_fields != val->m_fields) + return false; + return true; } +protected: + void AssignBody(InternalValue, RenderContext&); + +private: + const std::vector m_fields; +}; + +class SetLineStatement final : public SetStatement +{ +public: + VISITABLE_STATEMENT(); + + SetLineStatement(std::vector fields, ExpressionEvaluatorPtr<> expr) + : SetStatement(std::move(fields)), m_expr(std::move(expr)) + { + } + void Render(OutStream& os, RenderContext& values) override; + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_expr != val->m_expr) + return false; + return true; + } private: - std::vector m_fields; - ExpressionEvaluatorPtr<> m_expr; + const ExpressionEvaluatorPtr<> m_expr; +}; + +class SetBlockStatement : public SetStatement +{ +public: + using SetStatement::SetStatement; + + void SetBody(RendererPtr renderer) + { + m_body = std::move(renderer); + } + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (!SetStatement::IsEqual(*val)) + return false; + if (m_body != val->m_body) + return false; + return true; + } +protected: + InternalValue RenderBody(RenderContext&); + +private: + RendererPtr m_body; +}; + +class SetRawBlockStatement final : public SetBlockStatement +{ +public: + VISITABLE_STATEMENT(); + + using SetBlockStatement::SetBlockStatement; + + void Render(OutStream&, RenderContext&) override; + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (!SetBlockStatement::IsEqual(*val)) + return false; + return true; + } +}; + +class SetFilteredBlockStatement final : public SetBlockStatement +{ +public: + VISITABLE_STATEMENT(); + + explicit SetFilteredBlockStatement(std::vector fields, ExpressionEvaluatorPtr expr) + : SetBlockStatement(std::move(fields)), m_expr(std::move(expr)) + { + } + + void Render(OutStream&, RenderContext&) override; + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (!SetBlockStatement::IsEqual(*val)) + return false; + if (m_expr != val->m_expr) + return false; + return true; + } + +private: + const ExpressionEvaluatorPtr m_expr; }; class ParentBlockStatement : public Statement { public: + VISITABLE_STATEMENT(); + ParentBlockStatement(std::string name, bool isScoped) : m_name(std::move(name)) , m_isScoped(isScoped) @@ -141,19 +311,35 @@ class ParentBlockStatement : public Statement void SetMainBody(RendererPtr renderer) { - m_mainBody = renderer; + m_mainBody = std::move(renderer); } void Render(OutStream &os, RenderContext &values) override; + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_name != val->m_name) + return false; + if (m_isScoped != val->m_isScoped) + return false; + if (m_mainBody != val->m_mainBody) + return false; + return true; + } + private: std::string m_name; - bool m_isScoped; + bool m_isScoped{}; RendererPtr m_mainBody; }; class BlockStatement : public Statement { public: + VISITABLE_STATEMENT(); + BlockStatement(std::string name) : m_name(std::move(name)) { @@ -163,10 +349,21 @@ class BlockStatement : public Statement void SetMainBody(RendererPtr renderer) { - m_mainBody = renderer; + m_mainBody = std::move(renderer); } void Render(OutStream &os, RenderContext &values) override; + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_name != val->m_name) + return false; + if (m_mainBody != val->m_mainBody) + return false; + return true; + } private: std::string m_name; RendererPtr m_mainBody; @@ -175,6 +372,8 @@ class BlockStatement : public Statement class ExtendsStatement : public Statement { public: + VISITABLE_STATEMENT(); + using BlocksCollection = std::unordered_map>; ExtendsStatement(std::string name, bool isPath) @@ -188,16 +387,120 @@ class ExtendsStatement : public Statement { m_blocks[block->GetName()] = block; } + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_templateName != val->m_templateName) + return false; + if (m_isPath != val->m_isPath) + return false; + if (m_blocks != val->m_blocks) + return false; + return true; + } private: std::string m_templateName; - bool m_isPath; + bool m_isPath{}; BlocksCollection m_blocks; void DoRender(OutStream &os, RenderContext &values); }; +class IncludeStatement : public Statement +{ +public: + VISITABLE_STATEMENT(); + + IncludeStatement(bool ignoreMissing, bool withContext) + : m_ignoreMissing(ignoreMissing) + , m_withContext(withContext) + {} + + void SetIncludeNamesExpr(ExpressionEvaluatorPtr<> expr) + { + m_expr = std::move(expr); + } + + void Render(OutStream& os, RenderContext& values) override; + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_ignoreMissing != val->m_ignoreMissing) + return false; + if (m_withContext != val->m_withContext) + return false; + if (m_expr != val->m_expr) + return false; + return true; + } +private: + bool m_ignoreMissing{}; + bool m_withContext{}; + ExpressionEvaluatorPtr<> m_expr; +}; + +class ImportStatement : public Statement +{ +public: + VISITABLE_STATEMENT(); + + explicit ImportStatement(bool withContext) + : m_withContext(withContext) + {} + + void SetImportNameExpr(ExpressionEvaluatorPtr<> expr) + { + m_nameExpr = std::move(expr); + } + + void SetNamespace(std::string name) + { + m_namespace = std::move(name); + } + + void AddNameToImport(std::string name, std::string alias) + { + m_namesToImport[std::move(name)] = std::move(alias); + } + + void Render(OutStream& os, RenderContext& values) override; + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_namespace != val->m_namespace) + return false; + if (m_withContext != val->m_withContext) + return false; + if (m_namesToImport != val->m_namesToImport) + return false; + if (m_nameExpr != val->m_nameExpr) + return false; + if (m_renderer != val->m_renderer) + return false; + return true; + } +private: + void ImportNames(RenderContext& values, InternalValueMap& importedScope, const std::string& scopeName) const; + +private: + bool m_withContext{}; + RendererPtr m_renderer; + ExpressionEvaluatorPtr<> m_nameExpr; + nonstd::optional m_namespace; + std::unordered_map m_namesToImport; +}; + class MacroStatement : public Statement { public: + VISITABLE_STATEMENT(); + MacroStatement(std::string name, MacroParams params) : m_name(std::move(name)) , m_params(std::move(params)) @@ -206,27 +509,43 @@ class MacroStatement : public Statement void SetMainBody(RendererPtr renderer) { - m_mainBody = renderer; + m_mainBody = std::move(renderer); } + void Render(OutStream &os, RenderContext &values) override; + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_name != val->m_name) + return false; + if (m_params != val->m_params) + return false; + if (m_mainBody != val->m_mainBody) + return false; + return true; + } + protected: - void InvokeMacroRenderer(const CallParams& callParams, OutStream& stream, RenderContext& context); + void InvokeMacroRenderer(const std::vector& params, 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); + std::vector PrepareMacroParams(RenderContext& values); 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) + VISITABLE_STATEMENT(); + + MacroCallStatement(std::string macroName, CallParamsInfo callParams, MacroParams callbackParams) : MacroStatement("$call$", std::move(callbackParams)) , m_macroName(std::move(macroName)) , m_callParams(std::move(callParams)) @@ -235,13 +554,108 @@ class MacroCallStatement : public MacroStatement void Render(OutStream &os, RenderContext &values) override; + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_macroName != val->m_macroName) + return false; + if (m_callParams != val->m_callParams) + return false; + return true; + } protected: void SetupMacroScope(InternalValueMap& scope) override; protected: std::string m_macroName; - CallParams m_callParams; + CallParamsInfo m_callParams; +}; + +class DoStatement : public Statement +{ +public: + VISITABLE_STATEMENT(); + + DoStatement(ExpressionEvaluatorPtr<> expr) : m_expr(expr) {} + + void Render(OutStream &os, RenderContext &values) override; + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_expr != val->m_expr) + return false; + return true; + } +private: + ExpressionEvaluatorPtr<> m_expr; }; -} // jinja2 -#endif // STATEMENTS_H +class WithStatement : public Statement +{ +public: + VISITABLE_STATEMENT(); + + void SetScopeVars(std::vector>> vars) + { + m_scopeVars = std::move(vars); + } + void SetMainBody(RendererPtr renderer) + { + m_mainBody = std::move(renderer); + } + + void Render(OutStream &os, RenderContext &values) override; + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_scopeVars != val->m_scopeVars) + return false; + if (m_mainBody != val->m_mainBody) + return false; + return true; + } +private: + std::vector>> m_scopeVars; + RendererPtr m_mainBody; +}; + +class FilterStatement : public Statement +{ +public: + VISITABLE_STATEMENT(); + + explicit FilterStatement(ExpressionEvaluatorPtr expr) + : m_expr(std::move(expr)) {} + + void SetBody(RendererPtr renderer) + { + m_body = std::move(renderer); + } + + void Render(OutStream &, RenderContext &) override; + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + if (m_expr != val->m_expr) + return false; + if (m_body != val->m_body) + return false; + return true; + } +private: + ExpressionEvaluatorPtr m_expr; + RendererPtr m_body; +}; + +} // namespace jinja2 + +#endif // JINJA2CPP_SRC_STATEMENTS_H diff --git a/src/string_converter_filter.cpp b/src/string_converter_filter.cpp index a51e0e2f..6c151ec7 100644 --- a/src/string_converter_filter.cpp +++ b/src/string_converter_filter.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -19,12 +20,12 @@ namespace filters { template -struct StringEncoder : public visitors::BaseVisitor<> +struct StringEncoder : public visitors::BaseVisitor { using BaseVisitor::operator(); template - InternalValue operator() (const std::basic_string& str) const + TargetString operator() (const std::basic_string& str) const { std::basic_string result; @@ -33,7 +34,20 @@ struct StringEncoder : public visitors::BaseVisitor<> static_cast(this)->EncodeChar(ch, [&result](auto ... chs) {AppendChar(result, chs...);}); } - return result; + return TargetString(std::move(result)); + } + + template + TargetString operator() (const nonstd::basic_string_view& str) const + { + std::basic_string result; + + for (auto& ch : str) + { + static_cast(this)->EncodeChar(ch, [&result](auto ... chs) {AppendChar(result, chs...);}); + } + + return TargetString(std::move(result)); } template @@ -162,12 +176,16 @@ StringConverter::StringConverter(FilterParams params, StringConverter::Mode mode case TruncateMode: ParseParams({{"length", false, static_cast(255)}, {"killwords", false, false}, {"end", false, std::string("...")}, {"leeway", false}}, params); break; + case CenterMode: + ParseParams({{"width", false, static_cast(80)}}, params); + break; + default: break; } } InternalValue StringConverter::Filter(const InternalValue& baseVal, RenderContext& context) { - InternalValue result; + TargetString result; auto isAlpha = ba::is_alpha(); auto isAlNum = ba::is_alnum(); @@ -175,9 +193,10 @@ InternalValue StringConverter::Filter(const InternalValue& baseVal, RenderContex switch (m_mode) { case TrimMode: - result = ApplyStringConverter(baseVal, [](auto str) -> InternalValue { + result = ApplyStringConverter(baseVal, [](auto strView) -> TargetString { + auto str = sv_to_string(strView); ba::trim_all(str); - return str; + return TargetString(str); }); break; case TitleMode: @@ -196,7 +215,7 @@ InternalValue StringConverter::Filter(const InternalValue& baseVal, RenderContex case WordCountMode: { int64_t wc = 0; - ApplyStringConverter(baseVal, [isDelim = true, &wc, &isAlpha, &isAlNum](auto ch, auto&& fn) mutable { + ApplyStringConverter(baseVal, [isDelim = true, &wc, &isAlNum](auto ch, auto&&) mutable { if (isDelim && isAlNum(ch)) { isDelim = false; @@ -205,8 +224,7 @@ InternalValue StringConverter::Filter(const InternalValue& baseVal, RenderContex } isDelim = !isAlNum(ch); }); - result = wc; - break; + return InternalValue(wc); } case UpperMode: result = ApplyStringConverter(baseVal, [&isAlpha](auto ch, auto&& fn) mutable { @@ -225,10 +243,14 @@ InternalValue StringConverter::Filter(const InternalValue& baseVal, RenderContex }); break; case ReplaceMode: - result = ApplyStringConverter(baseVal, [this, &context](auto str) -> InternalValue { - auto oldStr = GetAsSameString(str, this->GetArgumentValue("old", context)); - auto newStr = GetAsSameString(str, this->GetArgumentValue("new", context)); + result = ApplyStringConverter(baseVal, [this, &context](auto srcStr) -> TargetString { + std::decay_t emptyStrView; + using CharT = typename decltype(emptyStrView)::value_type; + std::basic_string emptyStr; + auto oldStr = GetAsSameString(srcStr, this->GetArgumentValue("old", context)).value_or(emptyStr); + auto newStr = GetAsSameString(srcStr, this->GetArgumentValue("new", context)).value_or(emptyStr); auto count = ConvertToInt(this->GetArgumentValue("count", context)); + auto str = sv_to_string(srcStr); if (count == 0) ba::replace_all(str, oldStr, newStr); else @@ -240,30 +262,35 @@ InternalValue StringConverter::Filter(const InternalValue& baseVal, RenderContex }); break; case TruncateMode: - result = ApplyStringConverter(baseVal, [this, &context, &isAlNum](auto str) -> InternalValue { + result = ApplyStringConverter(baseVal, [this, &context, &isAlNum](auto srcStr) -> TargetString { + std::decay_t emptyStrView; + using CharT = typename decltype(emptyStrView)::value_type; + std::basic_string emptyStr; auto length = ConvertToInt(this->GetArgumentValue("length", context)); auto killWords = ConvertToBool(this->GetArgumentValue("killwords", context)); - auto end = GetAsSameString(str, this->GetArgumentValue("end", context)); + auto end = GetAsSameString(srcStr, this->GetArgumentValue("end", context)); auto leeway = ConvertToInt(this->GetArgumentValue("leeway", context), 5); - if (str.size() <= length) - return str; + if (static_cast(srcStr.size()) <= length) + return sv_to_string(srcStr); + + auto str = sv_to_string(srcStr); if (killWords) { - if (str.size() > (length + leeway)) + if (static_cast(str.size()) > (length + leeway)) { - str.erase(str.begin() + length, str.end()); - str += end; + str.erase(str.begin() + static_cast(length), str.end()); + str += end.value_or(emptyStr); } return str; } - auto p = str.begin() + length; + auto p = str.begin() + static_cast(length); if (leeway != 0) { for (; leeway != 0 && p != str.end() && isAlNum(*p); -- leeway, ++ p); if (p == str.end()) - return str; + return TargetString(str); } if (isAlNum(*p)) @@ -272,19 +299,97 @@ InternalValue StringConverter::Filter(const InternalValue& baseVal, RenderContex } str.erase(p, str.end()); ba::trim_right(str); - str += end; + str += end.value_or(emptyStr); - return str; + return TargetString(std::move(str)); }); break; case UrlEncodeMode: result = Apply(baseVal); break; + case CapitalMode: + result = ApplyStringConverter(baseVal, [isFirstChar = true, &isAlpha](auto ch, auto&& fn) mutable { + if (isAlpha(ch)) + { + if (isFirstChar) + fn(std::toupper(ch, std::locale())); + else + fn(std::tolower(ch, std::locale())); + } + else + fn(ch); + + isFirstChar = false; + }); + break; + case EscapeHtmlMode: + result = ApplyStringConverter(baseVal, [](auto ch, auto&& fn) mutable { + switch(ch) + { + case '<': + fn('&', 'l', 't', ';'); + break; + case '>': + fn('&', 'g', 't', ';'); + break; + case '&': + fn('&', 'a', 'm', 'p', ';'); + break; + case '\'': + fn('&', '#', '3', '9', ';'); + break; + case '\"': + fn('&', '#', '3', '4', ';'); + break; + default: + fn(ch); + break; + } + }); + break; + case StriptagsMode: + result = ApplyStringConverter(baseVal, [](auto srcStr) -> TargetString { + auto str = sv_to_string(srcStr); + using StringT = decltype(str); + using CharT = typename StringT::value_type; + static const std::basic_regex STRIPTAGS_RE(UNIVERSAL_STR("(|<[^>]*>)").GetValueStr()); + str = std::regex_replace(str, STRIPTAGS_RE, UNIVERSAL_STR("").GetValueStr()); + ba::trim_all(str); + static const StringT html_entities [] { + UNIVERSAL_STR("&").GetValueStr(), UNIVERSAL_STR("&").GetValueStr(), + UNIVERSAL_STR("'").GetValueStr(), UNIVERSAL_STR("\'").GetValueStr(), + UNIVERSAL_STR(">").GetValueStr(), UNIVERSAL_STR(">").GetValueStr(), + UNIVERSAL_STR("<").GetValueStr(), UNIVERSAL_STR("<").GetValueStr(), + UNIVERSAL_STR(""").GetValueStr(), UNIVERSAL_STR("\"").GetValueStr(), + UNIVERSAL_STR("'").GetValueStr(), UNIVERSAL_STR("\'").GetValueStr(), + UNIVERSAL_STR(""").GetValueStr(), UNIVERSAL_STR("\"").GetValueStr(), + }; + for (auto it = std::begin(html_entities), end = std::end(html_entities); it < end; it += 2) + { + ba::replace_all(str, *it, *(it + 1)); + } + return str; + }); + break; + case CenterMode: + result = ApplyStringConverter(baseVal, [this, &context](auto srcStr) -> TargetString { + auto width = ConvertToInt(this->GetArgumentValue("width", context)); + auto str = sv_to_string(srcStr); + auto string_length = static_cast(str.size()); + if (string_length >= width) + return str; + auto whitespaces = width - string_length; + str.insert(0, static_cast(whitespaces + 1) / 2, ' '); + str.append(static_cast(whitespaces / 2), ' '); + return TargetString(std::move(str)); + }); + break; default: break; } - return result; -} + return std::move(result); } -} + +} // namespace filters +} // namespace jinja2 diff --git a/src/template.cpp b/src/template.cpp index d71e3075..27343875 100644 --- a/src/template.cpp +++ b/src/template.cpp @@ -1,11 +1,22 @@ #include "jinja2cpp/template.h" #include "template_impl.h" +#include + #include #include namespace jinja2 { +bool operator==(const Template& lhs, const Template& rhs) +{ + return lhs.IsEqual(rhs); +} + +bool operator==(const TemplateW& lhs, const TemplateW& rhs) +{ + return lhs.IsEqual(rhs); +} template auto GetImpl(std::shared_ptr impl) @@ -19,25 +30,22 @@ Template::Template(TemplateEnv* env) } -Template::~Template() -{ +Template::~Template() = default; -} - -ParseResult Template::Load(const char* tpl, std::string tplName) +Result Template::Load(const char* tpl, std::string tplName) { std::string t(tpl); auto result = GetImpl(m_impl)->Load(std::move(t), std::move(tplName)); - return !result ? ParseResult() : nonstd::make_unexpected(std::move(result.get())); + return !result ? Result() : nonstd::make_unexpected(std::move(result.get())); } -ParseResult Template::Load(const std::string& str, std::string tplName) +Result Template::Load(const std::string& str, std::string tplName) { auto result = GetImpl(m_impl)->Load(str, std::move(tplName)); - return !result ? ParseResult() : nonstd::make_unexpected(std::move(result.get())); + return !result ? Result() : nonstd::make_unexpected(std::move(result.get())); } -ParseResult Template::Load(std::istream& stream, std::string tplName) +Result Template::Load(std::istream& stream, std::string tplName) { std::string t; @@ -51,57 +59,74 @@ ParseResult Template::Load(std::istream& stream, std::string tplName) } auto result = GetImpl(m_impl)->Load(std::move(t), std::move(tplName)); - return !result ? ParseResult() : nonstd::make_unexpected(std::move(result.get())); + return !result ? Result() : nonstd::make_unexpected(std::move(result.get())); } -ParseResult Template::LoadFromFile(const std::string& fileName) +Result Template::LoadFromFile(const std::string& fileName) { std::ifstream file(fileName); if (!file.good()) - return ParseResult(); + return Result(); return Load(file, fileName); } -void Template::Render(std::ostream& os, const jinja2::ValuesMap& params) +Result Template::Render(std::ostream& os, const jinja2::ValuesMap& params) { - GetImpl(m_impl)->Render(os, params); + std::string buffer; + auto result = GetImpl(m_impl)->Render(buffer, params); + + if (!result) + os.write(buffer.data(), buffer.size()); + + return !result ? Result() : nonstd::make_unexpected(std::move(result.get())); } -std::string Template::RenderAsString(const jinja2::ValuesMap& params) +Result Template::RenderAsString(const jinja2::ValuesMap& params) { - std::ostringstream os; - Render(os, params); + std::string buffer; + auto result = GetImpl(m_impl)->Render(buffer, params); + return !result ? Result(std::move(buffer)) : Result(nonstd::make_unexpected(std::move(result.get())));; +} - return os.str(); +Result Template::GetMetadata() +{ + return GetImpl(m_impl)->GetMetadata(); } -TemplateW::TemplateW(TemplateEnv* env) - : m_impl(new TemplateImpl(env)) +Result> Template::GetMetadataRaw() { + return GetImpl(m_impl)->GetMetadataRaw(); +} +bool Template::IsEqual(const Template& other) const +{ + return m_impl == other.m_impl; } -TemplateW::~TemplateW() +TemplateW::TemplateW(TemplateEnv* env) + : m_impl(new TemplateImpl(env)) { } -ParseResultW TemplateW::Load(const wchar_t* tpl, std::string tplName) +TemplateW::~TemplateW() = default; + +ResultW TemplateW::Load(const wchar_t* tpl, std::string tplName) { std::wstring t(tpl); auto result = GetImpl(m_impl)->Load(t, std::move(tplName)); - return !result ? ParseResultW() : nonstd::make_unexpected(std::move(result.get())); + return !result ? ResultW() : nonstd::make_unexpected(std::move(result.get())); } -ParseResultW TemplateW::Load(const std::wstring& str, std::string tplName) +ResultW TemplateW::Load(const std::wstring& str, std::string tplName) { auto result = GetImpl(m_impl)->Load(str, std::move(tplName)); - return !result ? ParseResultW() : nonstd::make_unexpected(std::move(result.get())); + return !result ? ResultW() : nonstd::make_unexpected(std::move(result.get())); } -ParseResultW TemplateW::Load(std::wistream& stream, std::string tplName) +ResultW TemplateW::Load(std::wistream& stream, std::string tplName) { std::wstring t; @@ -115,29 +140,51 @@ ParseResultW TemplateW::Load(std::wistream& stream, std::string tplName) } auto result = GetImpl(m_impl)->Load(t, std::move(tplName)); - return !result ? ParseResultW() : nonstd::make_unexpected(std::move(result.get())); + return !result ? ResultW() : nonstd::make_unexpected(std::move(result.get())); } -ParseResultW TemplateW::LoadFromFile(const std::string& fileName) +ResultW TemplateW::LoadFromFile(const std::string& fileName) { std::wifstream file(fileName); if (!file.good()) - return ParseResultW(); + return ResultW(); return Load(file, fileName); } -void TemplateW::Render(std::wostream& os, const jinja2::ValuesMap& params) +ResultW TemplateW::Render(std::wostream& os, const jinja2::ValuesMap& params) +{ + std::wstring buffer; + auto result = GetImpl(m_impl)->Render(buffer, params); + if (!result) + os.write(buffer.data(), buffer.size()); + return !result ? ResultW() : ResultW(nonstd::make_unexpected(std::move(result.get()))); +} + +ResultW TemplateW::RenderAsString(const jinja2::ValuesMap& params) { - GetImpl(m_impl)->Render(os, params); + std::wstring buffer; + auto result = GetImpl(m_impl)->Render(buffer, params); + + return !result ? buffer : ResultW(nonstd::make_unexpected(std::move(result.get()))); } -std::wstring TemplateW::RenderAsString(const jinja2::ValuesMap& params) +ResultW TemplateW::GetMetadata() { - std::wostringstream os; - GetImpl(m_impl)->Render(os, params); + return GenericMap(); + // GetImpl(m_impl)->GetMetadata(); +} - return os.str(); +ResultW> TemplateW::GetMetadataRaw() +{ + return MetadataInfo(); + // GetImpl(m_impl)->GetMetadataRaw(); + ; } -} // jinga2 +bool TemplateW::IsEqual(const TemplateW& other) const +{ + return m_impl == other.m_impl; +} + +} // namespace jinja2 diff --git a/src/template_env.cpp b/src/template_env.cpp index ba5deb97..239a4f02 100644 --- a/src/template_env.cpp +++ b/src/template_env.cpp @@ -1,7 +1,6 @@ #include #include - namespace jinja2 { template @@ -10,60 +9,87 @@ struct TemplateFunctions; template<> struct TemplateFunctions { - static Template CreateTemplate(TemplateEnv* env) - { - return Template(env); - } - static auto LoadFile(const std::string& fileName, const IFilesystemHandler* fs) - { - return fs->OpenStream(fileName); - } + using ResultType = nonstd::expected; + static Template CreateTemplate(TemplateEnv* env) { return Template(env); } + static auto LoadFile(const std::string& fileName, const IFilesystemHandler* fs) { return fs->OpenStream(fileName); } }; template<> struct TemplateFunctions { - static TemplateW CreateTemplate(TemplateEnv* env) - { - return TemplateW(env); - } - static auto LoadFile(const std::string& fileName, const IFilesystemHandler* fs) - { - return fs->OpenWStream(fileName); - } + using ResultType = nonstd::expected; + static TemplateW CreateTemplate(TemplateEnv* env) { return TemplateW(env); } + static auto LoadFile(const std::string& fileName, const IFilesystemHandler* fs) { return fs->OpenWStream(fileName); } }; -template -auto LoadTemplateImpl(TemplateEnv* env, std::string fileName, const T& filesystemHandlers) +template +auto TemplateEnv::LoadTemplateImpl(TemplateEnv* env, std::string fileName, const T& filesystemHandlers, Cache& cache) { using Functions = TemplateFunctions; - auto result = Functions::CreateTemplate(env); + using ResultType = typename Functions::ResultType; + using ErrorType = typename ResultType::error_type; + auto tpl = Functions::CreateTemplate(env); + + { + std::shared_lock l(m_guard); + auto p = cache.find(fileName); + if (p != cache.end()) + { + if (m_settings.autoReload) + { + auto lastModified = p->second.handler->GetLastModificationDate(fileName); + if (!lastModified || (p->second.lastModification && lastModified.value() <= p->second.lastModification.value())) + return ResultType(p->second.tpl); + } + else + return ResultType(p->second.tpl); + } + } for (auto& fh : filesystemHandlers) { if (!fh.prefix.empty() && fileName.find(fh.prefix) != 0) continue; - auto stream = Functions::LoadFile(fileName, fh.handler.get()); if (stream) { - result.Load(*stream); - break; + auto res = tpl.Load(*stream, fileName); + if (!res) + return ResultType(res.get_unexpected()); + + if (m_settings.cacheSize != 0) + { + auto lastModified = fh.handler->GetLastModificationDate(fileName); + std::unique_lock l(m_guard); + auto& cacheEntry = cache[fileName]; + cacheEntry.tpl = tpl; + cacheEntry.handler = fh.handler; + cacheEntry.lastModification = lastModified; + } + + return ResultType(tpl); } } - return result; + typename ErrorType::Data errorData; + errorData.code = ErrorCode::FileNotFound; + errorData.srcLoc.col = 1; + errorData.srcLoc.line = 1; + errorData.srcLoc.fileName = ""; + errorData.extraParams.push_back(Value(fileName)); + + return ResultType(nonstd::make_unexpected(ErrorType(errorData))); } nonstd::expected TemplateEnv::LoadTemplate(std::string fileName) { - return LoadTemplateImpl(this, std::move(fileName), m_filesystemHandlers); + return LoadTemplateImpl(this, std::move(fileName), m_filesystemHandlers, m_templateCache); } nonstd::expected TemplateEnv::LoadTemplateW(std::string fileName) { - return LoadTemplateImpl(this, std::move(fileName), m_filesystemHandlers); + return LoadTemplateImpl(this, std::move(fileName), m_filesystemHandlers, m_templateWCache); } -} // jinja2 +} // namespace jinja2 diff --git a/src/template_impl.h b/src/template_impl.h index a54ba482..8f4ad3a6 100644 --- a/src/template_impl.h +++ b/src/template_impl.h @@ -1,25 +1,67 @@ -#ifndef TEMPLATE_IMPL_H -#define TEMPLATE_IMPL_H +#ifndef JINJA2CPP_SRC_TEMPLATE_IMPL_H +#define JINJA2CPP_SRC_TEMPLATE_IMPL_H -#include "jinja2cpp/value.h" -#include "jinja2cpp/template_env.h" #include "internal_value.h" +#include "jinja2cpp/binding/rapid_json.h" +#include "jinja2cpp/template_env.h" +#include "jinja2cpp/value.h" #include "renderer.h" #include "template_parser.h" #include "value_visitors.h" #include +#include #include -#include +#include +#include namespace jinja2 { +namespace detail +{ +template +struct RapidJsonEncodingType; + +template<> +struct RapidJsonEncodingType<1> +{ + using type = rapidjson::UTF8; +}; + +#ifdef BOOST_ENDIAN_BIG_BYTE +template<> +struct RapidJsonEncodingType<2> +{ + using type = rapidjson::UTF16BE; +}; + +template<> +struct RapidJsonEncodingType<4> +{ + using type = rapidjson::UTF32BE; +}; +#else +template<> +struct RapidJsonEncodingType<2> +{ + using type = rapidjson::UTF16LE; +}; + +template<> +struct RapidJsonEncodingType<4> +{ + using type = rapidjson::UTF32LE; +}; +#endif +} // namespace detail + +extern void SetupGlobals(InternalValueMap& globalParams); class ITemplateImpl { public: - virtual ~ITemplateImpl() {} + virtual ~ITemplateImpl() = default; }; @@ -44,13 +86,127 @@ struct TemplateLoader } }; +template +class GenericStreamWriter : public OutStream::StreamWriter +{ +public: + explicit GenericStreamWriter(std::basic_string& os) + : m_os(os) + {} + + // StreamWriter interface + void WriteBuffer(const void* ptr, size_t length) override + { + m_os.append(reinterpret_cast(ptr), length); + } + void WriteValue(const InternalValue& val) override + { + Apply>(val, m_os); + } + +private: + std::basic_string& m_os; +}; + +template +class StringStreamWriter : public OutStream::StreamWriter +{ +public: + explicit StringStreamWriter(std::basic_string* targetStr) + : m_targetStr(targetStr) + {} + + // StreamWriter interface + void WriteBuffer(const void* ptr, size_t length) override + { + m_targetStr->append(reinterpret_cast(ptr), length); + // m_os.write(reinterpret_cast(ptr), length); + } + void WriteValue(const InternalValue& val) override + { + Apply>(val, *m_targetStr); + } + +private: + std::basic_string* m_targetStr; +}; + +template +struct ErrorConverter; + +template +struct ErrorConverter, ErrorInfoTpl> +{ + static ErrorInfoTpl Convert(const ErrorInfoTpl& srcError) + { + typename ErrorInfoTpl::Data errorData; + errorData.code = srcError.GetCode(); + errorData.srcLoc = srcError.GetErrorLocation(); + errorData.locationDescr = ConvertString>(srcError.GetLocationDescr()); + errorData.extraParams = srcError.GetExtraParams(); + + return ErrorInfoTpl(errorData); + } +}; + +template +struct ErrorConverter, ErrorInfoTpl> +{ + static const ErrorInfoTpl& Convert(const ErrorInfoTpl& srcError) + { + return srcError; + } +}; + +template +inline bool operator==(const MetadataInfo& lhs, const MetadataInfo& rhs) +{ + if (lhs.metadata != rhs.metadata) + return false; + if (lhs.metadataType != rhs.metadataType) + return false; + if (lhs.location != rhs.location) + return false; + return true; +} + +template +inline bool operator!=(const MetadataInfo& lhs, const MetadataInfo& rhs) +{ + return !(lhs == rhs); +} + +inline bool operator==(const TemplateEnv& lhs, const TemplateEnv& rhs) +{ + return lhs.IsEqual(rhs); +} +inline bool operator!=(const TemplateEnv& lhs, const TemplateEnv& rhs) +{ + return !(lhs == rhs); +} + +inline bool operator==(const SourceLocation& lhs, const SourceLocation& rhs) +{ + if (lhs.fileName != rhs.fileName) + return false; + if (lhs.line != rhs.line) + return false; + if (lhs.col != rhs.col) + return false; + return true; +} +inline bool operator!=(const SourceLocation& lhs, const SourceLocation& rhs) +{ + return !(lhs == rhs); +} + template class TemplateImpl : public ITemplateImpl { public: using ThisType = TemplateImpl; - TemplateImpl(TemplateEnv* env) + explicit TemplateImpl(TemplateEnv* env) : m_env(env) { if (env) @@ -58,47 +214,91 @@ class TemplateImpl : public ITemplateImpl } auto GetRenderer() const {return m_renderer;} + auto GetTemplateName() const {}; boost::optional> Load(std::basic_string tpl, std::string tplName) { m_template = std::move(tpl); - TemplateParser parser(&m_template, m_settings, tplName.empty() ? std::string("noname.j2tpl") : std::move(tplName)); + m_templateName = tplName.empty() ? std::string("noname.j2tpl") : std::move(tplName); + TemplateParser parser(&m_template, m_settings, m_env, m_templateName); auto parseResult = parser.Parse(); if (!parseResult) return parseResult.error()[0]; m_renderer = *parseResult; + m_metadataInfo = parser.GetMetadataInfo(); return boost::optional>(); } - void Render(std::basic_ostream& os, const ValuesMap& params) + boost::optional> Render(std::basic_string& os, const ValuesMap& params) { + boost::optional> normalResult; + if (!m_renderer) - return; + { + typename ErrorInfoTpl::Data errorData; + errorData.code = ErrorCode::TemplateNotParsed; + errorData.srcLoc.col = 1; + errorData.srcLoc.line = 1; + errorData.srcLoc.fileName = ""; + + return ErrorInfoTpl(errorData); + } - InternalValueMap intParams; - for (auto& ip : params) + try { - auto valRef = &ip.second.data(); - auto newParam = boost::apply_visitor(visitors::InputValueConvertor(), *valRef); - if (!newParam) - intParams[ip.first] = std::move(ValueRef(static_cast(*valRef))); - else - intParams[ip.first] = newParam.get(); + InternalValueMap extParams; + InternalValueMap intParams; + + auto convertFn = [&intParams](const auto& params) { + for (auto& ip : params) + { + auto valRef = &ip.second.data(); + auto newParam = visit(visitors::InputValueConvertor(false, true), *valRef); + if (!newParam) + intParams[ip.first] = ValueRef(static_cast(*valRef)); + else + intParams[ip.first] = newParam.get(); + } + }; + + if (m_env) + { + m_env->ApplyGlobals(convertFn); + std::swap(extParams, intParams); + } + + convertFn(params); + SetupGlobals(extParams); + + RendererCallback callback(this); + RenderContext context(intParams, extParams, &callback); + InitRenderContext(context); + OutStream outStream([writer = GenericStreamWriter(os)]() mutable -> OutStream::StreamWriter* {return &writer;}); + m_renderer->Render(outStream, context); } - RendererCallback callback(this); - RenderContext context(intParams, &callback); - InitRenderContext(context); - OutStream outStream( - [this, &os](const void* ptr, size_t length) { - os.write(reinterpret_cast(ptr), length); - }, - [this, &os](const InternalValue& val) { - Apply>(val, os); + catch (const ErrorInfoTpl& error) + { + return ErrorConverter, ErrorInfoTpl>::Convert(error); + } + catch (const ErrorInfoTpl& error) + { + return ErrorConverter, ErrorInfoTpl>::Convert(error); } - ); - m_renderer->Render(outStream, context); + catch (const std::exception& ex) + { + typename ErrorInfoTpl::Data errorData; + errorData.code = ErrorCode::UnexpectedException; + errorData.srcLoc.col = 1; + errorData.srcLoc.line = 1; + errorData.srcLoc.fileName = m_templateName; + errorData.extraParams.push_back(Value(std::string(ex.what()))); + + return ErrorInfoTpl(errorData); + } + + return normalResult; } InternalValueMap& InitRenderContext(RenderContext& context) @@ -107,56 +307,181 @@ class TemplateImpl : public ITemplateImpl return curScope; } - auto LoadTemplate(const std::string& fileName) - { - using ResultType = boost::variant>, ErrorInfo>, nonstd::expected>, ErrorInfoW>>; - using TplOrError = nonstd::expected>, ErrorInfoTpl>; + using TplOrError = nonstd::expected>, ErrorInfoTpl>; + TplLoadResultType LoadTemplate(const std::string& fileName) + { if (!m_env) - return ResultType(EmptyValue()); + return TplLoadResultType(EmptyValue()); auto tplWrapper = TemplateLoader::Load(fileName, m_env); if (!tplWrapper) - return ResultType(TplOrError(tplWrapper.get_unexpected())); + return TplLoadResultType(TplOrError(tplWrapper.get_unexpected())); + + return TplLoadResultType(TplOrError(std::static_pointer_cast(tplWrapper.value().m_impl))); + } + + TplLoadResultType LoadTemplate(const InternalValue& fileName) + { + auto name = GetAsSameString(std::string(), fileName); + if (!name) + { + typename ErrorInfoTpl::Data errorData; + errorData.code = ErrorCode::InvalidTemplateName; + errorData.srcLoc.col = 1; + errorData.srcLoc.line = 1; + errorData.srcLoc.fileName = m_templateName; + errorData.extraParams.push_back(IntValue2Value(fileName)); + return TplOrError(nonstd::make_unexpected(ErrorInfoTpl(errorData))); + } - return ResultType(TplOrError(std::static_pointer_cast(tplWrapper.value().m_impl))); + return LoadTemplate(name.value()); + } + + nonstd::expected> GetMetadata() const + { + auto& metadataString = m_metadataInfo.metadata; + if (metadataString.empty()) + return GenericMap(); + + if (m_metadataInfo.metadataType == "json") + { + m_metadataJson = JsonDocumentType(); + rapidjson::ParseResult res = m_metadataJson.value().Parse(metadataString.data(), metadataString.size()); + if (!res) + { + typename ErrorInfoTpl::Data errorData; + errorData.code = ErrorCode::MetadataParseError; + errorData.srcLoc = m_metadataInfo.location; + std::string jsonError = rapidjson::GetParseError_En(res.Code()); + errorData.extraParams.push_back(Value(std::move(jsonError))); + return nonstd::make_unexpected(ErrorInfoTpl(errorData)); + } + m_metadata = std::move(nonstd::get(Reflect(m_metadataJson.value()).data())); + return m_metadata.value(); + } + return GenericMap(); + } + + nonstd::expected, ErrorInfoTpl> GetMetadataRaw() const { return m_metadataInfo; } + + bool operator==(const TemplateImpl& other) const + { + if (m_env && other.m_env) + { + if (*m_env != *other.m_env) + return false; + } + if (m_settings != other.m_settings) + return false; + if (m_template != other.m_template) + return false; + if (m_renderer && other.m_renderer && !m_renderer->IsEqual(*other.m_renderer)) + return false; + if (m_metadata != other.m_metadata) + return false; + if (m_metadataJson != other.m_metadataJson) + return false; + if (m_metadataInfo != other.m_metadataInfo) + return false; + return true; + } +private: + void ThrowRuntimeError(ErrorCode code, ValuesList extraParams) + { + typename ErrorInfoTpl::Data errorData; + errorData.code = code; + errorData.srcLoc.col = 1; + errorData.srcLoc.line = 1; + errorData.srcLoc.fileName = m_templateName; + errorData.extraParams = std::move(extraParams); + + throw ErrorInfoTpl(std::move(errorData)); } class RendererCallback : public IRendererCallback { public: - RendererCallback(ThisType* host) + explicit RendererCallback(ThisType* host) : m_host(host) {} TargetString GetAsTargetString(const InternalValue& val) override { - std::basic_ostringstream os; + std::basic_string os; Apply>(val, os); - return TargetString(os.str()); + return TargetString(std::move(os)); } - boost::variant; + str = string_t(); + return OutStream([writer = StringStreamWriter(&nonstd::get(str))]() mutable -> OutStream::StreamWriter* { return &writer; }); + } + + nonstd::variant>, ErrorInfo>, nonstd::expected>, ErrorInfoW>> LoadTemplate(const std::string& fileName) const override { return m_host->LoadTemplate(fileName); } + nonstd::variant>, ErrorInfo>, + nonstd::expected>, ErrorInfoW>> LoadTemplate(const InternalValue& fileName) const override + { + return m_host->LoadTemplate(fileName); + } + + void ThrowRuntimeError(ErrorCode code, ValuesList extraParams) override + { + m_host->ThrowRuntimeError(code, std::move(extraParams)); + } + + bool IsEqual(const IComparable& other) const override + { + auto* callback = dynamic_cast(&other); + if (!callback) + return false; + if (m_host && callback->m_host) + return *m_host == *(callback->m_host); + if ((!m_host && (callback->m_host)) || (m_host && !(callback->m_host))) + return false; + return true; + } + bool operator==(const IComparable& other) const + { + auto* callback = dynamic_cast(&other); + if (!callback) + return false; + if (m_host && callback->m_host) + return *m_host == *(callback->m_host); + if ((!m_host && (callback->m_host)) || (m_host && !(callback->m_host))) + return false; + return true; + } + private: ThisType* m_host; }; - private: - TemplateEnv* m_env; + using JsonDocumentType = rapidjson::GenericDocument::type>; + + TemplateEnv* m_env{}; Settings m_settings; std::basic_string m_template; + std::string m_templateName; RendererPtr m_renderer; + mutable nonstd::optional m_metadata; + mutable nonstd::optional m_metadataJson; + MetadataInfo m_metadataInfo; }; -} // jinja2 +} // namespace jinja2 -#endif // TEMPLATE_IMPL_H +#endif // JINJA2CPP_SRC_TEMPLATE_IMPL_H diff --git a/src/template_parser.cpp b/src/template_parser.cpp index 44455684..bd090393 100644 --- a/src/template_parser.cpp +++ b/src/template_parser.cpp @@ -1,5 +1,6 @@ #include "template_parser.h" -#include +#include "renderer.h" +#include namespace jinja2 { @@ -9,56 +10,79 @@ StatementsParser::ParseResult StatementsParser::Parse(LexScanner& lexer, Stateme Token tok = lexer.NextToken(); ParseResult result; - switch (tok.type) + switch (lexer.GetAsKeyword(tok)) { - case jinja2::Token::For: + case Keyword::For: result = ParseFor(lexer, statementsInfo, tok); break; - case jinja2::Token::Endfor: + case Keyword::Endfor: result = ParseEndFor(lexer, statementsInfo, tok); break; - case jinja2::Token::If: + case Keyword::If: result = ParseIf(lexer, statementsInfo, tok); break; - case jinja2::Token::Else: + case Keyword::Else: result = ParseElse(lexer, statementsInfo, tok); break; - case jinja2::Token::ElIf: + case Keyword::ElIf: result = ParseElIf(lexer, statementsInfo, tok); break; - case jinja2::Token::EndIf: + case Keyword::EndIf: result = ParseEndIf(lexer, statementsInfo, tok); break; - case jinja2::Token::Set: + case Keyword::Set: result = ParseSet(lexer, statementsInfo, tok); break; - case jinja2::Token::Block: + case Keyword::EndSet: + result = ParseEndSet(lexer, statementsInfo, tok); + break; + case Keyword::Block: result = ParseBlock(lexer, statementsInfo, tok); break; - case jinja2::Token::EndBlock: + case Keyword::EndBlock: result = ParseEndBlock(lexer, statementsInfo, tok); break; - case jinja2::Token::Extends: + case Keyword::Extends: result = ParseExtends(lexer, statementsInfo, tok); break; - case jinja2::Token::Macro: + case Keyword::Macro: result = ParseMacro(lexer, statementsInfo, tok); break; - case jinja2::Token::EndMacro: + case Keyword::EndMacro: result = ParseEndMacro(lexer, statementsInfo, tok); break; - case jinja2::Token::Call: + case Keyword::Call: result = ParseCall(lexer, statementsInfo, tok); break; - case jinja2::Token::EndCall: + case Keyword::EndCall: result = ParseEndCall(lexer, statementsInfo, tok); break; - case jinja2::Token::Filter: - case jinja2::Token::EndFilter: - case jinja2::Token::EndSet: - case jinja2::Token::Include: - case jinja2::Token::Import: - return MakeParseError(ErrorCode::YetUnsupported, tok); + case Keyword::Include: + result = ParseInclude(lexer, statementsInfo, tok); + break; + case Keyword::Import: + result = ParseImport(lexer, statementsInfo, tok); + break; + case Keyword::From: + result = ParseFrom(lexer, statementsInfo, tok); + break; + case Keyword::Do: + if (!m_settings.extensions.Do) + 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: + result = ParseFilter(lexer, statementsInfo, tok); + break; + case Keyword::EndFilter: + result = ParseEndFilter(lexer, statementsInfo, tok); + break; default: return MakeParseError(ErrorCode::UnexpectedToken, tok); } @@ -73,6 +97,38 @@ StatementsParser::ParseResult StatementsParser::Parse(LexScanner& lexer, Stateme return result; } +struct ErrorTokenConverter +{ + const Token& baseTok; + + explicit ErrorTokenConverter(const Token& t) + : baseTok(t) + {} + + Token operator()(const Token& tok) const + { + return tok; + } + + template + Token operator()(T tokType) const + { + auto newTok = baseTok; + newTok.type = static_cast(tokType); + if (newTok.type == Token::Identifier || newTok.type == Token::String) + newTok.range.endOffset = newTok.range.startOffset; + return newTok; + } +}; + +template +auto MakeParseErrorTL(ErrorCode code, const Token& baseTok, Args ... expectedTokens) +{ + ErrorTokenConverter tokCvt(baseTok); + + return MakeParseError(code, baseTok, {tokCvt(expectedTokens)...}); +} + StatementsParser::ParseResult StatementsParser::ParseFor(LexScanner &lexer, StatementInfoList &statementsInfo, const Token &stmtTok) { @@ -92,22 +148,18 @@ StatementsParser::ParseResult StatementsParser::ParseFor(LexScanner &lexer, Stat if (vars.empty()) return MakeParseError(ErrorCode::ExpectedIdentifier, lexer.PeekNextToken()); - if (!lexer.EatIfEqual(Token::In)) + if (!lexer.EatIfEqual(Keyword::In)) { Token tok1 = lexer.PeekNextToken(); Token tok2 = tok1; tok2.type = Token::Identifier; tok2.range.endOffset = tok2.range.startOffset; tok2.value = InternalValue(); - Token tok3 = tok2; - tok3.type = Token::In; - Token tok4 = tok2; - tok4.type = static_cast(','); - return MakeParseError(ErrorCode::ExpectedToken, tok1, {tok2, tok3, tok4}); + return MakeParseErrorTL(ErrorCode::ExpectedToken, tok1, tok2, Token::In, ','); } auto pivotToken = lexer.PeekNextToken(); - ExpressionParser exprPraser; + ExpressionParser exprPraser(m_settings); auto valueExpr = exprPraser.ParseFullExpression(lexer, false); if (!valueExpr) return valueExpr.get_unexpected(); @@ -115,25 +167,13 @@ StatementsParser::ParseResult StatementsParser::ParseFor(LexScanner &lexer, Stat Token flagsTok; bool isRecursive = false; - if (lexer.EatIfEqual(Token::Identifier, &flagsTok)) + if (lexer.EatIfEqual(Keyword::Recursive, &flagsTok)) { - auto flagsName = AsString(flagsTok.value); - if (flagsName != "recursive") - { - auto tok2 = flagsTok; - tok2.type = Token::Identifier; - tok2.range.endOffset = tok2.range.startOffset; - tok2.value = std::string("recursive"); - auto tok3 = flagsTok; - tok3.type = Token::If; - return MakeParseError(ErrorCode::ExpectedToken, flagsTok, {tok2, tok3}); - } - isRecursive = true; } ExpressionEvaluatorPtr<> ifExpr; - if (lexer.EatIfEqual(Token::If)) + if (lexer.EatIfEqual(Keyword::If)) { auto parsedExpr = exprPraser.ParseFullExpression(lexer, false); if (!parsedExpr) @@ -143,11 +183,7 @@ StatementsParser::ParseResult StatementsParser::ParseFor(LexScanner &lexer, Stat else if (lexer.PeekNextToken() != Token::Eof) { auto tok1 = lexer.PeekNextToken(); - auto tok2 = tok1; - tok2.type = Token::If; - auto tok3 = tok1; - tok3.type = Token::Eof; - return MakeParseError(ErrorCode::ExpectedToken, tok1, {tok2, tok3}); + return MakeParseErrorTL(ErrorCode::ExpectedToken, tok1, Token::If, Token::Recursive, Token::Eof); } auto renderer = std::make_shared(vars, *valueExpr, ifExpr, isRecursive); @@ -157,7 +193,7 @@ StatementsParser::ParseResult StatementsParser::ParseFor(LexScanner &lexer, Stat return ParseResult(); } -StatementsParser::ParseResult StatementsParser::ParseEndFor(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok) +StatementsParser::ParseResult StatementsParser::ParseEndFor(LexScanner&, StatementInfoList& statementsInfo, const Token& stmtTok) { if (statementsInfo.size() <= 1) return MakeParseError(ErrorCode::UnexpectedStatement, stmtTok); @@ -168,7 +204,7 @@ StatementsParser::ParseResult StatementsParser::ParseEndFor(LexScanner& lexer, S { auto r = std::static_pointer_cast(info.renderer); r->SetMainBody(info.compositions[0]); - elseRenderer = r; + elseRenderer = std::static_pointer_cast(r); statementsInfo.pop_back(); info = statementsInfo.back(); @@ -194,7 +230,7 @@ StatementsParser::ParseResult StatementsParser::ParseIf(LexScanner &lexer, State const Token &stmtTok) { auto pivotTok = lexer.PeekNextToken(); - ExpressionParser exprParser; + ExpressionParser exprParser(m_settings); auto valueExpr = exprParser.ParseFullExpression(lexer); if (!valueExpr) return MakeParseError(ErrorCode::ExpectedExpression, pivotTok); @@ -211,7 +247,7 @@ StatementsParser::ParseResult StatementsParser::ParseElse(LexScanner& /*lexer*/, { auto renderer = std::make_shared(ExpressionEvaluatorPtr<>()); StatementInfo statementInfo = StatementInfo::Create(StatementInfo::ElseIfStatement, stmtTok); - statementInfo.renderer = renderer; + statementInfo.renderer = std::static_pointer_cast(renderer); statementsInfo.push_back(statementInfo); return ParseResult(); } @@ -220,19 +256,19 @@ StatementsParser::ParseResult StatementsParser::ParseElIf(LexScanner& lexer, Sta , const Token& stmtTok) { auto pivotTok = lexer.PeekNextToken(); - ExpressionParser exprParser; + ExpressionParser exprParser(m_settings); auto valueExpr = exprParser.ParseFullExpression(lexer); if (!valueExpr) return MakeParseError(ErrorCode::ExpectedExpression, pivotTok); auto renderer = std::make_shared(*valueExpr); StatementInfo statementInfo = StatementInfo::Create(StatementInfo::ElseIfStatement, stmtTok); - statementInfo.renderer = renderer; + statementInfo.renderer = std::static_pointer_cast(renderer); statementsInfo.push_back(statementInfo); return ParseResult(); } -StatementsParser::ParseResult StatementsParser::ParseEndIf(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok) +StatementsParser::ParseResult StatementsParser::ParseEndIf(LexScanner&, StatementInfoList& statementsInfo, const Token& stmtTok) { if (statementsInfo.size() <= 1) return MakeParseError(ErrorCode::UnexpectedStatement, stmtTok); @@ -286,30 +322,60 @@ StatementsParser::ParseResult StatementsParser::ParseSet(LexScanner& lexer, Stat if (vars.empty()) return MakeParseError(ErrorCode::ExpectedIdentifier, lexer.PeekNextToken()); - auto operTok = lexer.NextToken(); - ExpressionEvaluatorPtr<> valueExpr; - if (operTok == '=') + ExpressionParser exprParser(m_settings); + if (lexer.EatIfEqual('=')) { - ExpressionParser exprParser; - auto expr = exprParser.ParseFullExpression(lexer); + const auto expr = exprParser.ParseFullExpression(lexer); if (!expr) return expr.get_unexpected(); - valueExpr = *expr; + statementsInfo.back().currentComposition->AddRenderer( + std::make_shared(std::move(vars), *expr)); + } + else if (lexer.EatIfEqual('|')) + { + const auto expr = exprParser.ParseFilterExpression(lexer); + if (!expr) + return expr.get_unexpected(); + auto statementInfo = StatementInfo::Create( + StatementInfo::SetStatement, stmtTok); + statementInfo.renderer = std::make_shared( + std::move(vars), *expr); + statementsInfo.push_back(std::move(statementInfo)); } else - return MakeParseError(ErrorCode::YetUnsupported, operTok, {stmtTok}); // TODO: Add handling of the block assignments - - auto renderer = std::make_shared(vars); - renderer->SetAssignmentExpr(valueExpr); - statementsInfo.back().currentComposition->AddRenderer(renderer); + { + auto operTok = lexer.NextToken(); + if (lexer.NextToken() != Token::Eof) + return MakeParseError(ErrorCode::YetUnsupported, operTok, {std::move(stmtTok)}); + auto statementInfo = StatementInfo::Create( + StatementInfo::SetStatement, stmtTok); + statementInfo.renderer = std::make_shared( + std::move(vars)); + statementsInfo.push_back(std::move(statementInfo)); + } - return ParseResult(); + return {}; } -StatementsParser::ParseResult StatementsParser::ParseEndSet(LexScanner& /*lexer*/, StatementInfoList& /*statementsInfo*/ +StatementsParser::ParseResult StatementsParser::ParseEndSet(LexScanner& + , StatementInfoList& statementsInfo , const Token& stmtTok) { - return MakeParseError(ErrorCode::YetUnsupported, stmtTok); + if (statementsInfo.size() <= 1) + return MakeParseError(ErrorCode::UnexpectedStatement, stmtTok); + + const auto info = statementsInfo.back(); + if (info.type != StatementInfo::SetStatement) + return MakeParseError(ErrorCode::UnexpectedStatement, stmtTok); + + auto &renderer = *boost::polymorphic_downcast( + info.renderer.get()); + renderer.SetBody(info.compositions[0]); + + statementsInfo.pop_back(); + statementsInfo.back().currentComposition->AddRenderer(info.renderer); + + return {}; } StatementsParser::ParseResult StatementsParser::ParseBlock(LexScanner& lexer, StatementInfoList& statementsInfo @@ -335,29 +401,25 @@ StatementsParser::ParseResult StatementsParser::ParseBlock(LexScanner& lexer, St else { bool isScoped = false; - if (lexer.EatIfEqual(Token::Identifier, &nextTok)) - { - auto id = AsString(nextTok.value); - if (id != "scoped") - { - auto tok2 = nextTok; - tok2.range.startOffset = tok2.range.endOffset; - tok2.value = std::string("scoped"); - return MakeParseError(ErrorCode::ExpectedToken, nextTok, {tok2}); - } + if (lexer.EatIfEqual(Keyword::Scoped, &nextTok)) isScoped = true; + else + { + nextTok = lexer.PeekNextToken(); + if (nextTok != Token::Eof) + return MakeParseErrorTL(ErrorCode::ExpectedToken, nextTok, Token::Scoped); } + blockRenderer = std::make_shared(blockName, isScoped); } StatementInfo statementInfo = StatementInfo::Create(blockType, stmtTok); - statementInfo.renderer = blockRenderer; + statementInfo.renderer = std::move(blockRenderer); statementsInfo.push_back(statementInfo); return ParseResult(); } -StatementsParser::ParseResult StatementsParser::ParseEndBlock(LexScanner& lexer, StatementInfoList& statementsInfo - , const Token& stmtTok) +StatementsParser::ParseResult StatementsParser::ParseEndBlock(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok) { if (statementsInfo.size() <= 1) return MakeParseError(ErrorCode::UnexpectedStatement, stmtTok); @@ -371,7 +433,7 @@ StatementsParser::ParseResult StatementsParser::ParseEndBlock(LexScanner& lexer, tok3.type = Token::Eof; return MakeParseError(ErrorCode::ExpectedToken, nextTok, {tok2, tok3}); } - + if (nextTok == Token::Identifier) lexer.EatToken(); @@ -386,12 +448,16 @@ StatementsParser::ParseResult StatementsParser::ParseEndBlock(LexScanner& lexer, auto extendsStmt = std::static_pointer_cast(extendsInfo.renderer); extendsStmt->AddBlock(std::static_pointer_cast(info.renderer)); } - else + else if (info.type == StatementInfo::ParentBlockStatement) { auto blockStmt = std::static_pointer_cast(info.renderer); blockStmt->SetMainBody(info.compositions[0]); statementsInfo.back().currentComposition->AddRenderer(info.renderer); } + else + { + return MakeParseError(ErrorCode::UnexpectedStatement, stmtTok); + } return ParseResult(); } @@ -401,6 +467,9 @@ StatementsParser::ParseResult StatementsParser::ParseExtends(LexScanner& lexer, if (statementsInfo.empty()) return MakeParseError(ErrorCode::UnexpectedStatement, stmtTok); + if (!m_env) + return MakeParseError(ErrorCode::TemplateEnvAbsent, stmtTok); + Token tok = lexer.NextToken(); if (tok != Token::String && tok != Token::Identifier) { @@ -408,9 +477,7 @@ StatementsParser::ParseResult StatementsParser::ParseExtends(LexScanner& lexer, tok2.type = Token::Identifier; tok2.range.endOffset = tok2.range.startOffset; tok2.value = EmptyValue{}; - auto tok3 = tok2; - tok3.type = Token::String; - return MakeParseError(ErrorCode::ExpectedToken, tok, {tok2, tok3}); + return MakeParseErrorTL(ErrorCode::ExpectedToken, tok, tok2, Token::String); } auto renderer = std::make_shared(AsString(tok.value), tok == Token::String); @@ -446,12 +513,8 @@ StatementsParser::ParseResult StatementsParser::ParseMacro(LexScanner& lexer, St 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}); + return MakeParseErrorTL(ErrorCode::UnexpectedToken, tok, Token::RBracket, Token::Eof); } auto renderer = std::make_shared(std::move(macroName), std::move(macroParams)); @@ -469,7 +532,7 @@ nonstd::expected StatementsParser::ParseMacroParams(Lex if (lexer.EatIfEqual(')')) return std::move(items); - ExpressionParser exprParser; + ExpressionParser exprParser(m_settings); do { @@ -489,7 +552,7 @@ nonstd::expected StatementsParser::ParseMacroParams(Lex MacroParam p; p.paramName = AsString(name.value); - p.defaultValue = defVal; + p.defaultValue = std::move(defVal); items.push_back(std::move(p)); } while (lexer.EatIfEqual(',')); @@ -501,7 +564,7 @@ nonstd::expected StatementsParser::ParseMacroParams(Lex return std::move(items); } -StatementsParser::ParseResult StatementsParser::ParseEndMacro(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok) +StatementsParser::ParseResult StatementsParser::ParseEndMacro(LexScanner&, StatementInfoList& statementsInfo, const Token& stmtTok) { if (statementsInfo.size() <= 1) return MakeParseError(ErrorCode::UnexpectedStatement, stmtTok); @@ -550,10 +613,10 @@ StatementsParser::ParseResult StatementsParser::ParseCall(LexScanner& lexer, Sta std::string macroName = AsString(nextTok.value); - CallParams callParams; + CallParamsInfo callParams; if (lexer.EatIfEqual('(')) { - ExpressionParser exprParser; + ExpressionParser exprParser(m_settings); auto result = exprParser.ParseCallParams(lexer); if (!result) return result.get_unexpected(); @@ -569,7 +632,7 @@ StatementsParser::ParseResult StatementsParser::ParseCall(LexScanner& lexer, Sta return ParseResult(); } -StatementsParser::ParseResult StatementsParser::ParseEndCall(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok) +StatementsParser::ParseResult StatementsParser::ParseEndCall(LexScanner&, StatementInfoList& statementsInfo, const Token& stmtTok) { if (statementsInfo.size() <= 1) return MakeParseError(ErrorCode::UnexpectedStatement, stmtTok); @@ -590,4 +653,320 @@ StatementsParser::ParseResult StatementsParser::ParseEndCall(LexScanner& lexer, return ParseResult(); } +StatementsParser::ParseResult StatementsParser::ParseInclude(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok) +{ + if (statementsInfo.empty()) + return MakeParseError(ErrorCode::UnexpectedStatement, stmtTok); + + // auto operTok = lexer.NextToken(); + ExpressionEvaluatorPtr<> valueExpr; + ExpressionParser exprParser(m_settings); + auto expr = exprParser.ParseFullExpression(lexer); + if (!expr) + return expr.get_unexpected(); + valueExpr = *expr; + + Token nextTok = lexer.PeekNextToken(); + bool isIgnoreMissing = false; + bool isWithContext = true; + bool hasIgnoreMissing = false; + if (lexer.EatIfEqual(Keyword::Ignore)) + { + if (lexer.EatIfEqual(Keyword::Missing)) + isIgnoreMissing = true; + else + return MakeParseErrorTL(ErrorCode::ExpectedToken, lexer.PeekNextToken(), Token::Missing); + + hasIgnoreMissing = true; + nextTok = lexer.PeekNextToken(); + } + + auto kw = lexer.GetAsKeyword(nextTok); + bool hasContextControl = false; + if (kw == Keyword::With || kw == Keyword::Without) + { + lexer.EatToken(); + isWithContext = kw == Keyword::With; + if (!lexer.EatIfEqual(Keyword::Context)) + return MakeParseErrorTL(ErrorCode::ExpectedToken, lexer.PeekNextToken(), Token::Context); + + nextTok = lexer.PeekNextToken(); + hasContextControl = true; + } + + if (nextTok != Token::Eof) + { + if (hasContextControl) + return MakeParseErrorTL(ErrorCode::ExpectedEndOfStatement, nextTok, Token::Eof); + + if (hasIgnoreMissing) + return MakeParseErrorTL(ErrorCode::UnexpectedToken, nextTok, Token::Eof, Token::With, Token::Without); + + return MakeParseErrorTL(ErrorCode::UnexpectedToken, nextTok, Token::Eof, Token::Ignore, Token::With, Token::Without); + } + + if (!m_env && !isIgnoreMissing) + return MakeParseError(ErrorCode::TemplateEnvAbsent, stmtTok); + + auto renderer = std::make_shared(isIgnoreMissing, isWithContext); + renderer->SetIncludeNamesExpr(valueExpr); + statementsInfo.back().currentComposition->AddRenderer(renderer); + + return ParseResult(); +} + +StatementsParser::ParseResult StatementsParser::ParseImport(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok) +{ + if (!m_env) + return MakeParseError(ErrorCode::TemplateEnvAbsent, stmtTok); + + ExpressionEvaluatorPtr<> valueExpr; + ExpressionParser exprParser(m_settings); + auto expr = exprParser.ParseFullExpression(lexer); + if (!expr) + return expr.get_unexpected(); + valueExpr = *expr; + + if (!lexer.EatIfEqual(Keyword::As)) + return MakeParseErrorTL(ErrorCode::ExpectedToken, lexer.PeekNextToken(), Token::As); + + Token name; + if (!lexer.EatIfEqual(Token::Identifier, &name)) + return MakeParseErrorTL(ErrorCode::ExpectedToken, lexer.PeekNextToken(), Token::Identifier); + + Token nextTok = lexer.PeekNextToken(); + auto kw = lexer.GetAsKeyword(nextTok); + bool hasContextControl = false; + bool isWithContext = false; + if (kw == Keyword::With || kw == Keyword::Without) + { + lexer.EatToken(); + isWithContext = kw == Keyword::With; + if (!lexer.EatIfEqual(Keyword::Context)) + return MakeParseErrorTL(ErrorCode::ExpectedToken, lexer.PeekNextToken(), Token::Context); + + nextTok = lexer.PeekNextToken(); + hasContextControl = true; + } + + if (nextTok != Token::Eof) + { + if (hasContextControl) + return MakeParseErrorTL(ErrorCode::ExpectedEndOfStatement, nextTok, Token::Eof); + + return MakeParseErrorTL(ErrorCode::UnexpectedToken, nextTok, Token::Eof, Token::With, Token::Without); + } + + auto renderer = std::make_shared(isWithContext); + renderer->SetImportNameExpr(valueExpr); + renderer->SetNamespace(AsString(name.value)); + statementsInfo.back().currentComposition->AddRenderer(renderer); + + return ParseResult(); } + +StatementsParser::ParseResult StatementsParser::ParseFrom(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok) +{ + if (!m_env) + return MakeParseError(ErrorCode::TemplateEnvAbsent, stmtTok); + + ExpressionEvaluatorPtr<> valueExpr; + ExpressionParser exprParser(m_settings); + auto expr = exprParser.ParseFullExpression(lexer); + if (!expr) + return expr.get_unexpected(); + valueExpr = *expr; + + if (!lexer.EatIfEqual(Keyword::Import)) + return MakeParseErrorTL(ErrorCode::ExpectedToken, lexer.PeekNextToken(), Token::Identifier); + + std::vector> mappedNames; + + Token nextTok; + bool hasContextControl = false; + bool isWithContext = false; + + for (;;) + { + bool hasComma = false; + if (!mappedNames.empty()) + { + if (!lexer.EatIfEqual(Token::Comma)) + hasComma = true;; + } + + nextTok = lexer.PeekNextToken(); + auto kw = lexer.GetAsKeyword(nextTok); + if (kw == Keyword::With || kw == Keyword::Without) + { + lexer.NextToken(); + if (lexer.EatIfEqual(Keyword::Context)) + { + hasContextControl = true; + isWithContext = kw == Keyword::With; + nextTok = lexer.PeekNextToken(); + break; + } + else + { + lexer.ReturnToken(); + } + } + + if (hasComma) + break; + + std::pair macroMap; + if (!lexer.EatIfEqual(Token::Identifier, &nextTok)) + return MakeParseErrorTL(ErrorCode::ExpectedToken, nextTok, Token::Identifier); + + macroMap.first = AsString(nextTok.value); + + if (lexer.EatIfEqual(Keyword::As)) + { + if (!lexer.EatIfEqual(Token::Identifier, &nextTok)) + return MakeParseErrorTL(ErrorCode::ExpectedToken, nextTok, Token::Identifier); + macroMap.second = AsString(nextTok.value); + } + else + { + macroMap.second = macroMap.first; + } + mappedNames.push_back(std::move(macroMap)); + } + + if (nextTok != Token::Eof) + { + if (hasContextControl) + return MakeParseErrorTL(ErrorCode::ExpectedEndOfStatement, nextTok, Token::Eof); + + if (mappedNames.empty()) + MakeParseErrorTL(ErrorCode::UnexpectedToken, nextTok, Token::Eof, Token::Identifier); + else + MakeParseErrorTL(ErrorCode::UnexpectedToken, nextTok, Token::Eof, Token::Comma, Token::With, Token::Without); + } + + auto renderer = std::make_shared(isWithContext); + renderer->SetImportNameExpr(valueExpr); + + for (auto& nameInfo : mappedNames) + renderer->AddNameToImport(std::move(nameInfo.first), std::move(nameInfo.second)); + + statementsInfo.back().currentComposition->AddRenderer(renderer); + + return ParseResult(); +} + +StatementsParser::ParseResult StatementsParser::ParseDo(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& /*stmtTok*/) +{ + ExpressionEvaluatorPtr<> valueExpr; + ExpressionParser exprParser(m_settings); + auto expr = exprParser.ParseFullExpression(lexer); + if (!expr) + return expr.get_unexpected(); + valueExpr = *expr; + + auto renderer = std::make_shared(valueExpr); + statementsInfo.back().currentComposition->AddRenderer(renderer); + + 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(); +} + +StatementsParser::ParseResult StatementsParser::ParseFilter(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok) +{ + ExpressionParser exprParser(m_settings); + auto filterExpr = exprParser.ParseFilterExpression(lexer); + if (!filterExpr) + { + return filterExpr.get_unexpected(); + } + + auto renderer = std::make_shared(*filterExpr); + auto statementInfo = StatementInfo::Create( + StatementInfo::FilterStatement, stmtTok); + statementInfo.renderer = std::move(renderer); + statementsInfo.push_back(std::move(statementInfo)); + + return {}; +} + +StatementsParser::ParseResult StatementsParser::ParseEndFilter(LexScanner&, StatementInfoList& statementsInfo, const Token& stmtTok) +{ + if (statementsInfo.size() <= 1) + return MakeParseError(ErrorCode::UnexpectedStatement, stmtTok); + + const auto info = statementsInfo.back(); + if (info.type != StatementInfo::FilterStatement) + { + return MakeParseError(ErrorCode::UnexpectedStatement, stmtTok); + } + + statementsInfo.pop_back(); + auto &renderer = *boost::polymorphic_downcast(info.renderer.get()); + renderer.SetBody(info.compositions[0]); + + statementsInfo.back().currentComposition->AddRenderer(info.renderer); + + return {}; +} + +} // namespace jinja2 diff --git a/src/template_parser.h b/src/template_parser.h index b5b8654b..c183f5b6 100644 --- a/src/template_parser.h +++ b/src/template_parser.h @@ -1,28 +1,43 @@ -#ifndef TEMPLATE_PARSER_H -#define TEMPLATE_PARSER_H +#ifndef JINJA2CPP_SRC_TEMPLATE_PARSER_H +#define JINJA2CPP_SRC_TEMPLATE_PARSER_H -#include "renderer.h" -#include "template_parser.h" -#include "lexer.h" -#include "lexertk.h" #include "error_handling.h" -#include "expression_evaluator.h" #include "expression_parser.h" -#include "statements.h" #include "helpers.h" +#include "lexer.h" +#include "lexertk.h" +#include "renderer.h" +#include "statements.h" +#include "template_parser.h" #include "value_visitors.h" +#include #include #include - #include -#include -#include -#include -#include #include #include +#include +#include + +#ifdef JINJA2CPP_USE_REGEX_BOOST +#include +template +using BasicRegex = boost::basic_regex; +using Regex = boost::regex; +using WideRegex = boost::wregex; +template +using RegexIterator = boost::regex_iterator; +#else +#include +template +using BasicRegex = std::basic_regex; +using Regex = std::regex; +using WideRegex = std::wregex; +template +using RegexIterator = std::regex_iterator; +#endif namespace jinja2 { @@ -32,7 +47,7 @@ struct ParserTraits; struct KeywordsInfo { MultiStringLiteral name; - Token::Type type; + Keyword type; }; struct TokenStrInfo : MultiStringLiteral @@ -42,25 +57,27 @@ struct TokenStrInfo : MultiStringLiteral { return MultiStringLiteral::template GetValue(); } - }; template struct ParserTraitsBase { static Token::Type s_keywords[]; - static KeywordsInfo s_keywordsInfo[30]; + static KeywordsInfo s_keywordsInfo[41]; static std::unordered_map s_tokens; + static MultiStringLiteral s_regexp; }; +template +MultiStringLiteral ParserTraitsBase::s_regexp = UNIVERSAL_STR( + R"((\{\{)|(\}\})|(\{%[\+\-]?\s+raw\s+[\+\-]?%\})|(\{%[\+\-]?\s+endraw\s+[\+\-]?%\})|(\{%\s+meta\s+%\})|(\{%\s+endmeta\s+%\})|(\{%)|(%\})|(\{#)|(#\})|(\n))"); + template<> struct ParserTraits : public ParserTraitsBase<> { - static std::regex GetRoughTokenizer() - { - return std::regex(R"((\{\{)|(\}\})|(\{%)|(%\})|(\{#)|(#\})|(\n))"); - } - static std::regex GetKeywords() + static Regex GetRoughTokenizer() + { return Regex(s_regexp.GetValueStr()); } + static Regex GetKeywords() { std::string pattern; std::string prefix("(^"); @@ -76,12 +93,9 @@ struct ParserTraits : public ParserTraitsBase<> pattern += prefix + info.name.charValue + postfix; } - return std::regex(pattern); - } - static std::string GetAsString(const std::string& str, CharRange range) - { - return str.substr(range.startOffset, range.size()); + return Regex(pattern); } + static std::string GetAsString(const std::string& str, CharRange range) { return str.substr(range.startOffset, range.size()); } static InternalValue RangeToNum(const std::string& str, CharRange range, Token::Type hint) { char buff[std::max(std::numeric_limits::max_digits10, std::numeric_limits::max_digits10) * 2 + 1]; @@ -100,10 +114,10 @@ struct ParserTraits : public ParserTraitsBase<> { endBuff = nullptr; double dblVal = strtod(buff, nullptr); - result = dblVal; + result = static_cast(dblVal); } else - result = val; + result = static_cast(val); } return result; } @@ -112,11 +126,9 @@ struct ParserTraits : public ParserTraitsBase<> template<> struct ParserTraits : public ParserTraitsBase<> { - static std::wregex GetRoughTokenizer() - { - return std::wregex(LR"((\{\{)|(\}\})|(\{%)|(%\})|(\{#)|(#\})|(\n))"); - } - static std::wregex GetKeywords() + static WideRegex GetRoughTokenizer() + { return WideRegex(s_regexp.GetValueStr()); } + static WideRegex GetKeywords() { std::wstring pattern; std::wstring prefix(L"(^"); @@ -132,31 +144,43 @@ struct ParserTraits : public ParserTraitsBase<> pattern += prefix + info.name.wcharValue + postfix; } - return std::wregex(pattern); + return WideRegex(pattern); } static std::string GetAsString(const std::wstring& str, CharRange range) { - auto tmpStr = str.substr(range.startOffset, range.size()); - std::string result; - result.resize(tmpStr.size(), '\0'); -#ifdef _MSC_VER - size_t dummy = 0; - wcstombs_s(&dummy, &result[0], result.size(), tmpStr.c_str(), tmpStr.size()); -#else - wcstombs(&result[0], tmpStr.c_str(), result.size()); -#endif - return result; + auto srcStr = str.substr(range.startOffset, range.size()); + return detail::StringConverter::DoConvert(srcStr); } - static InternalValue RangeToNum(const std::wstring& /*str*/, CharRange /*range*/, Token::Type /*hint*/) + static InternalValue RangeToNum(const std::wstring& str, CharRange range, Token::Type hint) { - return InternalValue(); + wchar_t buff[std::max(std::numeric_limits::max_digits10, std::numeric_limits::max_digits10) * 2 + 1]; + std::copy(str.data() + range.startOffset, str.data() + range.endOffset, buff); + buff[range.size()] = 0; + InternalValue result; + if (hint == Token::IntegerNum) + { + result = static_cast(wcstoll(buff, nullptr, 0)); + } + else + { + wchar_t* endBuff = nullptr; + int64_t val = wcstoll(buff, &endBuff, 10); + if ((errno == ERANGE) || *endBuff) + { + endBuff = nullptr; + double dblVal = wcstod(buff, nullptr); + result = static_cast(dblVal); + } + else + result = static_cast(val); + } + return result; } }; struct StatementInfo { - enum Type - { + enum Type { TemplateRoot, IfStatement, ElseIfStatement, @@ -166,7 +190,9 @@ struct StatementInfo BlockStatement, ParentBlockStatement, MacroStatement, - MacroCallStatement + MacroCallStatement, + WithStatement, + FilterStatement }; using ComposedPtr = std::shared_ptr; @@ -194,12 +220,18 @@ class StatementsParser public: using ParseResult = nonstd::expected; + StatementsParser(const Settings& settings, TemplateEnv* env) + : m_settings(settings) + , m_env(env) + { + } + ParseResult Parse(LexScanner& lexer, StatementInfoList& statementsInfo); private: - ParseResult ParseFor(LexScanner &lexer, StatementInfoList &statementsInfo, const Token& stmtTok); + ParseResult ParseFor(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); ParseResult ParseEndFor(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); - ParseResult ParseIf(LexScanner &lexer, StatementInfoList &statementsInfo, const Token& stmtTok); + ParseResult ParseIf(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); ParseResult ParseElse(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); ParseResult ParseElIf(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); ParseResult ParseEndIf(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& pos); @@ -213,6 +245,18 @@ class StatementsParser 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); + ParseResult ParseInclude(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); + ParseResult ParseImport(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); + ParseResult ParseFrom(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); + ParseResult ParseDo(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); + ParseResult ParseWith(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& token); + ParseResult ParseEndWith(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); + ParseResult ParseFilter(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); + ParseResult ParseEndFilter(LexScanner& lexer, StatementInfoList& statementsInfo, const Token& stmtTok); + +private: + Settings m_settings; + TemplateEnv* m_env; }; template @@ -221,16 +265,18 @@ class TemplateParser : public LexerHelper public: using string_t = std::basic_string; using traits_t = ParserTraits; - using sregex_iterator = std::regex_iterator; + using sregex_iterator = RegexIterator; using ErrorInfo = ErrorInfoTpl; using ParseResult = nonstd::expected>; - TemplateParser(const string_t* tpl, const Settings& setts, std::string tplName) + TemplateParser(const string_t* tpl, const Settings& setts, TemplateEnv* env, std::string tplName) : m_template(tpl) , m_templateName(std::move(tplName)) , m_settings(setts) + , m_env(env) , m_roughTokenizer(traits_t::GetRoughTokenizer()) , m_keywords(traits_t::GetKeywords()) + , m_metadataType(setts.m_defaultMetadataType) { } @@ -252,12 +298,24 @@ class TemplateParser : public LexerHelper return composeRenderer; } -private: - enum + MetadataInfo GetMetadataInfo() const { + MetadataInfo result; + result.metadataType = m_metadataType; + result.metadata = m_metadata; + result.location = m_metadataLocation; + return result; + } + +private: + enum { RM_Unknown = 0, RM_ExprBegin = 1, RM_ExprEnd, + RM_RawBegin, + RM_RawEnd, + RM_MetaBegin, + RM_MetaEnd, RM_StmtBegin, RM_StmtEnd, RM_CommentBegin, @@ -271,14 +329,7 @@ class TemplateParser : public LexerHelper unsigned lineNumber; }; - enum class TextBlockType - { - RawText, - Expression, - Statement, - Comment, - LineStatement - }; + enum class TextBlockType { RawText, Expression, Statement, Comment, LineStatement, RawBlock, MetaBlock }; struct TextBlockInfo { @@ -297,9 +348,10 @@ class TemplateParser : public LexerHelper // One line, no customization if (matches == 0) { - CharRange range{0ULL, m_template->size()}; - m_lines.push_back(LineInfo{range, 0}); - m_textBlocks.push_back(TextBlockInfo{range, (!m_template->empty() && m_template->front() == '#') ? TextBlockType::LineStatement : TextBlockType::RawText}); + CharRange range{ 0ULL, m_template->size() }; + m_lines.push_back(LineInfo{ range, 0 }); + m_textBlocks.push_back( + TextBlockInfo{ range, (!m_template->empty() && m_template->front() == '#') ? TextBlockType::LineStatement : TextBlockType::RawText }); return nonstd::expected>(); } @@ -317,10 +369,27 @@ class TemplateParser : public LexerHelper if (!result) { foundErrors.push_back(result.error()); + return nonstd::make_unexpected(std::move(foundErrors)); } } while (matchBegin != matchEnd); FinishCurrentLine(m_template->size()); - FinishCurrentBlock(m_template->size()); + + if (m_currentBlockInfo.type == TextBlockType::RawBlock) + { + nonstd::expected result = + MakeParseError(ErrorCode::ExpectedRawEnd, MakeToken(Token::RawEnd, { m_template->size(), m_template->size() })); + foundErrors.push_back(result.error()); + return nonstd::make_unexpected(std::move(foundErrors)); + } + else if (m_currentBlockInfo.type == TextBlockType::MetaBlock) + { + nonstd::expected result = + MakeParseError(ErrorCode::ExpectedMetaEnd, MakeToken(Token::RawEnd, { m_template->size(), m_template->size() })); + foundErrors.push_back(result.error()); + return nonstd::make_unexpected(std::move(foundErrors)); + } + + FinishCurrentBlock(m_template->size(), TextBlockType::RawText); if (!foundErrors.empty()) return nonstd::make_unexpected(std::move(foundErrors)); @@ -329,9 +398,9 @@ class TemplateParser : public LexerHelper nonstd::expected ParseRoughMatch(sregex_iterator& curMatch, const sregex_iterator& /*endMatch*/) { auto match = *curMatch; - ++ curMatch; + ++curMatch; unsigned matchType = RM_Unknown; - for (unsigned idx = 1; idx != match.size(); ++ idx) + for (unsigned idx = 1; idx != match.size(); ++idx) { if (match.length(idx) != 0) { @@ -344,105 +413,219 @@ class TemplateParser : public LexerHelper switch (matchType) { - case RM_NewLine: - FinishCurrentLine(match.position()); - m_currentLineInfo.range.startOffset = m_currentLineInfo.range.endOffset + 1; - if (m_currentLineInfo.range.startOffset < m_template->size() && + case RM_NewLine: + FinishCurrentLine(match.position()); + m_currentLineInfo.range.startOffset = m_currentLineInfo.range.endOffset + 1; + if (m_currentLineInfo.range.startOffset < m_template->size() && (m_currentBlockInfo.type == TextBlockType::RawText || m_currentBlockInfo.type == TextBlockType::LineStatement)) - { - if (m_currentBlockInfo.type == TextBlockType::LineStatement) { - FinishCurrentBlock(matchStart); - m_currentBlockInfo.range.startOffset = m_currentLineInfo.range.startOffset; + if (m_currentBlockInfo.type == TextBlockType::LineStatement) + { + FinishCurrentBlock(matchStart, TextBlockType::RawText); + m_currentBlockInfo.range.startOffset = m_currentLineInfo.range.startOffset; + } + + if (m_settings.useLineStatements) + m_currentBlockInfo.type = + (*m_template)[m_currentLineInfo.range.startOffset] == '#' ? TextBlockType::LineStatement : TextBlockType::RawText; + else + m_currentBlockInfo.type = TextBlockType::RawText; + } + break; + case RM_CommentBegin: + if (m_currentBlockInfo.type == TextBlockType::RawBlock) + break; + if (m_currentBlockInfo.type != TextBlockType::RawText) + { + FinishCurrentLine(match.position() + 2); + return MakeParseError(ErrorCode::UnexpectedCommentBegin, MakeToken(Token::CommentBegin, { matchStart, matchStart + 2 })); } - if (m_settings.useLineStatements) - m_currentBlockInfo.type = (*m_template)[m_currentLineInfo.range.startOffset] == '#' ? TextBlockType::LineStatement : TextBlockType::RawText; - else - m_currentBlockInfo.type = TextBlockType::RawText; - } - break; - case RM_CommentBegin: - if (m_currentBlockInfo.type != TextBlockType::RawText) - return MakeParseError(ErrorCode::UnexpectedCommentBegin, MakeToken(Token::CommentBegin, {matchStart, matchStart + 2})); - - FinishCurrentBlock(matchStart); - m_currentBlockInfo.range.startOffset = matchStart + 2; - m_currentBlockInfo.type = TextBlockType::Comment; - break; - - case RM_CommentEnd: - if (m_currentBlockInfo.type != TextBlockType::Comment) - return MakeParseError(ErrorCode::UnexpectedCommentEnd, MakeToken(Token::CommentEnd, {matchStart, matchStart + 2})); - - FinishCurrentBlock(matchStart); - m_currentBlockInfo.range.startOffset = matchStart + 2; - break; - case RM_ExprBegin: - StartControlBlock(TextBlockType::Expression, matchStart); - break; - case RM_ExprEnd: - if (m_currentBlockInfo.type == TextBlockType::RawText) - return MakeParseError(ErrorCode::UnexpectedExprEnd, MakeToken(Token::ExprEnd, {matchStart, matchStart + 2})); - else if (m_currentBlockInfo.type != TextBlockType::Expression || (*m_template)[match.position() - 1] == '\'') + FinishCurrentBlock(matchStart, TextBlockType::Comment); + m_currentBlockInfo.range.startOffset = matchStart + 2; + m_currentBlockInfo.type = TextBlockType::Comment; break; - m_currentBlockInfo.range.startOffset = FinishCurrentBlock(matchStart); - break; - case RM_StmtBegin: - StartControlBlock(TextBlockType::Statement, matchStart); - break; - case RM_StmtEnd: - if (m_currentBlockInfo.type == TextBlockType::RawText) - return MakeParseError(ErrorCode::UnexpectedStmtEnd, MakeToken(Token::StmtEnd, {matchStart, matchStart + 2})); - else if (m_currentBlockInfo.type != TextBlockType::Statement || (*m_template)[match.position() - 1] == '\'') + case RM_CommentEnd: + if (m_currentBlockInfo.type == TextBlockType::RawBlock) + break; + if (m_currentBlockInfo.type != TextBlockType::Comment) + { + FinishCurrentLine(match.position() + 2); + return MakeParseError(ErrorCode::UnexpectedCommentEnd, MakeToken(Token::CommentEnd, { matchStart, matchStart + 2 })); + } + + m_currentBlockInfo.range.startOffset = FinishCurrentBlock(matchStart, TextBlockType::RawText); + break; + case RM_ExprBegin: + StartControlBlock(TextBlockType::Expression, matchStart); + break; + case RM_ExprEnd: + if (m_currentBlockInfo.type == TextBlockType::RawText) + { + FinishCurrentLine(match.position() + 2); + return MakeParseError(ErrorCode::UnexpectedExprEnd, MakeToken(Token::ExprEnd, { matchStart, matchStart + 2 })); + } + else if (m_currentBlockInfo.type != TextBlockType::Expression || (*m_template)[match.position() - 1] == '\'') + break; + + m_currentBlockInfo.range.startOffset = FinishCurrentBlock(matchStart, TextBlockType::RawText); break; + case RM_StmtBegin: + StartControlBlock(TextBlockType::Statement, matchStart); + break; + case RM_StmtEnd: + if (m_currentBlockInfo.type == TextBlockType::RawText) + { + FinishCurrentLine(match.position() + 2); + return MakeParseError(ErrorCode::UnexpectedStmtEnd, MakeToken(Token::StmtEnd, { matchStart, matchStart + 2 })); + } + else if (m_currentBlockInfo.type != TextBlockType::Statement || (*m_template)[match.position() - 1] == '\'') + break; - m_currentBlockInfo.range.startOffset = FinishCurrentBlock(matchStart); - break; + m_currentBlockInfo.range.startOffset = FinishCurrentBlock(matchStart, TextBlockType::RawText); + break; + case RM_RawBegin: + if (m_currentBlockInfo.type == TextBlockType::RawBlock) + break; + else if (m_currentBlockInfo.type != TextBlockType::RawText && m_currentBlockInfo.type != TextBlockType::Comment) + { + FinishCurrentLine(match.position() + match.length()); + return MakeParseError(ErrorCode::UnexpectedRawBegin, MakeToken(Token::RawBegin, { matchStart, matchStart + match.length() })); + } + StartControlBlock(TextBlockType::RawBlock, matchStart, matchStart + match.length()); + break; + case RM_RawEnd: + if (m_currentBlockInfo.type == TextBlockType::Comment) + break; + else if (m_currentBlockInfo.type != TextBlockType::RawBlock) + { + FinishCurrentLine(match.position() + match.length()); + return MakeParseError(ErrorCode::UnexpectedRawEnd, MakeToken(Token::RawEnd, { matchStart, matchStart + match.length() })); + } + m_currentBlockInfo.range.startOffset = FinishCurrentBlock(matchStart + match.length() - 2, TextBlockType::RawText, matchStart); + break; + case RM_MetaBegin: + if (m_currentBlockInfo.type == TextBlockType::Comment) + break; + if ((m_currentBlockInfo.type != TextBlockType::RawText && m_currentBlockInfo.type != TextBlockType::Comment) || m_hasMetaBlock) + { + FinishCurrentLine(match.position() + match.length()); + return MakeParseError(ErrorCode::UnexpectedMetaBegin, MakeToken(Token::MetaBegin, { matchStart, matchStart + match.length() })); + } + StartControlBlock(TextBlockType::MetaBlock, matchStart, matchStart + match.length()); + m_metadataLocation.line = m_currentLineInfo.lineNumber + 1; + m_metadataLocation.col = static_cast(match.position() - m_currentLineInfo.range.startOffset + 1); + m_metadataLocation.fileName = m_templateName; + break; + case RM_MetaEnd: + if (m_currentBlockInfo.type == TextBlockType::Comment) + break; + if (m_currentBlockInfo.type != TextBlockType::MetaBlock) + { + FinishCurrentLine(match.position() + match.length()); + return MakeParseError(ErrorCode::UnexpectedMetaEnd, MakeToken(Token::MetaEnd, { matchStart, matchStart + match.length() })); + } + m_currentBlockInfo.range.startOffset = FinishCurrentBlock(matchStart + match.length() - 2, TextBlockType::MetaBlock, matchStart); + m_hasMetaBlock = true; + break; } return nonstd::expected(); } - void StartControlBlock(TextBlockType blockType, size_t matchStart) + void StartControlBlock(TextBlockType blockType, size_t matchStart, size_t startOffset = 0) { - size_t startOffset = matchStart + 2; + if (!startOffset) + startOffset = matchStart + 2; + size_t endOffset = matchStart; - if (m_currentBlockInfo.type != TextBlockType::RawText) + if (m_currentBlockInfo.type != TextBlockType::RawText || m_currentBlockInfo.type == TextBlockType::RawBlock) return; else - endOffset = StripBlockLeft(m_currentBlockInfo, startOffset, endOffset); + endOffset = StripBlockLeft(m_currentBlockInfo, startOffset, endOffset, blockType == TextBlockType::Expression ? false : m_settings.lstripBlocks); - FinishCurrentBlock(endOffset); - if (startOffset < m_template->size()) + FinishCurrentBlock(endOffset, blockType); + if (startOffset < m_template->size() && blockType != TextBlockType::MetaBlock) { - if ((*m_template)[startOffset] == '+' || - (*m_template)[startOffset] == '-') - ++ startOffset; + if ((*m_template)[startOffset] == '+' || (*m_template)[startOffset] == '-') + ++startOffset; } - m_currentBlockInfo.range.startOffset = startOffset; + m_currentBlockInfo.type = blockType; + + if (blockType == TextBlockType::RawBlock) + startOffset = StripBlockRight(m_currentBlockInfo, startOffset - 2, m_settings.trimBlocks); + + m_currentBlockInfo.range.startOffset = startOffset; } - size_t StripBlockLeft(TextBlockInfo& currentBlockInfo, size_t ctrlCharPos, size_t endOffset) + size_t StripBlockRight(TextBlockInfo& /* currentBlockInfo */, size_t position, bool trimBlocks) { - bool doStrip = m_settings.lstripBlocks; + bool doTrim = trimBlocks; + + size_t newPos = position + 2; + + if ((m_currentBlockInfo.type != TextBlockType::RawText) && position != 0) + { + auto ctrlChar = (*m_template)[position - 1]; + doTrim = ctrlChar == '-' ? true : (ctrlChar == '+' ? false : doTrim); + } + + if (doTrim) + { + auto locale = std::locale(); + for (; newPos < m_template->size(); ++newPos) + { + auto ch = (*m_template)[newPos]; + if (ch == '\n') + { + ++newPos; + break; + } + if (!std::isspace(ch, locale)) + break; + } + } + return newPos; + } + + size_t StripBlockLeft(TextBlockInfo& currentBlockInfo, size_t ctrlCharPos, size_t endOffset, bool doStrip) + { + bool doTotalStrip = false; if (ctrlCharPos < m_template->size()) { auto ctrlChar = (*m_template)[ctrlCharPos]; - doStrip = ctrlChar == '+' ? false : (ctrlChar == '-' ? true : doStrip); + if (ctrlChar == '+') + doStrip = false; + else + doTotalStrip = ctrlChar == '-'; + + doStrip |= doTotalStrip; } - if (!doStrip || currentBlockInfo.type != TextBlockType::RawText) + if (!doStrip || (currentBlockInfo.type != TextBlockType::RawText && currentBlockInfo.type != TextBlockType::RawBlock)) return endOffset; auto locale = std::locale(); auto& tpl = *m_template; - for (; endOffset > 0; -- endOffset) + auto originalOffset = endOffset; + bool sameLine = true; + for (; endOffset != currentBlockInfo.range.startOffset && endOffset > 0; --endOffset) { auto ch = tpl[endOffset - 1]; - if (!std::isspace(ch, locale) || ch == '\n') - break; + if (!std::isspace(ch, locale)) + { + if (!sameLine) + break; + + return doTotalStrip ? endOffset : originalOffset; + } + if (ch == '\n') + { + if (!doTotalStrip) + break; + sameLine = false; + } } return endOffset; } @@ -450,7 +633,6 @@ class TemplateParser : public LexerHelper nonstd::expected> DoFineParsing(std::shared_ptr renderers) { std::vector errors; - TextBlockInfo* prevBlock = nullptr; StatementInfoList statementsStack; StatementInfo root = StatementInfo::Create(StatementInfo::TemplateRoot, Token(), renderers); statementsStack.push_back(root); @@ -458,45 +640,50 @@ class TemplateParser : public LexerHelper { auto block = origBlock; if (block.type == TextBlockType::LineStatement) - ++ block.range.startOffset; + ++block.range.startOffset; switch (block.type) { - case TextBlockType::RawText: - { - if (block.range.size() == 0) + case TextBlockType::RawBlock: + case TextBlockType::RawText: + { + auto range = block.range; + if (range.size() == 0) + break; + auto renderer = std::make_shared(m_template->data() + range.startOffset, range.size()); + statementsStack.back().currentComposition->AddRenderer(renderer); break; - auto range = block.range; - if ((*m_template)[range.startOffset] == '\n' && prevBlock != nullptr && - prevBlock->type != TextBlockType::RawText && prevBlock->type != TextBlockType::Expression) - range.startOffset ++; - if (range.size() == 0) + } + case TextBlockType::MetaBlock: + { + auto range = block.range; + if (range.size() == 0) + break; + auto metadata = nonstd::basic_string_view(m_template->data() + range.startOffset, range.size()); + if (!boost::algorithm::all(metadata, boost::algorithm::is_space())) + m_metadata = metadata; + break; + } + case TextBlockType::Expression: + { + auto parseResult = InvokeParser(block); + if (parseResult) + statementsStack.back().currentComposition->AddRenderer(*parseResult); + else + errors.push_back(parseResult.error()); + break; + } + case TextBlockType::Statement: + case TextBlockType::LineStatement: + { + auto parseResult = InvokeParser(block, statementsStack); + if (!parseResult) + errors.push_back(parseResult.error()); + break; + } + default: break; - auto renderer = std::make_shared(m_template->data() + range.startOffset, range.size()); - statementsStack.back().currentComposition->AddRenderer(renderer); - break; - } - case TextBlockType::Expression: - { - auto parseResult = InvokeParser(block); - if (parseResult) - statementsStack.back().currentComposition->AddRenderer(*parseResult); - else - errors.push_back(parseResult.error()); - break; - } - case TextBlockType::Statement: - case TextBlockType::LineStatement: - { - auto parseResult = InvokeParser(block, statementsStack); - if (!parseResult) - errors.push_back(parseResult.error()); - break; - } - default: - break; } - prevBlock = &origBlock; } if (!errors.empty()) @@ -504,26 +691,28 @@ class TemplateParser : public LexerHelper return nonstd::expected>(); } - template - nonstd::expected InvokeParser(const TextBlockInfo& block, Args&& ... args) + template + nonstd::expected InvokeParser(const TextBlockInfo& block, Args&&... args) { lexertk::generator tokenizer; auto range = block.range; auto start = m_template->data(); if (!tokenizer.process(start + range.startOffset, start + range.endOffset)) - return MakeParseError(ErrorCode::Unspecified, MakeToken(Token::Unknown, {range.startOffset, range.startOffset + 1})); + return MakeParseError(ErrorCode::Unspecified, MakeToken(Token::Unknown, { range.startOffset, range.startOffset + 1 })); tokenizer.begin(); - Lexer lexer([this, &tokenizer, adjust = range.startOffset]() mutable { - lexertk::token tok = tokenizer.next_token(); - tok.position += adjust; - return tok; - }, this); + Lexer lexer( + [&tokenizer, adjust = range.startOffset]() mutable { + lexertk::token tok = tokenizer.next_token(); + tok.position += adjust; + return tok; + }, + this); if (!lexer.Preprocess()) - return MakeParseError(ErrorCode::Unspecified, MakeToken(Token::Unknown, {range.startOffset, range.startOffset + 1})); + return MakeParseError(ErrorCode::Unspecified, MakeToken(Token::Unknown, { range.startOffset, range.startOffset + 1 })); - P praser; + P praser(m_settings, m_env); LexScanner scanner(lexer); auto result = praser.Parse(scanner, std::forward(args)...); if (!result) @@ -567,7 +756,7 @@ class TemplateParser : public LexerHelper Token tok; tok.type = type; tok.range = range; - tok.value = value; + tok.value = TargetString(static_cast(value)); return tok; } @@ -576,54 +765,53 @@ class TemplateParser : public LexerHelper { auto p = traits_t::s_tokens.find(tok.type); if (p != traits_t::s_tokens.end()) - return p->second.template GetValue(); + return p->second.template GetValueStr(); if (tok.range.size() != 0) - return m_template->substr(tok.range.startOffset, tok.range.size()); + return string_t(m_template->substr(tok.range.startOffset, tok.range.size())); else if (tok.type == Token::Identifier) { - if (tok.value.which() != 0) + if (!tok.value.IsEmpty()) { std::basic_string tpl; - return GetAsSameString(tpl, tok.value); + return GetAsSameString(tpl, tok.value).value_or(std::basic_string()); } - return UNIVERSAL_STR("<>").template GetValue(); + return UNIVERSAL_STR("<>").template GetValueStr(); } else if (tok.type == Token::String) - return UNIVERSAL_STR("<>").template GetValue(); + return UNIVERSAL_STR("<>").template GetValueStr(); return string_t(); } - size_t FinishCurrentBlock(size_t position) + size_t FinishCurrentBlock(size_t position, TextBlockType nextBlockType, size_t matchStart = 0) { - bool doTrim = m_settings.trimBlocks && m_currentBlockInfo.type == TextBlockType::Statement; - size_t newPos = position + 2; + size_t newPos = position; - if ((m_currentBlockInfo.type != TextBlockType::RawText) && position != 0) + if (m_currentBlockInfo.type == TextBlockType::RawBlock || m_currentBlockInfo.type == TextBlockType::MetaBlock) { - auto ctrlChar = (*m_template)[position - 1]; - doTrim = ctrlChar == '-' ? true : (ctrlChar == '+' ? false : doTrim); - if (ctrlChar == '+' || ctrlChar == '-') - -- position; + size_t currentPosition = matchStart ? matchStart : position; + auto origPos = position; + position = StripBlockLeft(m_currentBlockInfo, currentPosition + 2, currentPosition, m_settings.lstripBlocks); + newPos = StripBlockRight(m_currentBlockInfo, origPos, m_settings.trimBlocks); } - - if (doTrim) + else { - auto locale = std::locale(); - for (;newPos < m_template->size(); ++ newPos) + if (m_currentBlockInfo.type == TextBlockType::RawText) + position = + StripBlockLeft(m_currentBlockInfo, position + 2, position, nextBlockType == TextBlockType::Expression ? false : m_settings.lstripBlocks); + else if (nextBlockType == TextBlockType::RawText) + newPos = StripBlockRight(m_currentBlockInfo, position, m_currentBlockInfo.type == TextBlockType::Expression ? false : m_settings.trimBlocks); + + if ((m_currentBlockInfo.type != TextBlockType::RawText) && position != 0) { - auto ch = (*m_template)[newPos]; - if (ch == '\n') - { - ++ newPos; - break; - } - if (!std::isspace(ch, locale)) - break; + auto ctrlChar = (*m_template)[position - 1]; + if (ctrlChar == '+' || ctrlChar == '-') + --position; } } + m_currentBlockInfo.range.endOffset = position; m_textBlocks.push_back(m_currentBlockInfo); m_currentBlockInfo.type = TextBlockType::RawText; @@ -634,20 +822,20 @@ class TemplateParser : public LexerHelper { m_currentLineInfo.range.endOffset = static_cast(position); m_lines.push_back(m_currentLineInfo); - m_currentLineInfo.lineNumber ++; + m_currentLineInfo.lineNumber++; } void OffsetToLinePos(size_t offset, unsigned& line, unsigned& col) { - auto p = std::find_if(m_lines.begin(), m_lines.end(), [offset](const LineInfo& info) { - return offset >= info.range.startOffset && offset < info.range.endOffset;}); + auto p = std::find_if( + m_lines.begin(), m_lines.end(), [offset](const LineInfo& info) { return offset >= info.range.startOffset && offset < info.range.endOffset; }); if (p == m_lines.end()) { - if (offset != m_lines.back().range.endOffset) + if (m_lines.empty() || offset != m_lines.back().range.endOffset) { - line = 0; - col = 0; + line = 1; + col = 1; return; } p = m_lines.end() - 1; @@ -662,10 +850,10 @@ class TemplateParser : public LexerHelper if (line == 0 && col == 0) return string_t(); - -- line; - -- col; + --line; + --col; - auto toCharT = [](char ch) {return static_cast(ch);}; + auto toCharT = [](char ch) { return static_cast(ch); }; auto& lineInfo = m_lines[line]; std::basic_ostringstream os; @@ -687,11 +875,11 @@ class TemplateParser : public LexerHelper if (col < spacePrefixLen) { - for (unsigned i = 0; i < col; ++ i) + for (unsigned i = 0; i < col; ++i) os << toCharT(' '); os << toCharT('^'); - for (int i = 0; i < tailLen; ++ i) + for (int i = 0; i < tailLen; ++i) os << toCharT('-'); return os.str(); } @@ -701,32 +889,32 @@ class TemplateParser : public LexerHelper if (actualHeadLen == headLen) { - for (int i = 0; i < col - actualHeadLen - spacePrefixLen; ++ i) + for (std::size_t i = 0; i < col - actualHeadLen - spacePrefixLen; ++i) os << toCharT(' '); } - for (int i = 0; i < actualHeadLen; ++ i) + for (int i = 0; i < actualHeadLen; ++i) os << toCharT('-'); os << toCharT('^'); - for (int i = 0; i < tailLen; ++ i) + for (int i = 0; i < tailLen; ++i) os << toCharT('-'); return os.str(); } // LexerHelper interface - std::string GetAsString(const CharRange& range) override - { - return traits_t::GetAsString(*m_template, range); - } + std::string GetAsString(const CharRange& range) override { return traits_t::GetAsString(*m_template, range); } InternalValue GetAsValue(const CharRange& range, Token::Type type) override { if (type == Token::String) - return InternalValue(m_template->substr(range.startOffset, range.size())); - else if (type == Token::IntegerNum || type == Token::FloatNum) + { + auto rawValue = CompileEscapes(m_template->substr(range.startOffset, range.size())); + return InternalValue(TargetString(std::move(rawValue))); + } + if (type == Token::IntegerNum || type == Token::FloatNum) return traits_t::RangeToNum(*m_template, range, type); return InternalValue(); } - Token::Type GetKeyword(const CharRange& range) override + Keyword GetKeyword(const CharRange& range) override { auto matchBegin = sregex_iterator(m_template->begin() + range.startOffset, m_template->begin() + range.endOffset, m_keywords); auto matchEnd = sregex_iterator(); @@ -734,10 +922,10 @@ class TemplateParser : public LexerHelper auto matches = std::distance(matchBegin, matchEnd); // One line, no customization if (matches == 0) - return Token::Unknown; + return Keyword::Unknown; auto& match = *matchBegin; - for (size_t idx = 1; idx != match.size(); ++ idx) + for (size_t idx = 1; idx != match.size(); ++idx) { if (match.length(idx) != 0) { @@ -745,120 +933,149 @@ class TemplateParser : public LexerHelper } } - return Token::Unknown; - } - char GetCharAt(size_t /*pos*/) override - { - return '\0'; + return Keyword::Unknown; } + char GetCharAt(size_t /*pos*/) override { return '\0'; } + private: const string_t* m_template; std::string m_templateName; const Settings& m_settings; - std::basic_regex m_roughTokenizer; - std::basic_regex m_keywords; + TemplateEnv* m_env = nullptr; + BasicRegex m_roughTokenizer; + BasicRegex m_keywords; std::vector m_lines; std::vector m_textBlocks; - LineInfo m_currentLineInfo; - TextBlockInfo m_currentBlockInfo; + LineInfo m_currentLineInfo = {}; + TextBlockInfo m_currentBlockInfo = {}; + bool m_hasMetaBlock = false; + nonstd::basic_string_view m_metadata; + std::string m_metadataType; + SourceLocation m_metadataLocation; }; template -KeywordsInfo ParserTraitsBase::s_keywordsInfo[30] = { - {UNIVERSAL_STR("for"), Token::For}, - {UNIVERSAL_STR("endfor"), Token::Endfor}, - {UNIVERSAL_STR("in"), Token::In}, - {UNIVERSAL_STR("if"), Token::If}, - {UNIVERSAL_STR("else"), Token::Else}, - {UNIVERSAL_STR("elif"), Token::ElIf}, - {UNIVERSAL_STR("endif"), Token::EndIf}, - {UNIVERSAL_STR("or"), Token::LogicalOr}, - {UNIVERSAL_STR("and"), Token::LogicalAnd}, - {UNIVERSAL_STR("not"), Token::LogicalNot}, - {UNIVERSAL_STR("is"), Token::Is}, - {UNIVERSAL_STR("block"), Token::Block}, - {UNIVERSAL_STR("endblock"), Token::EndBlock}, - {UNIVERSAL_STR("extends"), Token::Extends}, - {UNIVERSAL_STR("macro"), Token::Macro}, - {UNIVERSAL_STR("endmacro"), Token::EndMacro}, - {UNIVERSAL_STR("call"), Token::Call}, - {UNIVERSAL_STR("endcall"), Token::EndCall}, - {UNIVERSAL_STR("filter"), Token::Filter}, - {UNIVERSAL_STR("endfilter"), Token::EndFilter}, - {UNIVERSAL_STR("set"), Token::Set}, - {UNIVERSAL_STR("endset"), Token::EndSet}, - {UNIVERSAL_STR("include"), Token::Include}, - {UNIVERSAL_STR("import"), Token::Import}, - {UNIVERSAL_STR("true"), Token::True}, - {UNIVERSAL_STR("false"), Token::False}, - {UNIVERSAL_STR("True"), Token::True}, - {UNIVERSAL_STR("False"), Token::False}, - {UNIVERSAL_STR("none"), Token::None}, - {UNIVERSAL_STR("None"), Token::None}, +KeywordsInfo ParserTraitsBase::s_keywordsInfo[41] = { + { UNIVERSAL_STR("for"), Keyword::For }, + { UNIVERSAL_STR("endfor"), Keyword::Endfor }, + { UNIVERSAL_STR("in"), Keyword::In }, + { UNIVERSAL_STR("if"), Keyword::If }, + { UNIVERSAL_STR("else"), Keyword::Else }, + { UNIVERSAL_STR("elif"), Keyword::ElIf }, + { UNIVERSAL_STR("endif"), Keyword::EndIf }, + { UNIVERSAL_STR("or"), Keyword::LogicalOr }, + { UNIVERSAL_STR("and"), Keyword::LogicalAnd }, + { UNIVERSAL_STR("not"), Keyword::LogicalNot }, + { UNIVERSAL_STR("is"), Keyword::Is }, + { UNIVERSAL_STR("block"), Keyword::Block }, + { UNIVERSAL_STR("endblock"), Keyword::EndBlock }, + { UNIVERSAL_STR("extends"), Keyword::Extends }, + { UNIVERSAL_STR("macro"), Keyword::Macro }, + { UNIVERSAL_STR("endmacro"), Keyword::EndMacro }, + { UNIVERSAL_STR("call"), Keyword::Call }, + { UNIVERSAL_STR("endcall"), Keyword::EndCall }, + { UNIVERSAL_STR("filter"), Keyword::Filter }, + { UNIVERSAL_STR("endfilter"), Keyword::EndFilter }, + { UNIVERSAL_STR("set"), Keyword::Set }, + { UNIVERSAL_STR("endset"), Keyword::EndSet }, + { UNIVERSAL_STR("include"), Keyword::Include }, + { UNIVERSAL_STR("import"), Keyword::Import }, + { UNIVERSAL_STR("true"), Keyword::True }, + { UNIVERSAL_STR("false"), Keyword::False }, + { UNIVERSAL_STR("True"), Keyword::True }, + { UNIVERSAL_STR("False"), Keyword::False }, + { UNIVERSAL_STR("none"), Keyword::None }, + { UNIVERSAL_STR("None"), Keyword::None }, + { 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 }, + { UNIVERSAL_STR("context"), Keyword::Context }, + { UNIVERSAL_STR("from"), Keyword::From }, + { UNIVERSAL_STR("as"), Keyword::As }, + { UNIVERSAL_STR("do"), Keyword::Do }, }; template std::unordered_map ParserTraitsBase::s_tokens = { - {Token::Unknown, UNIVERSAL_STR("<>")}, - {Token::Lt, UNIVERSAL_STR("<")}, - {Token::Gt, UNIVERSAL_STR(">")}, - {Token::Plus, UNIVERSAL_STR("+")}, - {Token::Minus, UNIVERSAL_STR("-")}, - {Token::Percent, UNIVERSAL_STR("%")}, - {Token::Mul, UNIVERSAL_STR("*")}, - {Token::Div, UNIVERSAL_STR("/")}, - {Token::LBracket, UNIVERSAL_STR("(")}, - {Token::RBracket, UNIVERSAL_STR(")")}, - {Token::LSqBracket, UNIVERSAL_STR("[")}, - {Token::RSqBracket, UNIVERSAL_STR("]")}, - {Token::LCrlBracket, UNIVERSAL_STR("{")}, - {Token::RCrlBracket, UNIVERSAL_STR("}")}, - {Token::Assign, UNIVERSAL_STR("=")}, - {Token::Comma, UNIVERSAL_STR(",")}, - {Token::Eof, UNIVERSAL_STR("<>")}, - {Token::Equal, UNIVERSAL_STR("==")}, - {Token::NotEqual, UNIVERSAL_STR("!=")}, - {Token::LessEqual, UNIVERSAL_STR("<=")}, - {Token::GreaterEqual, UNIVERSAL_STR(">=")}, - {Token::StarStar, UNIVERSAL_STR("**")}, - {Token::DashDash, UNIVERSAL_STR("//")}, - {Token::LogicalOr, UNIVERSAL_STR("or")}, - {Token::LogicalAnd, UNIVERSAL_STR("and")}, - {Token::LogicalNot, UNIVERSAL_STR("not")}, - {Token::MulMul, UNIVERSAL_STR("**")}, - {Token::DivDiv, UNIVERSAL_STR("//")}, - {Token::True, UNIVERSAL_STR("true")}, - {Token::False, UNIVERSAL_STR("false")}, - {Token::None, UNIVERSAL_STR("none")}, - {Token::In, UNIVERSAL_STR("in")}, - {Token::Is, UNIVERSAL_STR("is")}, - {Token::For, UNIVERSAL_STR("for")}, - {Token::Endfor, UNIVERSAL_STR("endfor")}, - {Token::If, UNIVERSAL_STR("if")}, - {Token::Else, UNIVERSAL_STR("else")}, - {Token::ElIf, UNIVERSAL_STR("elif")}, - {Token::EndIf, UNIVERSAL_STR("endif")}, - {Token::Block, UNIVERSAL_STR("block")}, - {Token::EndBlock, UNIVERSAL_STR("endblock")}, - {Token::Extends, UNIVERSAL_STR("extends")}, - {Token::Macro, UNIVERSAL_STR("macro")}, - {Token::EndMacro, UNIVERSAL_STR("endmacro")}, - {Token::Call, UNIVERSAL_STR("call")}, - {Token::EndCall, UNIVERSAL_STR("endcall")}, - {Token::Filter, UNIVERSAL_STR("filter")}, - {Token::EndFilter, UNIVERSAL_STR("endfilter")}, - {Token::Set, UNIVERSAL_STR("set")}, - {Token::EndSet, UNIVERSAL_STR("endset")}, - {Token::Include, UNIVERSAL_STR("include")}, - {Token::Import, UNIVERSAL_STR("import")}, - {Token::CommentBegin, UNIVERSAL_STR("{#")}, - {Token::CommentEnd, UNIVERSAL_STR("#}")}, - {Token::StmtBegin, UNIVERSAL_STR("{%")}, - {Token::StmtEnd, UNIVERSAL_STR("%}")}, - {Token::ExprBegin, UNIVERSAL_STR("{{")}, - {Token::ExprEnd, UNIVERSAL_STR("}}")}, + { Token::Unknown, UNIVERSAL_STR("<>") }, + { Token::Lt, UNIVERSAL_STR("<") }, + { Token::Gt, UNIVERSAL_STR(">") }, + { Token::Plus, UNIVERSAL_STR("+") }, + { Token::Minus, UNIVERSAL_STR("-") }, + { Token::Percent, UNIVERSAL_STR("%") }, + { Token::Mul, UNIVERSAL_STR("*") }, + { Token::Div, UNIVERSAL_STR("/") }, + { Token::LBracket, UNIVERSAL_STR("(") }, + { Token::RBracket, UNIVERSAL_STR(")") }, + { Token::LSqBracket, UNIVERSAL_STR("[") }, + { Token::RSqBracket, UNIVERSAL_STR("]") }, + { Token::LCrlBracket, UNIVERSAL_STR("{") }, + { Token::RCrlBracket, UNIVERSAL_STR("}") }, + { Token::Assign, UNIVERSAL_STR("=") }, + { Token::Comma, UNIVERSAL_STR(",") }, + { Token::Eof, UNIVERSAL_STR("<>") }, + { Token::Equal, UNIVERSAL_STR("==") }, + { Token::NotEqual, UNIVERSAL_STR("!=") }, + { Token::LessEqual, UNIVERSAL_STR("<=") }, + { Token::GreaterEqual, UNIVERSAL_STR(">=") }, + { Token::StarStar, UNIVERSAL_STR("**") }, + { Token::DashDash, UNIVERSAL_STR("//") }, + { Token::LogicalOr, UNIVERSAL_STR("or") }, + { Token::LogicalAnd, UNIVERSAL_STR("and") }, + { Token::LogicalNot, UNIVERSAL_STR("not") }, + { Token::MulMul, UNIVERSAL_STR("**") }, + { Token::DivDiv, UNIVERSAL_STR("//") }, + { Token::True, UNIVERSAL_STR("true") }, + { Token::False, UNIVERSAL_STR("false") }, + { Token::None, UNIVERSAL_STR("none") }, + { Token::In, UNIVERSAL_STR("in") }, + { Token::Is, UNIVERSAL_STR("is") }, + { Token::For, UNIVERSAL_STR("for") }, + { Token::Endfor, UNIVERSAL_STR("endfor") }, + { Token::If, UNIVERSAL_STR("if") }, + { Token::Else, UNIVERSAL_STR("else") }, + { Token::ElIf, UNIVERSAL_STR("elif") }, + { Token::EndIf, UNIVERSAL_STR("endif") }, + { Token::Block, UNIVERSAL_STR("block") }, + { Token::EndBlock, UNIVERSAL_STR("endblock") }, + { Token::Extends, UNIVERSAL_STR("extends") }, + { Token::Macro, UNIVERSAL_STR("macro") }, + { Token::EndMacro, UNIVERSAL_STR("endmacro") }, + { Token::Call, UNIVERSAL_STR("call") }, + { Token::EndCall, UNIVERSAL_STR("endcall") }, + { Token::Filter, UNIVERSAL_STR("filter") }, + { Token::EndFilter, UNIVERSAL_STR("endfilter") }, + { Token::Set, UNIVERSAL_STR("set") }, + { Token::EndSet, UNIVERSAL_STR("endset") }, + { Token::Include, UNIVERSAL_STR("include") }, + { Token::Import, UNIVERSAL_STR("import") }, + { 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") }, + { Token::Context, UNIVERSAL_STR("context") }, + { Token::From, UNIVERSAL_STR("form") }, + { Token::As, UNIVERSAL_STR("as") }, + { Token::Do, UNIVERSAL_STR("do") }, + { Token::RawBegin, UNIVERSAL_STR("{% raw %}") }, + { Token::RawEnd, UNIVERSAL_STR("{% endraw %}") }, + { Token::MetaBegin, UNIVERSAL_STR("{% meta %}") }, + { Token::MetaEnd, UNIVERSAL_STR("{% endmeta %}") }, + { Token::CommentBegin, UNIVERSAL_STR("{#") }, + { Token::CommentEnd, UNIVERSAL_STR("#}") }, + { Token::StmtBegin, UNIVERSAL_STR("{%") }, + { Token::StmtEnd, UNIVERSAL_STR("%}") }, + { Token::ExprBegin, UNIVERSAL_STR("{{") }, + { Token::ExprEnd, UNIVERSAL_STR("}}") }, }; -} // jinga2 +} // namespace jinja2 -#endif // TEMPLATE_PARSER_H +#endif // JINJA2CPP_SRC_TEMPLATE_PARSER_H diff --git a/src/testers.cpp b/src/testers.cpp index 7f803044..bf18bb99 100644 --- a/src/testers.cpp +++ b/src/testers.cpp @@ -50,11 +50,11 @@ std::unordered_map s_testers = { {"upper", TesterFactory::MakeCreator(testers::ValueTester::IsUpperMode)}, }; -TesterPtr CreateTester(std::string testerName, CallParams params) +TesterPtr CreateTester(std::string testerName, CallParamsInfo params) { auto p = s_testers.find(testerName); if (p == s_testers.end()) - return TesterPtr(); + return std::make_shared(std::move(testerName), std::move(params)); return p->second(std::move(params)); } @@ -86,7 +86,7 @@ bool Defined::Test(const InternalValue& baseVal, RenderContext& /*context*/) StartsWith::StartsWith(TesterParams params) { bool parsed = true; - auto args = helpers::ParseCallParams({{"str", true}}, params, parsed); + auto args = helpers::ParseCallParamsInfo({ { "str", true } }, params, parsed); m_stringEval = args["str"]; } @@ -163,6 +163,11 @@ struct ValueKindGetter : visitors::BaseVisitor { return ValueKind::String; } + template + ValueKind operator()(const nonstd::basic_string_view&) const + { + return ValueKind::String; + } ValueKind operator()(int64_t) const { return ValueKind::Integer; @@ -187,7 +192,7 @@ struct ValueKindGetter : visitors::BaseVisitor { return ValueKind::Callable; } - ValueKind operator()(RendererBase*) const + ValueKind operator()(IRendererBase*) const { return ValueKind::Renderer; } @@ -210,14 +215,14 @@ bool ValueTester::Test(const InternalValue& baseVal, RenderContext& context) if (valKind == ValueKind::Integer) { auto intVal = ConvertToInt(val); - result = testMode == (intVal & 1) == (EvenTest ? 0 : 1); + result = (intVal & 1) == (testMode == EvenTest ? 0 : 1); } else if (valKind == ValueKind::Double) { auto dblVal = ConvertToDouble(val); - int64_t intVal = dblVal; + int64_t intVal = static_cast(dblVal); if (dblVal == intVal) - result = testMode == (intVal & 1) == (EvenTest ? 0 : 1); + result = (intVal & 1) == (testMode == EvenTest ? 0 : 1); } return result; }; @@ -249,22 +254,33 @@ bool ValueTester::Test(const InternalValue& baseVal, RenderContext& context) { bool isConverted = false; auto seq = GetArgumentValue("seq", context); - ListAdapter values = ConvertToList(seq, InternalValue(), isConverted); + auto seqKind = Apply(seq); + if (seqKind == ValueKind::List) { + ListAdapter values = ConvertToList(seq, InternalValue(), isConverted); - if (!isConverted) - return false; + if (!isConverted) + return false; - auto equalComparator = [&baseVal](auto& val) { - InternalValue cmpRes; + auto equalComparator = [&baseVal](auto& val) { + InternalValue cmpRes; + cmpRes = Apply2(val, baseVal, BinaryExpression::LogicalEq); + return ConvertToBool(cmpRes); + }; - cmpRes = Apply2(val, baseVal, BinaryExpression::LogicalEq); + auto p = std::find_if(values.begin(), values.end(), equalComparator); + result = p != values.end(); + } else if (seqKind == ValueKind::String) { + result = ApplyStringConverter(baseVal, [&](const auto& srcStr) { + std::decay_t emptyStrView; + using CharT = typename decltype(emptyStrView)::value_type; + std::basic_string emptyStr; - return ConvertToBool(cmpRes); - }; - - auto p = std::find_if(values.begin(), values.end(), equalComparator); - result = p != values.end(); + auto substring = sv_to_string(srcStr); + auto seq = GetAsSameString(srcStr, this->GetArgumentValue("seq", context)).value_or(emptyStr); + return seq.find(substring) != std::string::npos; + }); + } break; } case IsEvenMode: @@ -325,5 +341,39 @@ bool ValueTester::Test(const InternalValue& baseVal, RenderContext& context) } return result; } + +UserDefinedTester::UserDefinedTester(std::string testerName, TesterParams params) + : m_testerName(std::move(testerName)) +{ + ParseParams({{"*args"}, {"**kwargs"}}, params); + m_callParams.kwParams = m_args.extraKwArgs; + m_callParams.posParams = m_args.extraPosArgs; } + +bool UserDefinedTester::Test(const InternalValue& baseVal, RenderContext& context) +{ + bool testerFound = false; + auto testerValPtr = context.FindValue(m_testerName, testerFound); + if (!testerFound) + return false; + + const Callable* callable = GetIf(&testerValPtr->second); + if (callable == nullptr || callable->GetKind() != Callable::UserCallable) + return false; + + CallParams tmpCallParams = helpers::EvaluateCallParams(m_callParams, context); + CallParams callParams; + callParams.kwParams = std::move(tmpCallParams.kwParams); + callParams.posParams.reserve(tmpCallParams.posParams.size() + 1); + callParams.posParams.push_back(baseVal); + for (auto& p : tmpCallParams.posParams) + callParams.posParams.push_back(std::move(p)); + + InternalValue result; + if (callable->GetType() != Callable::Type::Expression) + return false; + + return ConvertToBool(callable->GetExpressionCallable()(callParams, context)); } +} // namespace testers +} // namespace jinja2 diff --git a/src/testers.h b/src/testers.h index 992ac21c..9d29e6cb 100644 --- a/src/testers.h +++ b/src/testers.h @@ -1,5 +1,5 @@ -#ifndef TESTERS_H -#define TESTERS_H +#ifndef JINJA2CPP_SRC_TESTERS_H +#define JINJA2CPP_SRC_TESTERS_H #include "expression_evaluator.h" #include "function_base.h" @@ -12,9 +12,9 @@ namespace jinja2 { using TesterPtr = std::shared_ptr; -using TesterParams = CallParams; +using TesterParams = CallParamsInfo; -extern TesterPtr CreateTester(std::string testerName, CallParams params); +extern TesterPtr CreateTester(std::string testerName, CallParamsInfo params); namespace testers { @@ -29,6 +29,13 @@ class Comparator : public TesterBase Comparator(TesterParams params, BinaryExpression::Operation op); bool Test(const InternalValue& baseVal, RenderContext& context) override; + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return m_op == val->m_op; + } private: BinaryExpression::Operation m_op; }; @@ -40,6 +47,13 @@ class StartsWith : public IsExpression::ITester bool Test(const InternalValue& baseVal, RenderContext& context) override; + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return m_stringEval == val->m_stringEval; + } private: ExpressionEvaluatorPtr<> m_stringEval; }; @@ -66,11 +80,37 @@ class ValueTester : public TesterBase ValueTester(TesterParams params, Mode mode); bool Test(const InternalValue& baseVal, RenderContext& context) override; + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return m_mode == val->m_mode; + } private: Mode m_mode; }; -} // testers -} // jinja2 +class UserDefinedTester : public TesterBase +{ +public: + UserDefinedTester(std::string filterName, TesterParams params); + + bool Test(const InternalValue& baseVal, RenderContext& context) override; + + bool IsEqual(const IComparable& other) const override + { + auto* val = dynamic_cast(&other); + if (!val) + return false; + return m_testerName == val->m_testerName && m_callParams == val->m_callParams; + } +private: + std::string m_testerName; + TesterParams m_callParams; +}; +} // namespace testers +} // namespace jinja2 -#endif // TESTERS_H +#endif // JINJA2CPP_SRC_TESTERS_H diff --git a/src/value_helpers.h b/src/value_helpers.h index 0b50216b..d5ac829f 100644 --- a/src/value_helpers.h +++ b/src/value_helpers.h @@ -1,5 +1,5 @@ -#ifndef VALUE_HELPERS_H -#define VALUE_HELPERS_H +#ifndef JINJA2CPP_SRC_VALUE_HELPERS_H +#define JINJA2CPP_SRC_VALUE_HELPERS_H #include @@ -162,6 +162,6 @@ inline auto end(const GenericList& list) return ConstGenericListIterator(); } #endif -} // jinja2 +} // namespace jinja2 -#endif // VALUE_HELPERS_H +#endif // JINJA2CPP_SRC_VALUE_HELPERS_H diff --git a/src/value_visitors.h b/src/value_visitors.h index 9f4d06ef..193680c5 100644 --- a/src/value_visitors.h +++ b/src/value_visitors.h @@ -1,16 +1,20 @@ -#ifndef VALUE_VISITORS_H -#define VALUE_VISITORS_H +#ifndef JINJA2CPP_SRC_VALUE_VISITORS_H +#define JINJA2CPP_SRC_VALUE_VISITORS_H #include "expression_evaluator.h" +#include "helpers.h" #include "jinja2cpp/value.h" #include #include +#include +#include #include #include #include #include +#include namespace jinja2 { @@ -18,38 +22,79 @@ namespace jinja2 namespace detail { +template +struct RecursiveUnwrapper +{ + V* m_visitor{}; + + RecursiveUnwrapper(V* v) + : m_visitor(v) + {} + + + template + static const auto& UnwrapRecursive(const T& arg) + { + return arg; // std::forward(arg); + } + + template + static auto& UnwrapRecursive(const RecursiveWrapper& arg) + { + return arg.GetValue(); + } + +// template +// static auto& UnwrapRecursive(RecursiveWrapper& arg) +// { +// return arg.GetValue(); +// } + + template + auto operator()(const Args& ... args) const + { + assert(m_visitor != nullptr); + return (*m_visitor)(UnwrapRecursive(args)...); + } +}; + template -auto ApplyUnwrapped(const InternalValue& val, Fn&& fn) +auto ApplyUnwrapped(const InternalValueData& val, Fn&& fn) { - auto valueRef = boost::get(&val); - auto targetString = boost::get(&val); - // auto internalValueRef = boost::get(&val); + auto valueRef = GetIf(&val); + auto targetString = GetIf(&val); + auto targetSV = GetIf(&val); + // auto internalValueRef = GetIf(&val); if (valueRef != nullptr) return fn(valueRef->get().data()); else if (targetString != nullptr) return fn(*targetString); + else if (targetSV != nullptr) + return fn(*targetSV); // else if (internalValueRef != nullptr) // return fn(internalValueRef->get()); return fn(val); } -} // detail +} // namespace detail template auto Apply(const InternalValue& val, Args&& ... args) { - return detail::ApplyUnwrapped(val, [&args...](auto& val) { - return boost::apply_visitor(V(args...), val); + return detail::ApplyUnwrapped(val.GetData(), [&args...](auto& val) { + auto v = V(args...); + return nonstd::visit(detail::RecursiveUnwrapper(&v), val); }); } template auto Apply2(const InternalValue& val1, const InternalValue& val2, Args&& ... args) { - return detail::ApplyUnwrapped(val1, [&val2, &args...](auto& uwVal1) { - return detail::ApplyUnwrapped(val2, [&uwVal1, &args...](auto& uwVal2) { - return boost::apply_visitor(V(args...), uwVal1, uwVal2); + return detail::ApplyUnwrapped(val1.GetData(), [&val2, &args...](auto& uwVal1) { + return detail::ApplyUnwrapped(val2.GetData(), [&uwVal1, &args...](auto& uwVal2) { + auto v = V(args...); + return nonstd::visit(detail::RecursiveUnwrapper(&v), uwVal1, uwVal2); }); }); } @@ -59,7 +104,7 @@ bool ConvertToBool(const InternalValue& val); namespace visitors { template -struct BaseVisitor : public boost::static_visitor +struct BaseVisitor { R operator() (const GenericMap&) const { @@ -100,17 +145,23 @@ struct BaseVisitor : public boost::static_visitor template -struct ValueRendererBase : public boost::static_visitor<> +struct ValueRendererBase { - ValueRendererBase(std::basic_ostream& os) + ValueRendererBase(std::basic_string& os) : m_os(&os) { } template - void operator()(const T& val) const + void operator()(const T& val) const; + void operator()(double val) const; + void operator()(const nonstd::basic_string_view& val) const + { + m_os->append(val.begin(), val.end()); + } + void operator()(const std::basic_string& val) const { - (*m_os) << val; + m_os->append(val.begin(), val.end()); } void operator()(const EmptyValue&) const {} @@ -122,32 +173,73 @@ struct ValueRendererBase : public boost::static_visitor<> void operator()(const ListAdapter&) const {} void operator()(const ValueRef&) const {} void operator()(const TargetString&) const {} + void operator()(const TargetStringView&) const {} void operator()(const KeyValuePair&) const {} void operator()(const Callable&) const {} - void operator()(const RendererBase*) const {} + void operator()(const UserCallable&) const {} + void operator()(const std::shared_ptr) const {} + template + void operator()(const boost::recursive_wrapper&) const {} + template + void operator()(const RecWrapper&) const {} - std::basic_ostream* m_os; + auto GetOs() const { return std::back_inserter(*m_os); } + + std::basic_string* m_os; }; -struct InputValueConvertor : public boost::static_visitor> +template<> +template +void ValueRendererBase::operator()(const T& val) const +{ + fmt::format_to(GetOs(), "{}", val); +} + +template<> +template +void ValueRendererBase::operator()(const T& val) const +{ + fmt::format_to(GetOs(), L"{}", val); +} + +template<> +inline void ValueRendererBase::operator()(double val) const +{ + fmt::format_to(GetOs(), "{:.8g}", val); +} + +template<> +inline void ValueRendererBase::operator()(double val) const +{ + fmt::format_to(GetOs(), L"{:.8g}", val); +} + +struct InputValueConvertor { using result_t = boost::optional; - InputValueConvertor(bool byValue = false) + InputValueConvertor(bool byValue, bool allowStringRef) : m_byValue(byValue) + , m_allowStringRef(allowStringRef) { } template - result_t operator() (const std::basic_string& val) const + result_t operator()(const std::basic_string& val) const { - // if (m_byValue) - return result_t(InternalValue(TargetString(val))); + if (m_allowStringRef) + return result_t(TargetStringView(nonstd::basic_string_view(val))); + + return result_t(TargetString(val)); + } - // return result_t(); + template + result_t operator()(std::basic_string& val) const + { + return result_t(TargetString(std::move(val))); } - result_t operator() (const ValuesList& vals) const + result_t operator()(const ValuesList& vals) const { if (m_byValue) { @@ -179,45 +271,58 @@ struct InputValueConvertor : public boost::static_visitor + result_t operator()(const RecWrapper& val) const + { + return this->operator()(const_cast(*val)); } - result_t operator() (GenericMap& vals) const + template + result_t operator()(RecWrapper& val) const { - return result_t(InternalValue(MapAdapter::CreateAdapter(std::move(vals)))); + return this->operator()(*val); } template - result_t operator() (T&& val) const + result_t operator()(const T& val) const { - return result_t(InternalValue(std::forward(val))); + return result_t(InternalValue(val)); } - bool m_byValue; + static result_t ConvertUserCallable(const UserCallable& val); + + bool m_byValue{}; + bool m_allowStringRef{}; }; template @@ -226,34 +331,47 @@ struct ValueRenderer; template<> struct ValueRenderer : ValueRendererBase { - ValueRenderer(std::ostream& os) + ValueRenderer(std::string& os) : ValueRendererBase::ValueRendererBase(os) { } using ValueRendererBase::operator (); - void operator()(const std::wstring&) const {} + void operator()(const std::wstring& str) const + { + (*m_os) += ConvertString(str); + } + void operator()(const nonstd::wstring_view& str) const + { + (*m_os) += ConvertString(str); + } void operator() (bool val) const { - (*m_os) << (val ? "true" : "false"); + m_os->append(val ? "true" : "false"); } }; template<> struct ValueRenderer : ValueRendererBase { - ValueRenderer(std::wostream& os) + ValueRenderer(std::wstring& os) : ValueRendererBase::ValueRendererBase(os) { } using ValueRendererBase::operator (); - void operator()(const std::string&) const + void operator()(const std::string& str) const + { + (*m_os) += ConvertString(str); + } + void operator()(const nonstd::string_view& str) const { + (*m_os) += ConvertString(str); } void operator() (bool val) const { - (*m_os) << (val ? L"true" : L"false"); + // fmt::format_to(GetOs(), L"{}", (const wchar_t*)(val ? "true" : "false")); + m_os->append(val ? L"true" : L"false"); } }; @@ -291,7 +409,7 @@ struct UnaryOperation : BaseVisitor switch (m_oper) { case jinja2::UnaryExpression::LogicalNot: - result = val ? false : true; + result = fabs(val) > std::numeric_limits::epsilon() ? false : true; break; case jinja2::UnaryExpression::UnaryPlus: result = +val; @@ -365,6 +483,22 @@ struct UnaryOperation : BaseVisitor return result; } + template + InternalValue operator() (const nonstd::basic_string_view& val) const + { + InternalValue result; + switch (m_oper) + { + case jinja2::UnaryExpression::LogicalNot: + result = val.empty(); + break; + default: + break; + } + + return result; + } + InternalValue operator() (const EmptyValue&) const { InternalValue result; @@ -386,6 +520,7 @@ struct UnaryOperation : BaseVisitor struct BinaryMathOperation : BaseVisitor<> { using BaseVisitor::operator (); + using ResultType = InternalValue; // InternalValue operator() (int, int) const {return InternalValue();} bool AlmostEqual(double x, double y) const @@ -400,9 +535,9 @@ struct BinaryMathOperation : BaseVisitor<> { } - InternalValue operator() (double left, double right) const + ResultType operator() (double left, double right) const { - InternalValue result = 0.0; + ResultType result = 0.0; switch (m_oper) { case jinja2::BinaryExpression::Plus: @@ -417,8 +552,8 @@ struct BinaryMathOperation : BaseVisitor<> case jinja2::BinaryExpression::Div: result = left / right; break; - case jinja2::BinaryExpression::DivReminder: - result = std::remainder(left, right); + case jinja2::BinaryExpression::DivRemainder: + result = std::fmod(left, right); break; case jinja2::BinaryExpression::DivInteger: { @@ -454,9 +589,9 @@ struct BinaryMathOperation : BaseVisitor<> return result; } - InternalValue operator() (int64_t left, int64_t right) const + ResultType operator() (int64_t left, int64_t right) const { - InternalValue result; + ResultType result; switch (m_oper) { case jinja2::BinaryExpression::Plus: @@ -472,7 +607,7 @@ struct BinaryMathOperation : BaseVisitor<> result = left / right; break; case jinja2::BinaryExpression::Div: - case jinja2::BinaryExpression::DivReminder: + case jinja2::BinaryExpression::DivRemainder: case jinja2::BinaryExpression::Pow: result = this->operator ()(static_cast(left), static_cast(right)); break; @@ -501,25 +636,111 @@ struct BinaryMathOperation : BaseVisitor<> return result; } - InternalValue operator() (double left, int64_t right) const + ResultType operator() (int64_t left, double right) const { return this->operator ()(static_cast(left), static_cast(right)); } - InternalValue operator() (int64_t left, double right) const + ResultType operator() (double left, int64_t right) const { return this->operator ()(static_cast(left), static_cast(right)); } template - InternalValue operator() (const std::basic_string& left, const std::basic_string& right) const + ResultType operator() (const std::basic_string &left, const std::basic_string &right) const { - InternalValue result; + return ProcessStrings(nonstd::basic_string_view(left), nonstd::basic_string_view(right)); + } + + template + std::enable_if_t::value, ResultType> operator() (const std::basic_string& left, const std::basic_string& right) const + { + auto rightStr = ConvertString>(right); + return ProcessStrings(nonstd::basic_string_view(left), nonstd::basic_string_view(rightStr)); + } + + template + ResultType operator() (const nonstd::basic_string_view &left, const std::basic_string &right) const + { + return ProcessStrings(left, nonstd::basic_string_view(right)); + } + + template + std::enable_if_t::value, ResultType> operator() (const nonstd::basic_string_view& left, const std::basic_string& right) const + { + auto rightStr = ConvertString>(right); + return ProcessStrings(left, nonstd::basic_string_view(rightStr)); + } + + template + ResultType operator() (const std::basic_string &left, const nonstd::basic_string_view &right) const + { + return ProcessStrings(nonstd::basic_string_view(left), right); + } + + template + std::enable_if_t::value, ResultType> operator() (const std::basic_string& left, const nonstd::basic_string_view& right) const + { + auto rightStr = ConvertString>(right); + return ProcessStrings(nonstd::basic_string_view(left), nonstd::basic_string_view(rightStr)); + } + + template + ResultType operator() (const nonstd::basic_string_view &left, const nonstd::basic_string_view &right) const + { + return ProcessStrings(left, right); + } + + template + std::enable_if_t::value, ResultType> operator() (const nonstd::basic_string_view& left, const nonstd::basic_string_view& right) const + { + auto rightStr = ConvertString>(right); + return ProcessStrings(left, nonstd::basic_string_view(rightStr)); + } + + template + ResultType operator() (const std::basic_string &left, int64_t right) const + { + return RepeatString(nonstd::basic_string_view(left), right); + } + + template + ResultType operator() (const nonstd::basic_string_view &left, int64_t right) const + { + return RepeatString(left, right); + } + + template + ResultType RepeatString(const nonstd::basic_string_view& left, const int64_t right) const + { + using string = std::basic_string; + ResultType result; + + if(m_oper == jinja2::BinaryExpression::Mul) + { + string str; + for (int i = 0; i < right; ++i) + str.append(left.begin(), left.end()); + result = TargetString(std::move(str)); + } + return result; + } + + template + ResultType ProcessStrings(const nonstd::basic_string_view& left, const nonstd::basic_string_view& right) const + { + using string = std::basic_string; + ResultType result; + switch (m_oper) { case jinja2::BinaryExpression::Plus: - result = left + right; + { + auto str = string(left.begin(), left.end()); + str.append(right.begin(), right.end()); + result = TargetString(std::move(str)); break; + } case jinja2::BinaryExpression::LogicalEq: result = m_compType == BinaryExpression::CaseSensitive ? left == right : boost::iequals(left, right); break; @@ -559,9 +780,9 @@ struct BinaryMathOperation : BaseVisitor<> return result; } - InternalValue operator() (const KeyValuePair& left, const KeyValuePair& right) const + ResultType operator() (const KeyValuePair& left, const KeyValuePair& right) const { - InternalValue result; + ResultType result; switch (m_oper) { case jinja2::BinaryExpression::LogicalEq: @@ -577,9 +798,43 @@ struct BinaryMathOperation : BaseVisitor<> return result; } - InternalValue operator() (bool left, bool right) const + ResultType operator() (const ListAdapter& left, const ListAdapter& right) const { - InternalValue result; + ResultType result; + if (m_oper == jinja2::BinaryExpression::Plus) + { + InternalValueList values; + values.reserve(left.GetSize().value_or(0) + right.GetSize().value_or(0)); + for (auto& v : left) + values.push_back(v); + for (auto& v : right) + values.push_back(v); + result = ListAdapter::CreateAdapter(std::move(values)); + } + + return result; + } + + ResultType operator() (const ListAdapter& left, int64_t right) const + { + ResultType result; + if (right >= 0 && m_oper == jinja2::BinaryExpression::Mul) + { + InternalValueList values; + values.reserve(left.GetSize().value_or(0)); + for (auto& v : left) + values.push_back(v); + auto listSize = values.size() * right; + result = ListAdapter::CreateAdapter(static_cast(listSize), + [size = values.size(), values = std::move(values)](size_t idx) { return values[idx % size]; }); + } + + return result; + } + + ResultType operator() (bool left, bool right) const + { + ResultType result; switch (m_oper) { case jinja2::BinaryExpression::LogicalEq: @@ -598,9 +853,9 @@ struct BinaryMathOperation : BaseVisitor<> return result; } - InternalValue operator() (EmptyValue, EmptyValue) const + ResultType operator() (EmptyValue, EmptyValue) const { - InternalValue result; + ResultType result; switch (m_oper) { case jinja2::BinaryExpression::LogicalEq: @@ -617,9 +872,9 @@ struct BinaryMathOperation : BaseVisitor<> } template - InternalValue operator() (EmptyValue, T&&) const + ResultType operator() (EmptyValue, T&&) const { - InternalValue result; + ResultType result; switch (m_oper) { case jinja2::BinaryExpression::LogicalEq: @@ -636,9 +891,9 @@ struct BinaryMathOperation : BaseVisitor<> } template - InternalValue operator() (T&&, EmptyValue) const + ResultType operator() (T&&, EmptyValue) const { - InternalValue result; + ResultType result; switch (m_oper) { case jinja2::BinaryExpression::LogicalEq: @@ -683,14 +938,20 @@ struct BooleanEvaluator : BaseVisitor return !str.empty(); } + template + bool operator()(const nonstd::basic_string_view& str) const + { + return !str.empty(); + } + bool operator() (const MapAdapter& val) const { - return val.GetSize() != 0; + return val.GetSize() != 0ULL; } bool operator() (const ListAdapter& val) const { - return val.GetSize() != 0; + return val.GetSize() != 0ULL; } bool operator() (const EmptyValue&) const @@ -700,7 +961,7 @@ struct BooleanEvaluator : BaseVisitor }; template -struct NumberEvaluator : public boost::static_visitor +struct NumberEvaluator { NumberEvaluator(TargetType def = 0) : m_def(def) {} @@ -730,36 +991,66 @@ using IntegerEvaluator = NumberEvaluator; using DoubleEvaluator = NumberEvaluator; -struct StringJoiner : BaseVisitor<> +struct StringJoiner : BaseVisitor { using BaseVisitor::operator (); - InternalValue operator() (EmptyValue, const std::string& str) const + template + TargetString operator() (EmptyValue, const std::basic_string& str) const { return str; } - InternalValue operator() (const std::string& left, const std::string& right) const + template + TargetString operator() (EmptyValue, const nonstd::basic_string_view& str) const + { + return std::basic_string(str.begin(), str.end()); + } + + template + TargetString operator() (const std::basic_string& left, const std::basic_string& right) const { return left + right; } -}; -namespace -{ -inline std::string GetSampleString(); -} + template + std::enable_if_t::value, TargetString> operator() (const std::basic_string& left, const std::basic_string& right) const + { + return left + ConvertString>(right); + } + + template + TargetString operator() (std::basic_string left, const nonstd::basic_string_view& right) const + { + left.append(right.begin(), right.end()); + return std::move(left); + } + + template + std::enable_if_t::value, TargetString> operator() (std::basic_string left, const nonstd::basic_string_view& right) const + { + auto r = ConvertString>(right); + left.append(r.begin(), r.end()); + return std::move(left); + } +}; template -struct StringConverterImpl : public BaseVisitor()(GetSampleString()))> +struct StringConverterImpl : public BaseVisitor()(std::declval()))> { - using R = decltype(std::declval()(std::string())); + using R = decltype(std::declval()(nonstd::string_view())); using BaseVisitor::operator (); StringConverterImpl(const Fn& fn) : m_fn(fn) {} template R operator()(const std::basic_string& str) const + { + return m_fn(nonstd::basic_string_view(str)); + } + + template + R operator()(const nonstd::basic_string_view& str) const { return m_fn(str); } @@ -768,18 +1059,37 @@ struct StringConverterImpl : public BaseVisitor()(GetS }; template -struct SameStringGetter : public visitors::BaseVisitor> +struct SameStringGetter : public visitors::BaseVisitor>> { using ResultString = std::basic_string; - using BaseVisitor::operator (); + using OtherString = std::conditional_t::value, std::wstring, std::string>; + using ResultStringView = nonstd::basic_string_view; + using OtherStringView = std::conditional_t::value, nonstd::wstring_view, nonstd::string_view>; + using Result = nonstd::expected; + using BaseVisitor::operator (); - ResultString operator()(const ResultString& str) const + Result operator()(const ResultString& str) const { - return str; + return nonstd::make_unexpected(str); + } + + Result operator()(const ResultStringView& str) const + { + return nonstd::make_unexpected(ResultString(str.begin(), str.end())); + } + + Result operator()(const OtherString& str) const + { + return nonstd::make_unexpected(ConvertString(str)); + } + + Result operator()(const OtherStringView& str) const + { + return nonstd::make_unexpected(ConvertString(str)); } }; -} // visitors +} // namespace visitors inline bool ConvertToBool(const InternalValue& val) { @@ -803,11 +1113,39 @@ auto ApplyStringConverter(const InternalValue& str, Fn&& fn) } template -auto GetAsSameString(const std::basic_string& s, const InternalValue& val) +auto GetAsSameString(const std::basic_string&, const InternalValue& val) +{ + using Result = nonstd::optional>; + auto result = Apply>(val); + if (!result) + return Result(result.error()); + + return Result(); +} + +template +auto GetAsSameString(const nonstd::basic_string_view&, const InternalValue& val) +{ + using Result = nonstd::optional>; + auto result = Apply>(val); + if (!result) + return Result(result.error()); + + return Result(); +} + +inline bool operator==(const InternalValueData& lhs, const InternalValueData& rhs) +{ + InternalValue cmpRes; + cmpRes = Apply2(lhs, rhs, BinaryExpression::LogicalEq); + return ConvertToBool(cmpRes); +} + +inline bool operator!=(const InternalValueData& lhs, const InternalValueData& rhs) { - return Apply>(val); + return !(lhs == rhs); } -} // jinja2 +} // namespace jinja2 -#endif // VALUE_VISITORS_H +#endif // JINJA2CPP_SRC_VALUE_VISITORS_H diff --git a/test/basic_tests.cpp b/test/basic_tests.cpp index a568049e..e9e53ddf 100644 --- a/test/basic_tests.cpp +++ b/test/basic_tests.cpp @@ -4,115 +4,302 @@ #include "gtest/gtest.h" #include "jinja2cpp/template.h" +#include "test_tools.h" using namespace jinja2; -TEST(BasicTests, PlainSingleLineTemplateProcessing) -{ - std::string source = "Hello World from Parser!"; +using BasicMultiStrTest = BasicTemplateRenderer; - Template tpl; - ASSERT_TRUE(tpl.Load(source)); - std::string result = tpl.RenderAsString(ValuesMap{}); - std::cout << result << std::endl; - std::string expectedResult = "Hello World from Parser!"; - EXPECT_STREQ(expectedResult.c_str(), result.c_str()); +MULTISTR_TEST(BasicMultiStrTest, PlainSingleLineTemplateProcessing, R"(Hello World from Parser!)", R"(Hello World from Parser!)") +{ } -TEST(BasicTests, PlainSingleLineTemplateProcessing_Wide) +MULTISTR_TEST(BasicMultiStrTest, PlainMultiLineTemplateProcessing, R"(Hello World +from Parser!)", R"(Hello World +from Parser!)") { - std::wstring source = L"Hello World from Parser!"; - - TemplateW tpl; - ASSERT_TRUE(tpl.Load(source)); +} - std::wstring result = tpl.RenderAsString(ValuesMap{}); - std::wcout << result << std::endl; - std::wstring expectedResult = L"Hello World from Parser!"; - EXPECT_STREQ(expectedResult.c_str(), result.c_str()); +MULTISTR_TEST(BasicMultiStrTest, InlineCommentsSkip, "Hello World {#Comment to skip #}from Parser!", "Hello World from Parser!") +{ } -TEST(BasicTests, PlainMultiLineTemplateProcessing) +MULTISTR_TEST(BasicMultiStrTest, SingleLineCommentsSkip, +R"(Hello World +{#Comment to skip #} +from Parser!)", +R"(Hello World + +from Parser!)") { - std::string source = R"(Hello World -from Parser!)"; +} - Template tpl; - ASSERT_TRUE(tpl.Load(source)); +MULTISTR_TEST(BasicMultiStrTest, MultiLineCommentsSkip, +R"(Hello World +{#Comment to +skip #} +from Parser!)", +R"(Hello World - std::string result = tpl.RenderAsString(ValuesMap{}); - std::cout << result << std::endl; - std::string expectedResult = R"(Hello World -from Parser!)"; - EXPECT_STREQ(expectedResult.c_str(), result.c_str()); +from Parser!)") +{ } -TEST(BasicTests, InlineCommentsSkip) +MULTISTR_TEST(BasicMultiStrTest, CommentedOutCodeSkip, +R"(Hello World +{#Comment to + {{for}} + {{endfor}} +skip #} +{#Comment to + {% + skip #} +from Parser!)", +R"(Hello World + + +from Parser!)") { - std::string source = "Hello World {#Comment to skip #}from Parser!"; +} - Template tpl; +TEST(BasicTests, StripSpaces_None) +{ + std::string source = R"(Hello World +# + {{ ' -expr- ' }} +# + {% if true %}{{ ' -stmt- ' }}{% endif %} +# + {# comment 1 #}{{ ' -comm- ' }}{# comment 2 #} +# + i = {{ '-expr- ' }} +# + i = {% if true %}{{ '-stmt- ' }}{% endif %} +# + i = {# comment 1 #}{{ '-comm- ' }}{# comment 2 #} +# +{% for i in range(3) %} + #### + i = {{i}} + #### +{% endfor %} + #### +from Parser!)"; + + TemplateEnv env; + env.GetSettings().lstripBlocks = false; + env.GetSettings().trimBlocks = false; + Template tpl(&env); ASSERT_TRUE(tpl.Load(source)); - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; - std::string expectedResult = "Hello World from Parser!"; + std::string expectedResult = "Hello World\n" + "#\n" + " -expr- \n" + "#\n" + " -stmt- \n" + "#\n" + " -comm- \n" + "#\n" + " i = -expr- \n" + "#\n" + " i = -stmt- \n" + "#\n" + " i = -comm- \n" + "#\n" + "\n" + " ####\n" + " i = 0\n" + " ####\n" + "\n" + " ####\n" + " i = 1\n" + " ####\n" + "\n" + " ####\n" + " i = 2\n" + " ####\n" + "\n" + " ####\n" + "from Parser!"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); } -TEST(BasicTests, SingleLineCommentsSkip) +TEST(BasicTests, StripSpaces_LStripBlocks) { std::string source = R"(Hello World -{#Comment to skip #} -from Parser!)"; - - Template tpl; +# + {{ ' -expr- ' }} +# + {% if true %}{{ ' -stmt- ' }}{% endif %} +# + {# comment 1 #}{{ ' -comm- ' }}{# comment 2 #} +# + i = {{ '-expr- ' }} +# + i = {% if true %}{{ '-stmt- ' }}{% endif %} +# + i = {# comment 1 #}{{ '-comm- ' }}{# comment 2 #} +# +{% for i in range(3) %} + #### + i = {{i}} + #### +{% endfor %} + #### +from Parser!)"; + + TemplateEnv env; + env.GetSettings().lstripBlocks = true; + env.GetSettings().trimBlocks = false; + Template tpl(&env); ASSERT_TRUE(tpl.Load(source)); - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; - std::string expectedResult = R"(Hello World -from Parser!)"; + std::string expectedResult = "Hello World\n" + "#\n" + " -expr- \n" + "#\n" + " -stmt- \n" + "#\n" + " -comm- \n" + "#\n" + " i = -expr- \n" + "#\n" + " i = -stmt- \n" + "#\n" + " i = -comm- \n" + "#\n" + "\n" + " ####\n" + " i = 0\n" + " ####\n" + "\n" + " ####\n" + " i = 1\n" + " ####\n" + "\n" + " ####\n" + " i = 2\n" + " ####\n" + "\n" + " ####\n" + "from Parser!"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); } -TEST(BasicTests, MultiLineCommentsSkip) +TEST(BasicTests, StripSpaces_TrimBlocks) { std::string source = R"(Hello World -{#Comment to -skip #} -from Parser!)"; - - Template tpl; +# + {{ ' -expr- ' }} +# + {% if true %}{{ ' -stmt- ' }}{% endif %} +# + {# comment 1 #}{{ ' -comm- ' }}{# comment 2 #} +# + i = {{ '-expr- ' }} +# + i = {% if true %}{{ '-stmt- ' }}{% endif %} +# + i = {# comment 1 #}{{ '-comm- ' }}{# comment 2 #} +# +{% for i in range(3) %} + #### + i = {{i}} + #### +{% endfor %} + #### +from Parser!)"; + + TemplateEnv env; + env.GetSettings().lstripBlocks = false; + env.GetSettings().trimBlocks = true; + Template tpl(&env); ASSERT_TRUE(tpl.Load(source)); - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; - std::string expectedResult = R"(Hello World -from Parser!)"; + std::string expectedResult = "Hello World\n" + "#\n" + " -expr- \n" + "#\n" + " -stmt- #\n" + " -comm- #\n" + " i = -expr- \n" + "#\n" + " i = -stmt- #\n" + " i = -comm- #\n" + " ####\n" + " i = 0\n" + " ####\n" + " ####\n" + " i = 1\n" + " ####\n" + " ####\n" + " i = 2\n" + " ####\n" + " ####\n" + "from Parser!"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); } -TEST(BasicTests, CommentedOutCodeSkip) +TEST(BasicTests, StripSpaces_LStripBlocks_TrimBlocks) { std::string source = R"(Hello World -{#Comment to - {{for}} - {{endfor}} -skip #} -{#Comment to - {% - skip #} -from Parser!)"; - - Template tpl; +# + {{ ' -expr- ' }} +# + {% if true %}{{ ' -stmt- ' }}{% endif %} +# + {# comment 1 #}{{ ' -comm- ' }}{# comment 2 #} +# + i = {{ '-expr- ' }} +# + i = {% if true %}{{ '-stmt- ' }}{% endif %} +# + i = {# comment 1 #}{{ '-comm- ' }}{# comment 2 #} +# +{% for i in range(3) %} + #### + i = {{i}} + #### +{% endfor %} + #### +from Parser!)"; + + TemplateEnv env; + env.GetSettings().lstripBlocks = true; + env.GetSettings().trimBlocks = true; + Template tpl(&env); ASSERT_TRUE(tpl.Load(source)); - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; - std::string expectedResult = R"(Hello World -from Parser!)"; + std::string expectedResult = "Hello World\n" + "#\n" + " -expr- \n" + "#\n" + " -stmt- #\n" + " -comm- #\n" + " i = -expr- \n" + "#\n" + " i = -stmt- #\n" + " i = -comm- #\n" + " ####\n" + " i = 0\n" + " ####\n" + " ####\n" + " i = 1\n" + " ####\n" + " ####\n" + " i = 2\n" + " ####\n" + " ####\n" + "from Parser!"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); } @@ -125,10 +312,9 @@ from Parser!)"; Template tpl; ASSERT_TRUE(tpl.Load(source)); - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; - std::string expectedResult = R"(Hello World - -- + std::string expectedResult = R"(Hello World -- from Parser!)"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); } @@ -142,7 +328,7 @@ from Parser!)"; Template tpl; ASSERT_TRUE(tpl.Load(source)); - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; std::string expectedResult = R"(Hello World -- @@ -159,44 +345,40 @@ from Parser!)"; Template tpl; ASSERT_TRUE(tpl.Load(source)); - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; - std::string expectedResult = R"(Hello World - --< + std::string expectedResult = R"(Hello World --< from Parser!)"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); } TEST(BasicTests, StripLSpaces_4) { - std::string source = R"(Hello World + std::string source = "Hello World\t\t \t" R"( {%- set delim = ' --' %} {{+delim}}< from Parser!)"; Template tpl; ASSERT_TRUE(tpl.Load(source)); - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; - std::string expectedResult = R"(Hello World - --< + std::string expectedResult = R"(Hello World --< from Parser!)"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); } TEST(BasicTests, StripLSpaces_5) { - std::string source = R"(Hello World - {%- set delim = ' --' %} {{-delim}}< + std::string source = R"(Hello World{%- set delim = ' --' %} {{-delim}}< from Parser!)"; Template tpl; ASSERT_TRUE(tpl.Load(source)); - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; - std::string expectedResult = R"(Hello World - --< + std::string expectedResult = R"(Hello World --< from Parser!)"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); } @@ -210,7 +392,7 @@ from Parser!)"; Template tpl; ASSERT_TRUE(tpl.Load(source)); - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; std::string expectedResult = R"(Hello World > --< @@ -227,10 +409,9 @@ from Parser!)"; Template tpl; ASSERT_TRUE(tpl.Load(source)); - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; - std::string expectedResult = R"(Hello World - -- + std::string expectedResult = R"(Hello World -- from Parser!)"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); } @@ -244,10 +425,9 @@ from Parser!)"; Template tpl; ASSERT_TRUE(tpl.Load(source)); - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; - std::string expectedResult = R"(Hello World - -- + std::string expectedResult = R"(Hello World -- from Parser!)"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); } @@ -266,7 +446,7 @@ from Parser!)"; return; } - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; std::string expectedResult = R"(Hello World --from Parser!)"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); @@ -286,7 +466,7 @@ from Parser!)"; return; } - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; std::string expectedResult = R"(Hello World -- from Parser!)"; @@ -306,7 +486,7 @@ TEST(BasicTests, TrimSpaces_3) return; } - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; std::string expectedResult = R"(Hello World --)"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); @@ -329,7 +509,7 @@ from Parser!)"; return; } - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; std::string expectedResult = R"(Hello World str1 str2 str3 str4 from Parser!)"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); @@ -349,7 +529,7 @@ from Parser!)"; return; } - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; std::string expectedResult = R"(Hello World > -- < from Parser!)"; @@ -370,7 +550,7 @@ from Parser!)"; return; } - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; std::string expectedResult = R"(Hello World > -- < from Parser!)"; @@ -392,7 +572,7 @@ from Parser!)"; return; } - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; std::string expectedResult = R"(Hello World > -- < from Parser!)"; @@ -412,9 +592,10 @@ TEST(BasicTests, TrimSpaces_8) return; } - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; - std::string expectedResult = R"(Hello World> -- < + std::string expectedResult = R"(Hello World +> -- < from Parser!)"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); } @@ -434,7 +615,7 @@ from Parser!)"; return; } - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; std::string expectedResult = R"(Hello World -> -- < from Parser!)"; @@ -456,10 +637,36 @@ from Parser!)"; return; } - std::string result = tpl.RenderAsString(ValuesMap{}); + std::string result = tpl.RenderAsString(ValuesMap{}).value(); std::cout << result << std::endl; std::string expectedResult = R"(Hello World -- > -- < from Parser!)"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); } + +TEST(BasicTests, EnvTestPreservesGlobalVar) +{ + jinja2::TemplateEnv tplEnv; + tplEnv.AddGlobal("global_var", jinja2::Value("foo")); + tplEnv.AddGlobal("global_fn", jinja2::MakeCallable([]() { + return "bar"; + })); + std::string result1; + { + jinja2::Template tpl(&tplEnv); + tpl.Load("Hello {{ global_var }} {{ global_fn() }}!!!"); + result1 = tpl.RenderAsString(jinja2::ValuesMap{}).value(); + } + std::string result2; + { + jinja2::Template tpl(&tplEnv); + tpl.Load("Hello {{ global_var }} {{ global_fn() }}!!!"); + result2 = tpl.RenderAsString(jinja2::ValuesMap{}).value(); + } + ASSERT_EQ(result1, result2); +} + +MULTISTR_TEST(BasicMultiStrTest, LiteralWithEscapeCharacters, R"({{ 'Hello\t\nWorld\n\twith\nescape\tcharacters!' }})", "Hello\t\nWorld\n\twith\nescape\tcharacters!") +{ +} diff --git a/test/binding/boost_json_binding_test.cpp b/test/binding/boost_json_binding_test.cpp new file mode 100644 index 00000000..f8ec4531 --- /dev/null +++ b/test/binding/boost_json_binding_test.cpp @@ -0,0 +1,177 @@ +#include +#include + +#include +#include + +#include "../test_tools.h" +#include + +using BoostJsonTest = BasicTemplateRenderer; + +MULTISTR_TEST(BoostJsonTest, BasicReflection, R"({{ json.message }})", R"(Hello World from Parser!)") +{ + boost::json::value values = { + {"message", "Hello World from Parser!"} + }; + + params["json"] = jinja2::Reflect(std::move(values)); +} + +MULTISTR_TEST(BoostJsonTest, BasicTypesReflection, R"( +{{ json.bool | pprint }} +{{ json.small_int | pprint }} +{{ json.big_int | pprint }} +{{ json.double | pprint }} +{{ json.string | pprint }} +)", +R"( +true +100500 +100500100500100 +100.5 +'Hello World!' +)") +{ + boost::json::value values = { + {"bool", true}, + {"small_int", 100500}, + {"big_int", 100500100500100LL}, + {"double", 100.5}, + {"string", "Hello World!"}, + }; + + params["json"] = jinja2::Reflect(std::move(values)); +} + +MULTISTR_TEST(BoostJsonTest, BasicValuesReflection, R"( +{{ bool_val | pprint }} +{{ small_int_val | pprint }} +{{ big_int_val | pprint }} +{{ double_val | pprint }} +{{ string_val | pprint }} +{{ array_val | pprint }} +{{ object_val | pprint }} +{{ empty_val | pprint }} +)", + R"( +true +100500 +100500100500100 +100.5 +'Hello World!' +[1, 2, 3, 4] +{'message': 'Hello World from Parser!', 'message2': 'Hello World from Parser-123!'} +none +)") +{ + boost::json::value values = { + {"bool", true}, + {"small_int", 100500}, + {"big_int", 100500100500100LL}, + {"double", 100.5}, + {"string", "Hello World!"}, + {"array", {1, 2, 3, 4}}, + {"object", { + {"message", "Hello World from Parser!"}, + {"message2", "Hello World from Parser-123!"} + }}}; + + auto boolVal = values.at("bool"); + auto smallIntVal = values.at("small_int"); + auto bigIntVal = values.at("big_int"); + auto doubleVal = values.at("double"); + auto stringVal = values.at("string"); + auto arrayVal = values.at("array"); + auto objectVal = values.at("object"); + boost::json::value emptyVal; + + params["bool_val"] = jinja2::Reflect(std::move(boolVal)); + params["small_int_val"] = jinja2::Reflect(std::move(smallIntVal)); + params["big_int_val"] = jinja2::Reflect(std::move(bigIntVal)); + params["double_val"] = jinja2::Reflect(std::move(doubleVal)); + params["string_val"] = jinja2::Reflect(std::move(stringVal)); + params["array_val"] = jinja2::Reflect(std::move(arrayVal)); + params["object_val"] = jinja2::Reflect(std::move(objectVal)); + params["empty_val"] = jinja2::Reflect(std::move(emptyVal)); +} + +MULTISTR_TEST(BoostJsonTest, SubobjectReflection, +R"( +{{ json.object.message }} +{{ json.object.message3 }} +{{ json.object | list | join(', ') }} +)", +R"( +Hello World from Parser! + +message, message2 +)") +{ + boost::json::value values = { + {"object", { + {"message", "Hello World from Parser!"}, + {"message2", "Hello World from Parser-123!"} + }} + }; + + params["json"] = jinja2::Reflect(std::move(values)); +} + +MULTISTR_TEST(BoostJsonTest, ArrayReflection, +R"( +{{ json.array | sort | pprint }} +{{ json.array | length }} +{{ json.array | first }} +{{ json.array | last }} +{{ json.array[8] }}-{{ json.array[6] }}-{{ json.array[4] }} +)", +R"( +[1, 2, 3, 4, 5, 6, 7, 8, 9] +9 +9 +1 +1-3-5 +)") +{ + boost::json::value values = { + {"array", {9, 8, 7, 6, 5, 4, 3, 2, 1}} + }; + + params["json"] = jinja2::Reflect(std::move(values)); +} + +MULTISTR_TEST(BoostJsonTest, ParsedTypesReflection, R"( +{{ json.bool | pprint }} +{{ json.small_int | pprint }} +{{ json.big_int | pprint }} +{{ json.double | pprint }} +{{ json.string | pprint }} +{{ json.object.message | pprint }} +{{ json.array | sort | pprint }} +)", + R"( +true +100500 +100500100500100 +100.5 +'Hello World!' +'Hello World from Parser!' +[1, 2, 3, 4, 5, 6, 7, 8, 9] +)") +{ + boost::json::value values = boost::json::parse(R"( +{ + "big_int": 100500100500100, + "bool": true, + "double": 100.5, + "small_int": 100500, + "string": "Hello World!", + "object": {"message": "Hello World from Parser!"}, + "array": [9, 8, 7, 6, 5, 4, 3, 2, 1] +} +)");; + + params["json"] = jinja2::Reflect(std::move(values)); +} + diff --git a/test/binding/nlohmann_json_binding_test.cpp b/test/binding/nlohmann_json_binding_test.cpp new file mode 100644 index 00000000..7942b65e --- /dev/null +++ b/test/binding/nlohmann_json_binding_test.cpp @@ -0,0 +1,178 @@ +#include +#include + +#include "../test_tools.h" +#include + +#include +#include + + +using NlohmannJsonTest = BasicTemplateRenderer; + +MULTISTR_TEST(NlohmannJsonTest, BasicReflection, R"({{ json.message }})", R"(Hello World from Parser!)") +{ + nlohmann::json values = { + {"message", "Hello World from Parser!"} + }; + + params["json"] = jinja2::Reflect(std::move(values)); +} + +MULTISTR_TEST(NlohmannJsonTest, BasicTypesReflection, R"( +{{ json.bool | pprint }} +{{ json.small_int | pprint }} +{{ json.big_int | pprint }} +{{ json.double | pprint }} +{{ json.string | pprint }} +)", +R"( +true +100500 +100500100500100 +100.5 +'Hello World!' +)") +{ + nlohmann::json values = { + {"bool", true}, + {"small_int", 100500}, + {"big_int", 100500100500100LL}, + {"double", 100.5}, + {"string", "Hello World!"}, + }; + + params["json"] = jinja2::Reflect(std::move(values)); +} + +MULTISTR_TEST(NlohmannJsonTest, BasicValuesReflection, R"( +{{ bool_val | pprint }} +{{ small_int_val | pprint }} +{{ big_int_val | pprint }} +{{ double_val | pprint }} +{{ string_val | pprint }} +{{ array_val | pprint }} +{{ object_val | pprint }} +{{ empty_val | pprint }} +)", + R"( +true +100500 +100500100500100 +100.5 +'Hello World!' +[1, 2, 3, 4] +{'message': 'Hello World from Parser!', 'message2': 'Hello World from Parser-123!'} +none +)") +{ + nlohmann::json values = { + {"bool", true}, + {"small_int", 100500}, + {"big_int", 100500100500100LL}, + {"double", 100.5}, + {"string", "Hello World!"}, + {"array", {1, 2, 3, 4}}, + {"object", { + {"message", "Hello World from Parser!"}, + {"message2", "Hello World from Parser-123!"} + }}}; + + auto boolVal = values["bool"]; + auto smallIntVal = values["small_int"]; + auto bigIntVal = values["big_int"]; + auto doubleVal = values["double"]; + auto stringVal = values["string"]; + auto arrayVal = values["array"]; + auto objectVal = values["object"]; + nlohmann::json emptyVal; + + params["bool_val"] = jinja2::Reflect(std::move(boolVal)); + params["small_int_val"] = jinja2::Reflect(std::move(smallIntVal)); + params["big_int_val"] = jinja2::Reflect(std::move(bigIntVal)); + params["double_val"] = jinja2::Reflect(std::move(doubleVal)); + params["string_val"] = jinja2::Reflect(std::move(stringVal)); + params["array_val"] = jinja2::Reflect(std::move(arrayVal)); + params["object_val"] = jinja2::Reflect(std::move(objectVal)); + params["empty_val"] = jinja2::Reflect(std::move(emptyVal)); +} + +MULTISTR_TEST(NlohmannJsonTest, SubobjectReflection, +R"( +{{ json.object.message }} +{{ json.object.message3 }} +{{ json.object | list | join(', ') }} +)", +R"( +Hello World from Parser! + +message, message2 +)") +{ + nlohmann::json values = { + {"object", { + {"message", "Hello World from Parser!"}, + {"message2", "Hello World from Parser-123!"} + }} + }; + + params["json"] = jinja2::Reflect(std::move(values)); +} + +MULTISTR_TEST(NlohmannJsonTest, ArrayReflection, +R"( +{{ json.array | sort | pprint }} +{{ json.array | length }} +{{ json.array | first }} +{{ json.array | last }} +{{ json.array[8] }}-{{ json.array[6] }}-{{ json.array[4] }} +)", +R"( +[1, 2, 3, 4, 5, 6, 7, 8, 9] +9 +9 +1 +1-3-5 +)") +{ + nlohmann::json values = { + {"array", {9, 8, 7, 6, 5, 4, 3, 2, 1}} + }; + + params["json"] = jinja2::Reflect(std::move(values)); +} + +MULTISTR_TEST(NlohmannJsonTest, ParsedTypesReflection, R"( +{{ json.bool | pprint }} +{{ json.small_int | pprint }} +{{ json.big_int | pprint }} +{{ json.double | pprint }} +{{ json.string | pprint }} +{{ json.object.message | pprint }} +{{ json.array | sort | pprint }} +)", + R"( +true +100500 +100500100500100 +100.5 +'Hello World!' +'Hello World from Parser!' +[1, 2, 3, 4, 5, 6, 7, 8, 9] +)") +{ + nlohmann::json values = nlohmann::json::parse(R"( +{ + "big_int": 100500100500100, + "bool": true, + "double": 100.5, + "small_int": 100500, + "string": "Hello World!", + "object": {"message": "Hello World from Parser!"}, + "array": [9, 8, 7, 6, 5, 4, 3, 2, 1] +} +)");; + + params["json"] = jinja2::Reflect(std::move(values)); +} + diff --git a/test/binding/rapid_json_binding_test.cpp b/test/binding/rapid_json_binding_test.cpp new file mode 100644 index 00000000..d86f2f2e --- /dev/null +++ b/test/binding/rapid_json_binding_test.cpp @@ -0,0 +1,138 @@ +#include +#include + +#include "../test_tools.h" +#include + +#include +#include + + +class RapidJsonTest : public BasicTemplateRenderer +{ +public: + const auto& GetJson() const {return m_json;} + const auto& GetEmptyVal() const {return m_emptyVal;} +protected: + void SetUp() override + { + const char *json = R"( +{ + "message": "Hello World from Parser!", + "big_int": 100500100500100, + "bool_true": true, + "bool_false": false, + "double": 100.5, + "small_int": 100500, + "string": "Hello World!", + "object": {"message": "Hello World from Parser!", "message2": "Hello World from Parser-123!"}, + "array": [9, 8, 7, 6, 5, 4, 3, 2, 1] +} +)"; + m_json.Parse(json); + } +protected: + rapidjson::Document m_json; + rapidjson::Value m_emptyVal; +}; + +MULTISTR_TEST(RapidJsonTest, BasicReflection, R"({{ json.message }})", R"(Hello World from Parser!)") +{ + params["json"] = jinja2::Reflect(test.GetJson()); +} + +MULTISTR_TEST(RapidJsonTest, BasicTypesReflection, R"( +{{ json.bool_true | pprint }} +{{ json.bool_false | pprint }} +{{ json.small_int | pprint }} +{{ json.big_int | pprint }} +{{ json.double | pprint }} +{{ json.string | pprint }} +)", +R"( +true +false +100500 +100500100500100 +100.5 +'Hello World!' +)") +{ + params["json"] = jinja2::Reflect(test.GetJson()); +} + +MULTISTR_TEST(RapidJsonTest, BasicValuesReflection, R"( +{{ bool_val | pprint }} +{{ small_int_val | pprint }} +{{ big_int_val | pprint }} +{{ double_val | pprint }} +{{ string_val | pprint }} +{{ array_val | pprint }} +{{ object_val | pprint }} +{{ empty_val | pprint }} +)", + R"( +true +100500 +100500100500100 +100.5 +'Hello World!' +[9, 8, 7, 6, 5, 4, 3, 2, 1] +{'message': 'Hello World from Parser!', 'message2': 'Hello World from Parser-123!'} +none +)") +{ + auto& values = test.GetJson(); + + auto& boolVal = values["bool_true"]; + auto& smallIntVal = values["small_int"]; + auto& bigIntVal = values["big_int"]; + auto& doubleVal = values["double"]; + auto& stringVal = values["string"]; + auto& arrayVal = values["array"]; + auto& objectVal = values["object"]; + auto& emptyVal = test.GetEmptyVal(); + + params["bool_val"] = jinja2::Reflect(boolVal); + params["small_int_val"] = jinja2::Reflect(smallIntVal); + params["big_int_val"] = jinja2::Reflect(bigIntVal); + params["double_val"] = jinja2::Reflect(doubleVal); + params["string_val"] = jinja2::Reflect(stringVal); + params["array_val"] = jinja2::Reflect(arrayVal); + params["object_val"] = jinja2::Reflect(objectVal); + params["empty_val"] = jinja2::Reflect(emptyVal); +} + +MULTISTR_TEST(RapidJsonTest, SubobjectReflection, +R"( +{{ json.object.message }} +{{ json.object.message3 }} +{{ json.object | list | join(', ') }} +)", +R"( +Hello World from Parser! + +message, message2 +)") +{ + params["json"] = jinja2::Reflect(test.GetJson()); +} + +MULTISTR_TEST(RapidJsonTest, ArrayReflection, +R"( +{{ json.array | sort | pprint }} +{{ json.array | length }} +{{ json.array | first }} +{{ json.array | last }} +{{ json.array[8] }}-{{ json.array[6] }}-{{ json.array[4] }} +)", +R"( +[1, 2, 3, 4, 5, 6, 7, 8, 9] +9 +9 +1 +1-3-5 +)") +{ + params["json"] = jinja2::Reflect(test.GetJson()); +} diff --git a/test/binding/rapid_json_serializer_test.cpp b/test/binding/rapid_json_serializer_test.cpp new file mode 100644 index 00000000..3cc0fb94 --- /dev/null +++ b/test/binding/rapid_json_serializer_test.cpp @@ -0,0 +1,71 @@ +#include + +#ifndef JINJA2CPP_SHARED_LIB + +#include "../../src/binding/rapid_json_serializer.h" +#include + +namespace +{ +template +jinja2::InternalValue MakeInternalValue(T&& v) +{ + return jinja2::InternalValue(std::forward(v)); +} +} // namespace +TEST(RapidJsonSerializerTest, SerializeTrivialTypes) +{ + const jinja2::rapidjson_serializer::DocumentWrapper document; + + const auto stringValue = document.CreateValue(MakeInternalValue("string")); + const auto intValue = document.CreateValue(MakeInternalValue(123)); + const auto doubleValue = document.CreateValue(MakeInternalValue(12.34)); + + EXPECT_EQ("\"string\"", stringValue.AsString()); + EXPECT_EQ("123", intValue.AsString()); + EXPECT_EQ("12.34", doubleValue.AsString()); +} + +TEST(RapidJsonSerializerTest, SerializeComplexTypes) +{ + jinja2::rapidjson_serializer::DocumentWrapper document; + { + jinja2::InternalValueMap params = { { "string", MakeInternalValue("hello") } }; + const auto jsonValue = document.CreateValue(CreateMapAdapter(std::move(params))); + EXPECT_EQ("{\"string\":\"hello\"}", jsonValue.AsString()); + } + + { + jinja2::InternalValueMap params = { { "int", MakeInternalValue(123) } }; + const auto jsonValue = document.CreateValue(CreateMapAdapter(std::move(params))); + EXPECT_EQ("{\"int\":123}", jsonValue.AsString()); + } + + { + jinja2::InternalValueList array{ MakeInternalValue(1), MakeInternalValue(2), MakeInternalValue(3) }; + jinja2::InternalValueMap map{ { "array", jinja2::ListAdapter::CreateAdapter(std::move(array)) } }; + jinja2::InternalValueMap params = { { "map", CreateMapAdapter(std::move(map)) } }; + const auto jsonValue = document.CreateValue(CreateMapAdapter(std::move(params))); + EXPECT_EQ("{\"map\":{\"array\":[1,2,3]}}", jsonValue.AsString()); + } +} + +TEST(RapidJsonSerializerTest, SerializeComplexTypesWithIndention) +{ + const jinja2::rapidjson_serializer::DocumentWrapper document; + + jinja2::InternalValueList array{ MakeInternalValue(1), MakeInternalValue(2), MakeInternalValue(3) }; + jinja2::InternalValueMap map{ { "array", jinja2::ListAdapter::CreateAdapter(std::move(array)) } }; + jinja2::InternalValueMap params = { { "map", CreateMapAdapter(std::move(map)) } }; + const auto jsonValue = document.CreateValue(CreateMapAdapter(std::move(params))); + + auto indentedDocument = + R"({ + "map": { + "array": [1, 2, 3] + } +})"; + + EXPECT_EQ(indentedDocument, jsonValue.AsString(4)); +} +#endif diff --git a/test/errors_test.cpp b/test/errors_test.cpp index e4ba7560..bcb137b1 100644 --- a/test/errors_test.cpp +++ b/test/errors_test.cpp @@ -7,27 +7,251 @@ using namespace jinja2; struct ErrorsGenericTestTag; +struct ErrorsGenericExtensionTestTag; using ErrorsGenericTest = InputOutputPairTest; +using ErrorsGenericExtensionsTest = InputOutputPairTest; +TEST_F(TemplateEnvFixture, EnvironmentAbsentErrorsTest) +{ + Template tpl1; + auto parseResult = tpl1.Load("{% extends 'module' %}"); + ASSERT_FALSE(parseResult.has_value()); + + EXPECT_EQ("noname.j2tpl:1:4: error: Template environment doesn't set\n{% extends 'module' %}\n---^-------", ErrorToString(parseResult.error())); + + parseResult = tpl1.Load("{% include 'module' %}"); + ASSERT_FALSE(parseResult.has_value()); + + EXPECT_EQ("noname.j2tpl:1:4: error: Template environment doesn't set\n{% include 'module' %}\n---^-------", ErrorToString(parseResult.error())); + + parseResult = tpl1.Load("{% from 'module' %}"); + ASSERT_FALSE(parseResult.has_value()); + + EXPECT_EQ("noname.j2tpl:1:4: error: Template environment doesn't set\n{% from 'module' %}\n---^-------", ErrorToString(parseResult.error())); + + parseResult = tpl1.Load("{% import 'module' %}"); + ASSERT_FALSE(parseResult.has_value()); + + EXPECT_EQ("noname.j2tpl:1:4: error: Template environment doesn't set\n{% import 'module' %}\n---^-------", ErrorToString(parseResult.error())); +} + +TEST_F(TemplateEnvFixture, EnvironmentAbsentErrorsTest_Wide) +{ + TemplateW tpl1; + auto parseResult = tpl1.Load(L"{% extends 'module' %}"); + ASSERT_FALSE(parseResult.has_value()); + + EXPECT_EQ(L"noname.j2tpl:1:4: error: Template environment doesn't set\n{% extends 'module' %}\n---^-------", ErrorToString(parseResult.error())); + + parseResult = tpl1.Load(L"{% include 'module' %}"); + ASSERT_FALSE(parseResult.has_value()); + + EXPECT_EQ(L"noname.j2tpl:1:4: error: Template environment doesn't set\n{% include 'module' %}\n---^-------", ErrorToString(parseResult.error())); + + parseResult = tpl1.Load(L"{% from 'module' %}"); + ASSERT_FALSE(parseResult.has_value()); + + EXPECT_EQ(L"noname.j2tpl:1:4: error: Template environment doesn't set\n{% from 'module' %}\n---^-------", ErrorToString(parseResult.error())); + + parseResult = tpl1.Load(L"{% import 'module' %}"); + ASSERT_FALSE(parseResult.has_value()); + + EXPECT_EQ(L"noname.j2tpl:1:4: error: Template environment doesn't set\n{% import 'module' %}\n---^-------", ErrorToString(parseResult.error())); +} + +TEST_F(TemplateEnvFixture, RenderErrorsTest) +{ + Template tpl1; + auto renderResult = tpl1.RenderAsString({}); + ASSERT_FALSE(renderResult.has_value()); + + EXPECT_EQ(":1:1: error: Template not parsed\n", ErrorToString(renderResult.error())); + + Template tpl2; + tpl2.Load(R"({{ foo() }})"); + renderResult = tpl2.RenderAsString({{"foo", MakeCallable([]() -> Value {throw std::runtime_error("Bang!"); })}}); + ASSERT_FALSE(renderResult.has_value()); + + EXPECT_EQ("noname.j2tpl:1:1: error: Unexpected exception occurred during template processing. Exception: Bang!\n", ErrorToString(renderResult.error())); + + Template tpl3(&m_env); + auto parseResult = tpl3.Load("{% import name as name %}"); + EXPECT_TRUE(parseResult.has_value()); + if (!parseResult) + std::cout << parseResult.error() << std::endl; + renderResult = tpl3.RenderAsString({{"name", 10}}); + ASSERT_FALSE(renderResult.has_value()); + + EXPECT_EQ("noname.j2tpl:1:1: error: Invalid template name: 10\n", ErrorToString(renderResult.error())); +} + +TEST_F(TemplateEnvFixture, RenderErrorsTest_Wide) +{ + TemplateW tpl1; + auto renderResult = tpl1.RenderAsString({}); + ASSERT_FALSE(renderResult.has_value()); + + EXPECT_EQ(L":1:1: error: Template not parsed\n", ErrorToString(renderResult.error())); + + TemplateW tpl2; + tpl2.Load(LR"({{ foo() }})"); + renderResult = tpl2.RenderAsString({ {"foo", MakeCallable([]() -> Value {throw std::runtime_error("Bang!"); })} }); + ASSERT_FALSE(renderResult.has_value()); + + EXPECT_EQ(L"noname.j2tpl:1:1: error: Unexpected exception occurred during template processing. Exception: Bang!\n", ErrorToString(renderResult.error())); + + TemplateW tpl3(&m_env); + auto parseResult = tpl3.Load(L"{% import name as name %}"); + EXPECT_TRUE(parseResult.has_value()); + if (!parseResult) + std::wcout << parseResult.error() << std::endl; + renderResult = tpl3.RenderAsString({ {"name", 10} }); + ASSERT_FALSE(renderResult.has_value()); + + EXPECT_EQ(L"noname.j2tpl:1:1: error: Invalid template name: 10\n", ErrorToString(renderResult.error())); +} + +TEST_F(TemplateEnvFixture, ErrorPropagationTest) +{ + AddFile("module", "{% for %}"); + Template tpl1(&m_env); + auto parseResult = tpl1.Load("{% extends 'module' %}"); + ASSERT_TRUE(parseResult.has_value()); + auto renderResult = tpl1.RenderAsString({}); + ASSERT_FALSE(renderResult.has_value()); + + EXPECT_EQ("module:1:8: error: Identifier expected\n{% for %}\n ---^-------", ErrorToString(renderResult.error())); + + Template tpl2(&m_env); + parseResult = tpl2.Load("{% include 'module' %}"); + ASSERT_TRUE(parseResult.has_value()); + renderResult = tpl2.RenderAsString({}); + ASSERT_FALSE(renderResult.has_value()); + + EXPECT_EQ("module:1:8: error: Identifier expected\n{% for %}\n ---^-------", ErrorToString(renderResult.error())); + + Template tpl3(&m_env); + parseResult = tpl3.Load("{% from 'module' import name %}"); + ASSERT_TRUE(parseResult.has_value()); + renderResult = tpl3.RenderAsString({}); + ASSERT_FALSE(renderResult.has_value()); + + EXPECT_EQ("module:1:8: error: Identifier expected\n{% for %}\n ---^-------", ErrorToString(renderResult.error())); + + Template tpl4(&m_env); + parseResult = tpl4.Load("{% import 'module' as module %}"); + ASSERT_TRUE(parseResult.has_value()); + renderResult = tpl4.RenderAsString({}); + ASSERT_FALSE(renderResult.has_value()); + + EXPECT_EQ("module:1:8: error: Identifier expected\n{% for %}\n ---^-------", ErrorToString(renderResult.error())); +} + +TEST_F(TemplateEnvFixture, ErrorPropagationTest_Wide) +{ + AddFile("module", "{% for %}"); + TemplateW tpl1(&m_env); + auto parseResult = tpl1.Load(L"{% extends 'module' %}"); + ASSERT_TRUE(parseResult.has_value()); + auto renderResult = tpl1.RenderAsString({}); + ASSERT_FALSE(renderResult.has_value()); + + EXPECT_EQ(L"module:1:8: error: Identifier expected\n{% for %}\n ---^-------", ErrorToString(renderResult.error())); + + TemplateW tpl2(&m_env); + parseResult = tpl2.Load(L"{% include 'module' %}"); + ASSERT_TRUE(parseResult.has_value()); + renderResult = tpl2.RenderAsString({}); + ASSERT_FALSE(renderResult.has_value()); + + EXPECT_EQ(L"module:1:8: error: Identifier expected\n{% for %}\n ---^-------", ErrorToString(renderResult.error())); + + TemplateW tpl3(&m_env); + parseResult = tpl3.Load(L"{% from 'module' import name %}"); + ASSERT_TRUE(parseResult.has_value()); + renderResult = tpl3.RenderAsString({}); + ASSERT_FALSE(renderResult.has_value()); + + EXPECT_EQ(L"module:1:8: error: Identifier expected\n{% for %}\n ---^-------", ErrorToString(renderResult.error())); + + TemplateW tpl4(&m_env); + parseResult = tpl4.Load(L"{% import 'module' as module %}"); + ASSERT_TRUE(parseResult.has_value()); + renderResult = tpl4.RenderAsString({}); + ASSERT_FALSE(renderResult.has_value()); + + EXPECT_EQ(L"module:1:8: error: Identifier expected\n{% for %}\n ---^-------", ErrorToString(renderResult.error())); +} TEST_P(ErrorsGenericTest, Test) { auto& testParam = GetParam(); std::string source = testParam.tpl; - Template tpl; + TemplateEnv env; + Template tpl(&env); + auto parseResult = tpl.Load(source); + ASSERT_FALSE(parseResult.has_value()); + + auto result = ErrorToString(parseResult.error()); + std::cout << result << std::endl; + std::string expectedResult = testParam.result; + EXPECT_EQ(expectedResult, result); +} + +TEST_P(ErrorsGenericTest, Test_Wide) +{ + auto& testParam = GetParam(); + std::string source = testParam.tpl; + + TemplateEnv env; + TemplateW tpl(&env); + auto parseResult = tpl.Load(jinja2::ConvertString(source)); + ASSERT_FALSE(parseResult.has_value()); + + auto result = ErrorToString(parseResult.error()); + std::wcout << result << std::endl; + std::wstring expectedResult = jinja2::ConvertString(testParam.result); + EXPECT_EQ(expectedResult, result); +} + +TEST_P(ErrorsGenericExtensionsTest, Test) +{ + auto& testParam = GetParam(); + std::string source = testParam.tpl; + + TemplateEnv env; + env.GetSettings().extensions.Do = true; + + Template tpl(&env); auto parseResult = tpl.Load(source); - EXPECT_FALSE(parseResult.has_value()); + ASSERT_FALSE(parseResult.has_value()); - std::ostringstream errorDescr; - errorDescr << parseResult.error(); - std::string result = errorDescr.str(); + auto result = ErrorToString(parseResult.error()); std::cout << result << std::endl; std::string expectedResult = testParam.result; EXPECT_EQ(expectedResult, result); } -INSTANTIATE_TEST_CASE_P(BasicTest, ErrorsGenericTest, ::testing::Values( +TEST_P(ErrorsGenericExtensionsTest, Test_Wide) +{ + auto& testParam = GetParam(); + std::string source = testParam.tpl; + + TemplateEnv env; + env.GetSettings().extensions.Do = true; + + TemplateW tpl(&env); + auto parseResult = tpl.Load(jinja2::ConvertString(source)); + ASSERT_FALSE(parseResult.has_value()); + + auto result = ErrorToString(parseResult.error()); + std::wcout << result << std::endl; + std::wstring expectedResult = jinja2::ConvertString(testParam.result); + EXPECT_EQ(expectedResult, result); +} + +INSTANTIATE_TEST_SUITE_P(BasicTest, ErrorsGenericTest, ::testing::Values( InputOutputPair{"{{}}", "noname.j2tpl:1:3: error: Unexpected token: '<>'\n{{}}\n--^-------"}, InputOutputPair{"{{ ) }}", @@ -48,7 +272,7 @@ INSTANTIATE_TEST_CASE_P(BasicTest, ErrorsGenericTest, ::testing::Values( "noname.j2tpl:1:1: error: Unexpected expression block end\n}}\n^-------"} )); -INSTANTIATE_TEST_CASE_P(BasicExpressionsTest, ErrorsGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(BasicExpressionsTest, ErrorsGenericTest, ::testing::Values( InputOutputPair{"{{ * or }}", "noname.j2tpl:1:4: error: Unexpected token: '*'\n{{ * or }}\n---^-------"}, InputOutputPair{"{{ 1 + }}", @@ -119,7 +343,7 @@ INSTANTIATE_TEST_CASE_P(BasicExpressionsTest, ErrorsGenericTest, ::testing::Valu "noname.j2tpl:1:3: error: Unexpected token: '<>'\n{{}}\n--^-------"} )); -INSTANTIATE_TEST_CASE_P(StatementsTest, ErrorsGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(StatementsTest_1, ErrorsGenericTest, ::testing::Values( InputOutputPair{"{% if %}", "noname.j2tpl:1:7: error: Expected expression, got: '<>'\n{% if %}\n ---^-------"}, InputOutputPair{"{% endif %}", @@ -141,9 +365,9 @@ INSTANTIATE_TEST_CASE_P(StatementsTest, ErrorsGenericTest, ::testing::Values( InputOutputPair{"{% for i in range(10,)%}", "noname.j2tpl:1:22: error: Unexpected token: ')'\n{% for i in range(10,)%}\n ---^-------"}, InputOutputPair{"{% for i in range(10) rec%}", - "noname.j2tpl:1:23: error: Unexpected token 'rec'. Expected: 'recursive', 'if'\n{% for i in range(10) rec%}\n ---^-------"}, + "noname.j2tpl:1:23: error: Unexpected token 'rec'. Expected: 'if', 'recursive', '<>'\n{% for i in range(10) rec%}\n ---^-------"}, InputOutputPair{"{% for i in range(10) endfor%}", - "noname.j2tpl:1:23: error: Unexpected token 'endfor'. Expected: 'if', '<>'\n{% for i in range(10) endfor%}\n ---^-------"}, + "noname.j2tpl:1:23: error: Unexpected token 'endfor'. Expected: 'if', 'recursive', '<>'\n{% for i in range(10) endfor%}\n ---^-------"}, InputOutputPair{"{% for i in range(10) if {key} %}", "noname.j2tpl:1:27: error: String expected\n{% for i in range(10) if {key} %}\n ---^-------"}, InputOutputPair{"{% for i in range(10) if true else hello %}", @@ -156,18 +380,46 @@ INSTANTIATE_TEST_CASE_P(StatementsTest, ErrorsGenericTest, ::testing::Values( "noname.j2tpl:2:4: error: Unexpected statement: 'endfor'\n{% endfor %}\n---^-------"}, InputOutputPair{"{% set %}", "noname.j2tpl:1:8: error: Identifier expected\n{% set %}\n ---^-------"}, - InputOutputPair{"{% set id%}", - "noname.j2tpl:1:10: error: This feature has not been supported yet\n{% set id%}\n ---^-------"}, InputOutputPair{"{% set 10%}", "noname.j2tpl:1:8: error: Identifier expected\n{% set 10%}\n ---^-------"}, InputOutputPair{"{% set i = {key] %}", "noname.j2tpl:1:13: error: String expected\n{% set i = {key] %}\n ---^-------"}, InputOutputPair{"{% set id=10%}\n{% endset %}", - "noname.j2tpl:2:4: error: This feature has not been supported yet\n{% endset %}\n---^-------"}, + "noname.j2tpl:2:4: error: Unexpected statement: 'endset'\n{% endset %}\n---^-------"}, InputOutputPair{"{% extends %}", "noname.j2tpl:1:12: error: Unexpected token '<>'. Expected: '<>', '<>'\n{% extends %}\n ---^-------"}, InputOutputPair{"{% extends 10 %}", "noname.j2tpl:1:12: error: Unexpected token '10'. Expected: '<>', '<>'\n{% extends 10 %}\n ---^-------"}, + InputOutputPair{R"({% extends "/_layouts/default.html" } + {% block content %} + {% endblock %} + )", + "noname.j2tpl:1:37: error: Expected end of statement, got: '}'\n{% extends \"/_layouts/default.html\" }\n ---^-------"}, + InputOutputPair{"{% import %}", + "noname.j2tpl:1:11: error: Unexpected token: '<>'\n{% import %}\n ---^-------"}, + InputOutputPair{"{% import 'foo' %}", + "noname.j2tpl:1:17: error: Unexpected token '<>'. Expected: 'as'\n{% import 'foo' %}\n ---^-------"}, + InputOutputPair{"{% import 'foo' as %}", + "noname.j2tpl:1:20: error: Unexpected token '<>'. Expected: '<>'\n{% import 'foo' as %}\n ---^-------"}, + InputOutputPair{"{% import 'foo', as %}", + "noname.j2tpl:1:16: error: Unexpected token ','. Expected: 'as'\n{% import 'foo', as %}\n ---^-------"}, + InputOutputPair{"{% import 'foo', %}", + "noname.j2tpl:1:16: error: Unexpected token ','. Expected: 'as'\n{% import 'foo', %}\n ---^-------"}, + InputOutputPair{"{% import 'foo', bar %}", + "noname.j2tpl:1:16: error: Unexpected token ','. Expected: 'as'\n{% import 'foo', bar %}\n ---^-------"}, + InputOutputPair{"{% from 'foo' import, %}", + "noname.j2tpl:1:21: error: Unexpected token ','. Expected: '<>'\n{% from 'foo' import, %}\n ---^-------"}, + InputOutputPair{"{% from 'foo' import %}", + "noname.j2tpl:1:22: error: Unexpected token '<>'. Expected: '<>'\n{% from 'foo' import %}\n ---^-------"}, + InputOutputPair{"{% from 'foo' import bar, %}", + "noname.j2tpl:1:27: error: Unexpected token '<>'. Expected: '<>'\n{% from 'foo' import bar, %}\n ---^-------"}, + InputOutputPair{"{% from 'foo' import bar,, with context %}", + "noname.j2tpl:1:26: error: Unexpected token ','. Expected: '<>'\n{% from 'foo' import bar,, with context %}\n ---^-------"}, + 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_SUITE_P(StatementsTest_2, ErrorsGenericTest, ::testing::Values( InputOutputPair{"{% block %}", "noname.j2tpl:1:10: error: Identifier expected\n{% block %}\n ---^-------"}, InputOutputPair{"{% block 10 %}", @@ -212,6 +464,39 @@ INSTANTIATE_TEST_CASE_P(StatementsTest, ErrorsGenericTest, ::testing::Values( "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{"{% 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{"{% if a == 42 %}{% endwith %}", + "noname.j2tpl:1:20: error: Unexpected statement: 'endwith'\n{% if a == 42 %}{% endwith %}\n ---^-------"}, InputOutputPair{"{{}}", - "noname.j2tpl:1:3: error: Unexpected token: '<>'\n{{}}\n--^-------"} + "noname.j2tpl:1:3: error: Unexpected token: '<>'\n{{}}\n--^-------"}, + InputOutputPair{"{% raw %}{% raw %}{{ x }{% endraw %}{% endraw %}", + "noname.j2tpl:1:37: error: Unexpected raw block end\n{% raw %}{% raw %}{{ x }{% endraw %}{% endraw %}\n ---^-------"}, + InputOutputPair{"{% raw %}", + "noname.j2tpl:1:10: error: Expected end of raw block\n{% raw %}\n ---^-------"}, + InputOutputPair{"{{ 2 + 3 + {% raw %} }}", "noname.j2tpl:1:12: error: Unexpected raw block begin\n{{ 2 + 3 + {% raw %}\n ---^-------" }, + InputOutputPair{ "{% meta %}", "noname.j2tpl:1:11: error: Expected end of meta block\n{% meta %}\n ---^-------" }, + InputOutputPair{ "{% endmeta %}", "noname.j2tpl:1:1: error: Unexpected meta block end\n{% endmeta %}\n^-------" })); + +INSTANTIATE_TEST_SUITE_P(ExtensionStatementsTest, ErrorsGenericExtensionsTest, ::testing::Values( + InputOutputPair{"{% do %}", + "noname.j2tpl:1:7: error: Unexpected token: '<>'\n{% do %}\n ---^-------"}, + InputOutputPair{"{% do 1 + %}", + "noname.j2tpl:1:11: error: Unexpected token: '<>'\n{% do 1 + %}\n ---^-------"} )); diff --git a/test/expressions_test.cpp b/test/expressions_test.cpp index 70dc6d54..462f50bd 100644 --- a/test/expressions_test.cpp +++ b/test/expressions_test.cpp @@ -8,9 +8,10 @@ using namespace jinja2; -TEST(ExpressionsTest, BinaryMathOperations) -{ - std::string source = R"( +using ExpressionsMultiStrTest = BasicTemplateRenderer; + +MULTISTR_TEST(ExpressionsMultiStrTest, BinaryMathOperations, +R"( {{ 1 + 10 }} {{ 1 - 10}} {{ 0.1 + 1 }} @@ -19,73 +20,156 @@ TEST(ExpressionsTest, BinaryMathOperations) {{ 7 / 3}} {{ 7 // 3 }} {{ 7 % intValue }} +{{ 11 % 7 }} {{ 3 ** 4 }} {{ 10 ** -2 }} {{ 10/10 + 2*5 }} +{{ ([1, 2] + [3, 4]) | pprint }} {{ 'Hello' + " " + 'World ' + stringValue }} -{{ 'Hello' ~ " " ~ 123 ~ ' ' ~ 1.234 ~ " " ~ true ~ " " ~ intValue ~ " " ~ false ~ ' ' ~ 'World ' ~ stringValue }} -)"; - - Template tpl; - ASSERT_TRUE(tpl.Load(source)); - - ValuesMap params = { - {"intValue", 3}, - {"doubleValue", 12.123f}, - {"stringValue", "rain"}, - {"boolFalseValue", false}, - {"boolTrueValue", true}, - }; - - std::string result = tpl.RenderAsString(params); - std::cout << result << std::endl; - std::string expectedResult = R"( +{{ 'Hello' + " " + 'World ' + wstringValue }} +{{ stringValue + ' ' + wstringValue }} +{{ wstringValue + ' ' + stringValue }} +{{ wstringValue + ' ' + wstringValue }} +{{ stringValue + ' ' + stringValue }} +{{ 'Hello' ~ " " ~ 123 ~ ' ' ~ 1.234 ~ " " ~ true ~ " " ~ intValue ~ " " ~ false ~ ' ' ~ 'World ' ~ stringValue ~ ' ' ~ wstringValue}} +{{ 'abc' * 0 }} +{{ 'abc' * 1 }} +{{ '123' * intValue }} +{{ ([1, 2, 3] * intValue) | pprint }} +{{ stringValue * intValue }} +{{ wstringValue * intValue }} +)", +//----------- +R"( 11 -9 1.1 -10.4 10 -2.33333 +2.3333333 2 1 +4 81 0.01 11 +[1, 2, 3, 4] Hello World rain -Hello 123 1.234 true 3 false World rain -)"; +Hello World rain +rain rain +rain rain +rain rain +rain rain +Hello 123 1.234 true 3 false World rain rain - EXPECT_STREQ(expectedResult.c_str(), result.c_str()); +abc +123123123 +[1, 2, 3, 1, 2, 3, 1, 2, 3] +rainrainrain +rainrainrain +)") +{ + params = { + {"intValue", 3}, + {"doubleValue", 12.123f}, + {"stringValue", "rain"}, + {"wstringValue", std::wstring(L"rain")}, + {"boolFalseValue", false}, + {"boolTrueValue", true}, + }; } -TEST(ExpressionsTest, IfExpression) -{ - std::string source = R"( +MULTISTR_TEST(ExpressionsMultiStrTest, IfExpression, +R"( {{ intValue if intValue is eq(3) }} {{ stringValue if intValue < 3 else doubleValue }} -)"; - - Template tpl; - ASSERT_TRUE(tpl.Load(source)); - - ValuesMap params = { +{{ wstringValue if intValue == 3 else doubleValue }} +)", +//----------- +R"( +3 +12.123 +rain +)") +{ + params = { {"intValue", 3}, {"doubleValue", 12.123f}, {"stringValue", "rain"}, + {"wstringValue", std::wstring(L"rain")}, {"boolFalseValue", false}, {"boolTrueValue", true}, }; +} + +MULTISTR_TEST(ExpressionsMultiStrTest, EmptyDict, +R"( +{% set d = {} %} +{{ d.asdf|default(42) }} +)", +//----------- +R"( + +42 +)") +{ +} + +TEST(ExpressionTest, DoStatement) +{ + std::string source = R"( +{{ data.strValue }}{% do setData('Inner Value') %} +{{ data.strValue }} +)"; + + TemplateEnv env; + env.GetSettings().extensions.Do = true; + + TestInnerStruct innerStruct; + innerStruct.strValue = "Outer Value"; + + ValuesMap params = { + {"data", Reflect(&innerStruct)}, + {"setData", MakeCallable( + [&innerStruct](const std::string& val) -> Value { + innerStruct.strValue = val; + return "String not to be shown"; + }, + ArgInfo{"val"}) + }, + }; + + Template tpl(&env); - std::string result = tpl.RenderAsString(params); + ASSERT_TRUE(tpl.Load(source)); + std::string result = tpl.RenderAsString(params).value(); std::cout << result << std::endl; std::string expectedResult = R"( -3 -12.123 +Outer Value +Inner Value )"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); } +TEST(ExpressionsTest, PipeOperatorPrecedenceTest) +{ + const std::string source = R"(>> {{ 2 < '6' | int }} << + >> {{ -30 | abs < str | int }} <<)"; + + Template tpl; + ASSERT_TRUE(tpl.Load(source)); + + const ValuesMap params = {{"str", "20"}}; + + const auto result = tpl.RenderAsString(params).value(); + std::cout << result << std::endl; + const std::string expectedResult = R"(>> true << + >> false <<)"; + + EXPECT_STREQ(expectedResult.c_str(), result.c_str()); +} + struct LogicalExprTestTag; using LogicalExprTest = InputOutputPairTest; @@ -103,15 +187,17 @@ TEST_P(LogicalExprTest, Test) return; } - std::string result = tpl.RenderAsString(PrepareTestData()); + std::string result = tpl.RenderAsString(PrepareTestData()).value(); std::cout << result << std::endl; std::string expectedResult = testParam.result; EXPECT_EQ(expectedResult, result); } -SUBSTITUION_TEST_P(ExpressionSubstitutionTest) +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(LogicalExprTest); -INSTANTIATE_TEST_CASE_P(ConstantSubstitutionTest, ExpressionSubstitutionTest, ::testing::Values( +SUBSTITUTION_TEST_P(ExpressionSubstitutionTest) + +INSTANTIATE_TEST_SUITE_P(ConstantSubstitutionTest, ExpressionSubstitutionTest, ::testing::Values( InputOutputPair{"'str1'", "str1"}, InputOutputPair{"\"str1\"", "str1"}, InputOutputPair{"100500", "100500"}, @@ -120,7 +206,7 @@ INSTANTIATE_TEST_CASE_P(ConstantSubstitutionTest, ExpressionSubstitutionTest, :: InputOutputPair{"false", "false"} )); -INSTANTIATE_TEST_CASE_P(LogicalExpressionTest, ExpressionSubstitutionTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(LogicalExpressionTest, ExpressionSubstitutionTest, ::testing::Values( InputOutputPair{"true", "true"}, InputOutputPair{"1 == 1", "true"}, InputOutputPair{"1 != 1", "false"}, @@ -145,7 +231,7 @@ INSTANTIATE_TEST_CASE_P(LogicalExpressionTest, ExpressionSubstitutionTest, ::tes InputOutputPair{"false", "false"} )); -INSTANTIATE_TEST_CASE_P(BasicValueSubstitutionTest, ExpressionSubstitutionTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(BasicValueSubstitutionTest, ExpressionSubstitutionTest, ::testing::Values( InputOutputPair{"intValue", "3"}, InputOutputPair{"doubleValue", "12.123"}, InputOutputPair{"stringValue", "rain"}, @@ -153,10 +239,11 @@ INSTANTIATE_TEST_CASE_P(BasicValueSubstitutionTest, ExpressionSubstitutionTest, InputOutputPair{"boolFalseValue", "false"} )); -INSTANTIATE_TEST_CASE_P(IndexSubscriptionTest, ExpressionSubstitutionTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(IndexSubscriptionTest, ExpressionSubstitutionTest, ::testing::Values( InputOutputPair{"intValue[0]", ""}, InputOutputPair{"doubleValue[0]", ""}, InputOutputPair{"stringValue[0]", "r"}, + InputOutputPair{"stringValue[100]", ""}, InputOutputPair{"boolTrueValue[0]", ""}, InputOutputPair{"boolFalseValue[0]", ""}, InputOutputPair{"intList[-1]", ""}, @@ -169,6 +256,11 @@ INSTANTIATE_TEST_CASE_P(IndexSubscriptionTest, ExpressionSubstitutionTest, ::tes InputOutputPair{"mapValue['stringVal']", "string100.5"}, InputOutputPair{"mapValue['boolValue']", "true"}, InputOutputPair{"mapValue['intVAl']", ""}, + InputOutputPair{"mapValue[0]", ""}, + InputOutputPair{"(mapValue | dictsort | first)['key']", "boolValue"}, + InputOutputPair{"(mapValue | dictsort | first)['value']", "true"}, + InputOutputPair{ "reflectedStringVector[0]", "9" }, + InputOutputPair{ "reflectedStringViewVector[0]", "9" }, InputOutputPair{"reflectedVal['intValue']", "0"}, InputOutputPair{"reflectedVal['dblValue']", "0"}, InputOutputPair{"reflectedVal['boolValue']", "false"}, @@ -176,21 +268,22 @@ INSTANTIATE_TEST_CASE_P(IndexSubscriptionTest, ExpressionSubstitutionTest, ::tes InputOutputPair{"reflectedVal['StrValue']", ""} )); -INSTANTIATE_TEST_CASE_P(DotSubscriptionTest, ExpressionSubstitutionTest, ::testing::Values( - InputOutputPair{"mapValue.intVal", "10"}, - InputOutputPair{"mapValue.dblVal", "100.5"}, - InputOutputPair{"mapValue.stringVal", "string100.5"}, - InputOutputPair{"mapValue.boolValue", "true"}, - InputOutputPair{"mapValue.intVAl", ""}, - InputOutputPair{"reflectedVal.intValue", "0"}, - InputOutputPair{"reflectedVal.dblValue", "0"}, - InputOutputPair{"reflectedVal.boolValue", "false"}, - InputOutputPair{"reflectedVal.strValue", "test string 0"}, - InputOutputPair{"reflectedVal.StrValue", ""} - )); +INSTANTIATE_TEST_SUITE_P(DotSubscriptionTest, ExpressionSubstitutionTest, ::testing::Values(InputOutputPair{ "mapValue.intVal", "10" }, + InputOutputPair{ "mapValue.dblVal", "100.5" }, + InputOutputPair{ "mapValue.stringVal", "string100.5" }, + InputOutputPair{ "mapValue.boolValue", "true" }, + InputOutputPair{ "mapValue.intVAl", "" }, + InputOutputPair{ "reflectedVal.intValue", "0" }, + InputOutputPair{ "reflectedVal.dblValue", "0" }, + InputOutputPair{ "reflectedVal.boolValue", "false" }, + InputOutputPair{ "reflectedVal.strValue", "test string 0" }, + InputOutputPair{ "reflectedVal.wstrValue", "test string 0" }, + InputOutputPair{ "reflectedVal.strViewValue", "test string 0" }, + InputOutputPair{ "reflectedVal.wstrViewValue", "test string 0" }, + InputOutputPair{ "reflectedVal.StrValue", "" })); -INSTANTIATE_TEST_CASE_P(ComplexSubscriptionTest, ExpressionSubstitutionTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(ComplexSubscriptionTest, ExpressionSubstitutionTest, ::testing::Values( InputOutputPair{"mapValue.reflectedList[1]['intValue']", "1"}, InputOutputPair{"mapValue['reflectedList'][1]['intValue']", "1"}, InputOutputPair{"mapValue.reflectedList[1].intValue", "1"}, @@ -207,5 +300,8 @@ INSTANTIATE_TEST_CASE_P(ComplexSubscriptionTest, ExpressionSubstitutionTest, ::t InputOutputPair{"reflectedList[1].strValue[0]", "t"}, InputOutputPair{"(reflectedList[1]).strValue[0]", "t"}, InputOutputPair{"(reflectedList | first).strValue[0]", "t"}, - InputOutputPair{"reflectedVal.strValue[0]", "t"} + InputOutputPair{"reflectedVal.strValue[0]", "t"}, + InputOutputPair{"reflectedVal.innerStruct.strValue", "Hello World!"}, + InputOutputPair{"reflectedVal.innerStructList[5].strValue", "Hello World!"}, + InputOutputPair{"reflectedVal.tmpStructList[5].strValue", "Hello World!"} )); diff --git a/test/extends_test.cpp b/test/extends_test.cpp index 300e9100..5214d1f0 100644 --- a/test/extends_test.cpp +++ b/test/extends_test.cpp @@ -6,19 +6,7 @@ #include "jinja2cpp/filesystem_handler.h" #include "jinja2cpp/template_env.h" -class ExtendsTest : public testing::Test -{ -public: - void SetUp() override - { - m_templateFs = std::make_shared(); - m_env.AddFilesystemHandler(std::string(), m_templateFs); - } - -protected: - std::shared_ptr m_templateFs; - jinja2::TemplateEnv m_env; -}; +using ExtendsTest = TemplateEnvFixture; TEST_F(ExtendsTest, BasicExtends) { @@ -28,11 +16,11 @@ TEST_F(ExtendsTest, BasicExtends) auto baseTpl = m_env.LoadTemplate("base.j2tpl").value(); auto tpl = m_env.LoadTemplate("derived.j2tpl").value(); - std::string baseResult = baseTpl.RenderAsString(jinja2::ValuesMap{}); + std::string baseResult = baseTpl.RenderAsString(jinja2::ValuesMap{}).value(); std::cout << baseResult << std::endl; std::string expectedResult = "Hello World!"; EXPECT_STREQ(expectedResult.c_str(), baseResult.c_str()); - std::string result = tpl.RenderAsString(jinja2::ValuesMap{}); + std::string result = tpl.RenderAsString(jinja2::ValuesMap{}).value(); std::cout << result << std::endl; EXPECT_STREQ(baseResult.c_str(), result.c_str()); } @@ -45,11 +33,11 @@ TEST_F(ExtendsTest, SimpleBlockExtends) auto baseTpl = m_env.LoadTemplate("base.j2tpl").value(); auto tpl = m_env.LoadTemplate("derived.j2tpl").value(); - std::string baseResult = baseTpl.RenderAsString(jinja2::ValuesMap{}); + std::string baseResult = baseTpl.RenderAsString(jinja2::ValuesMap{}).value(); std::cout << baseResult << std::endl; std::string expectedResult = "Hello World! -><-"; EXPECT_STREQ(expectedResult.c_str(), baseResult.c_str()); - std::string result = tpl.RenderAsString(jinja2::ValuesMap{}); + std::string result = tpl.RenderAsString(jinja2::ValuesMap{}).value(); std::cout << result << std::endl; expectedResult = "Hello World! ->Extended block!<-"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); @@ -65,15 +53,15 @@ TEST_F(ExtendsTest, TwoLevelBlockExtends) auto tpl = m_env.LoadTemplate("derived.j2tpl").value(); auto tpl2 = m_env.LoadTemplate("derived2.j2tpl").value(); - std::string baseResult = baseTpl.RenderAsString(jinja2::ValuesMap{}); + std::string baseResult = baseTpl.RenderAsString(jinja2::ValuesMap{}).value(); std::cout << baseResult << std::endl; std::string expectedResult = "Hello World! -><-"; EXPECT_STREQ(expectedResult.c_str(), baseResult.c_str()); - std::string result = tpl.RenderAsString(jinja2::ValuesMap{}); + std::string result = tpl.RenderAsString(jinja2::ValuesMap{}).value(); std::cout << result << std::endl; - expectedResult = "Hello World! ->Extended block!<-"; + expectedResult = "Hello World! ->Extended block!=>innerB1 content<=<-"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); - std::string result2 = tpl2.RenderAsString(jinja2::ValuesMap{}); + std::string result2 = tpl2.RenderAsString(jinja2::ValuesMap{}).value(); std::cout << result2 << std::endl; expectedResult = "Hello World! ->Extended block!derived2 block=>innerB1 content<=<-"; EXPECT_STREQ(expectedResult.c_str(), result2.c_str()); @@ -87,11 +75,11 @@ TEST_F(ExtendsTest, DoubleBlocksExtends) auto baseTpl = m_env.LoadTemplate("base.j2tpl").value(); auto tpl = m_env.LoadTemplate("derived.j2tpl").value(); - std::string baseResult = baseTpl.RenderAsString(jinja2::ValuesMap{}); + std::string baseResult = baseTpl.RenderAsString(jinja2::ValuesMap{}).value(); std::cout << baseResult << std::endl; std::string expectedResult = "Hello World! -><- -><-"; EXPECT_STREQ(expectedResult.c_str(), baseResult.c_str()); - std::string result = tpl.RenderAsString(jinja2::ValuesMap{}); + std::string result = tpl.RenderAsString(jinja2::ValuesMap{}).value(); std::cout << result << std::endl; expectedResult = "Hello World! ->Extended block b1!<- ->Extended block b2!<-"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); @@ -105,11 +93,11 @@ TEST_F(ExtendsTest, SuperBlocksExtends) auto baseTpl = m_env.LoadTemplate("base.j2tpl").value(); auto tpl = m_env.LoadTemplate("derived.j2tpl").value(); - std::string baseResult = baseTpl.RenderAsString(jinja2::ValuesMap{}); + std::string baseResult = baseTpl.RenderAsString(jinja2::ValuesMap{}).value(); std::cout << baseResult << std::endl; - std::string expectedResult = "Hello World! -><- -><-"; + std::string expectedResult = "Hello World! ->=>block b1<=<- -><-"; EXPECT_STREQ(expectedResult.c_str(), baseResult.c_str()); - std::string result = tpl.RenderAsString(jinja2::ValuesMap{}); + std::string result = tpl.RenderAsString(jinja2::ValuesMap{}).value(); std::cout << result << std::endl; expectedResult = "Hello World! ->Extended block b1!=>block b1<=<- ->Extended block b2!<-"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); @@ -128,17 +116,21 @@ TEST_F(ExtendsTest, SuperAndSelfBlocksExtends) auto baseTpl = m_env.LoadTemplate("base.j2tpl").value(); auto tpl = m_env.LoadTemplate("derived.j2tpl").value(); - std::string baseResult = baseTpl.RenderAsString(jinja2::ValuesMap{}); + std::string baseResult = baseTpl.RenderAsString(jinja2::ValuesMap{}).value(); std::cout << baseResult << std::endl; - std::string expectedResult = R"(Hello World!-><- ---><----><---><- + std::string expectedResult = R"(Hello World! +->=>block b1 - first entry<=<- +--><----><-- +-><- --><----><-- )"; EXPECT_STREQ(expectedResult.c_str(), baseResult.c_str()); - std::string result = tpl.RenderAsString(jinja2::ValuesMap{}); + std::string result = tpl.RenderAsString(jinja2::ValuesMap{}).value(); std::cout << result << std::endl; - expectedResult = R"(Hello World!->Extended block b1!=>block b1 - first entry<=<- --->Extended block b1!=>block b1 - first entry<=<----><--->Extended block b2!<- + expectedResult = R"(Hello World! +->Extended block b1!=>block b1 - first entry<=<- +-->Extended block b1!=>block b1 - first entry<=<----><-- +->Extended block b2!<- -->Extended block b1!=>block b1 - second entry<=<---->Extended block b2!<-- )"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); @@ -161,17 +153,21 @@ TEST_F(ExtendsTest, InnerBlocksExtends) auto baseTpl = m_env.LoadTemplate("base.j2tpl").value(); auto tpl = m_env.LoadTemplate("derived.j2tpl").value(); - std::string baseResult = baseTpl.RenderAsString(jinja2::ValuesMap{}); + std::string baseResult = baseTpl.RenderAsString(jinja2::ValuesMap{}).value(); std::cout << baseResult << std::endl; - std::string expectedResult = R"(Hello World!-><- ---><----><---><- + std::string expectedResult = R"(Hello World! +->=>block b1 - first entry<=<- +--><----><-- +-><- --><----><-- )"; EXPECT_STREQ(expectedResult.c_str(), baseResult.c_str()); - std::string result = tpl.RenderAsString(jinja2::ValuesMap{}); + std::string result = tpl.RenderAsString(jinja2::ValuesMap{}).value(); std::cout << result << std::endl; - expectedResult = R"(Hello World!->Extended block b1!=>block b1 - first entry<=###Extended innerB1 block first entry!###<- --->Extended block b1!=>block b1 - first entry<=###Extended innerB1 block first entry!###<----><--->Extended block b2!<- + expectedResult = R"(Hello World! +->Extended block b1!=>block b1 - first entry<=###Extended innerB1 block first entry!###<- +-->Extended block b1!=>block b1 - first entry<=###Extended innerB1 block first entry!###<----><-- +->Extended block b2!<- -->Extended block b1!=>block b1 - second entry<=###Extended innerB1 block second entry!###<---->Extended block b2!<-- )"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); @@ -187,14 +183,90 @@ TEST_F(ExtendsTest, ScopedBlocksExtends) auto baseTpl = m_env.LoadTemplate("base.j2tpl").value(); auto tpl = m_env.LoadTemplate("derived.j2tpl").value(); - std::string baseResult = baseTpl.RenderAsString(jinja2::ValuesMap{}); + std::string baseResult = baseTpl.RenderAsString(jinja2::ValuesMap{}).value(); std::cout << baseResult << std::endl; std::string expectedResult = "Hello World!\n-><-\n-><-"; EXPECT_STREQ(expectedResult.c_str(), baseResult.c_str()); - std::string result = tpl.RenderAsString(jinja2::ValuesMap{}); + std::string result = tpl.RenderAsString(jinja2::ValuesMap{}).value(); std::cout << result << std::endl; expectedResult = R"(Hello World! -><- ->0123456789<-)"; EXPECT_STREQ(expectedResult.c_str(), result.c_str()); } + +TEST_F(ExtendsTest, NoScopedGlobalVarsAccess) +{ + m_templateFs->AddFile("base.j2tpl", + "Hello World! ->{% block b1 %}=>block b1<={% endblock %}<- ->{% block b2 %}{% endblock b2%}{% block b3 %}Parent block loop {% for i " + "in range(num) %}Foo{{i+1}},{% endfor %}{% endblock b3%}<-"); + m_templateFs->AddFile( + "derived.j2tpl", + R"({% extends "base.j2tpl" %}{%block b1%}Extended block b1!{{super()}}{%endblock%}Some Stuff{%block b2%}Extended block b2!{%endblock%}{% block b3 %}This is overriden block "A". {% for i in range(num) %}Foo{{i+1}},{% endfor %}{% endblock b3 %})"); + + auto baseTpl = m_env.LoadTemplate("base.j2tpl").value(); + auto tpl = m_env.LoadTemplate("derived.j2tpl").value(); + + m_env.AddGlobal("num", 3); + + std::string baseResult = baseTpl.RenderAsString(jinja2::ValuesMap{}).value(); + std::cout << baseResult << std::endl; + std::string expectedResult = "Hello World! ->=>block b1<=<- ->Parent block loop Foo1,Foo2,Foo3,<-"; + EXPECT_STREQ(expectedResult.c_str(), baseResult.c_str()); + std::string result = tpl.RenderAsString(jinja2::ValuesMap{}).value(); + std::cout << result << std::endl; + expectedResult = "Hello World! ->Extended block b1!=>block b1<=<- ->Extended block b2!This is overriden block \"A\". Foo1,Foo2,Foo3,<-"; + EXPECT_STREQ(expectedResult.c_str(), result.c_str()); +} + +TEST_F(ExtendsTest, MacroUsage) +{ + m_templateFs->AddFile("base.j2tpl", R"(Hello World! +{% macro testMacro(str) %}{{ str | upper }}{% endmacro %} +{% block regularBlock %}{% endblock regularBlock%} +{% block scopedBlock scoped %}{% endblock scopedBlock%} +)"); + m_templateFs->AddFile("derived.j2tpl", R"({% extends "base.j2tpl" %} +{% block regularBlock %}->{{ testMacro('RegularMacroText') }}<-{% endblock %} +Some Stuff +{% block scopedBlock %}->{{ testMacro('ScopedMacroText') }}<-{% endblock %} +)"); + + auto baseTpl = m_env.LoadTemplate("base.j2tpl").value(); + auto tpl = m_env.LoadTemplate("derived.j2tpl").value(); + + std::string baseResult = baseTpl.RenderAsString(jinja2::ValuesMap{}).value(); + std::cout << baseResult << std::endl; + std::string expectedResult = "Hello World!\n\n\n\n"; + EXPECT_STREQ(expectedResult.c_str(), baseResult.c_str()); + std::string result = tpl.RenderAsString(jinja2::ValuesMap{}).value(); + std::cout << result << std::endl; + expectedResult = R"(Hello World! + +-><- +->SCOPEDMACROTEXT<- +)"; + EXPECT_STREQ(expectedResult.c_str(), result.c_str()); +} + +TEST_F(ExtendsTest, MacroUsageWithTrimming) +{ + m_templateFs->AddFile("base.j2tpl", R"({% macro testMacro(str) -%} +#{{ str | upper }}# +{%- endmacro %} +{%- block body scoped%}{% endblock body%})"); + m_templateFs->AddFile("derived.j2tpl", +R"({% extends "base.j2tpl" %}{% block body %}->{{ testMacro('RegularMacroText') }}<-{% endblock %})"); + + auto baseTpl = m_env.LoadTemplate("base.j2tpl").value(); + auto tpl = m_env.LoadTemplate("derived.j2tpl").value(); + + std::string baseResult = baseTpl.RenderAsString(jinja2::ValuesMap{}).value(); + std::cout << baseResult << std::endl; + std::string expectedResult = ""; + EXPECT_STREQ(expectedResult.c_str(), baseResult.c_str()); + std::string result = tpl.RenderAsString(jinja2::ValuesMap{}).value(); + std::cout << result << std::endl; + expectedResult = R"(->#REGULARMACROTEXT#<-)"; + EXPECT_STREQ(expectedResult.c_str(), result.c_str()); +} diff --git a/test/filesystem_handler_test.cpp b/test/filesystem_handler_test.cpp index fa485a8f..b271d2e3 100644 --- a/test/filesystem_handler_test.cpp +++ b/test/filesystem_handler_test.cpp @@ -1,6 +1,10 @@ #include #include +#include + +#include +#include class FilesystemHandlerTest : public testing::Test { @@ -125,3 +129,166 @@ LR"(Hello World! EXPECT_TRUE((bool)test1Stream); EXPECT_EQ(test1Content, ReadFile(test1Stream)); } + +TEST_F(FilesystemHandlerTest, TestDefaultCaching) +{ + const std::string test1Content = R"( +Line1 +Line2 +Line3 +)"; + const std::string test2Content = R"( +Line6 +Line7 +Line8 +)"; + jinja2::MemoryFileSystem fs; + fs.AddFile("test1.j2tpl", test1Content); + + jinja2::TemplateEnv env; + + env.AddFilesystemHandler("", fs); + auto tpl1 = env.LoadTemplate("test1.j2tpl").value(); + EXPECT_EQ(test1Content, tpl1.RenderAsString({}).value()); + + fs.AddFile("test1.j2tpl", test2Content); + auto tpl2 = env.LoadTemplate("test1.j2tpl").value(); + EXPECT_EQ(test1Content, tpl2.RenderAsString({}).value()); +} + +TEST_F(FilesystemHandlerTest, TestNoCaching) +{ + const std::string test1Content = R"( +Line1 +Line2 +Line3 +)"; + const std::string test2Content = R"( +Line6 +Line7 +Line8 +)"; + jinja2::MemoryFileSystem fs; + fs.AddFile("test1.j2tpl", test1Content); + + jinja2::TemplateEnv env; + env.GetSettings().cacheSize = 0; + + env.AddFilesystemHandler("", fs); + auto tpl1 = env.LoadTemplate("test1.j2tpl").value(); + EXPECT_EQ(test1Content, tpl1.RenderAsString({}).value()); + + fs.AddFile("test1.j2tpl", test2Content); + auto tpl2 = env.LoadTemplate("test1.j2tpl").value(); + EXPECT_EQ(test2Content, tpl2.RenderAsString({}).value()); +} + +TEST_F(FilesystemHandlerTest, TestDefaultRFSCaching) +{ + const std::string test1Content = R"( +Line1 +Line2 +Line3 +)"; + const std::string test2Content = R"( +Line6 +Line7 +Line8 +)"; + const std::string fileName = "test_data/cached_content.j2tpl"; + + jinja2::RealFileSystem fs; + { + std::ofstream os(fileName); + os << test1Content; + } + + jinja2::TemplateEnv env; + env.GetSettings().autoReload = false; + + env.AddFilesystemHandler("", fs); + auto tpl1 = env.LoadTemplate(fileName).value(); + EXPECT_EQ(test1Content, tpl1.RenderAsString({}).value()); + + { + std::ofstream os(fileName); + os << test2Content; + } + + auto tpl2 = env.LoadTemplate(fileName).value(); + EXPECT_EQ(test1Content, tpl2.RenderAsString({}).value()); +} + +TEST_F(FilesystemHandlerTest, TestRFSCachingReload) +{ + const std::string test1Content = R"( +Line1 +Line2 +Line3 +)"; + const std::string test2Content = R"( +Line6 +Line7 +Line8 +)"; + const std::string fileName = "test_data/cached_content.j2tpl"; + + jinja2::RealFileSystem fs; + { + std::ofstream os(fileName); + os << test1Content; + } + + jinja2::TemplateEnv env; + + env.AddFilesystemHandler("", fs); + auto tpl1 = env.LoadTemplate(fileName).value(); + EXPECT_EQ(test1Content, tpl1.RenderAsString({}).value()); + + std::this_thread::sleep_for(std::chrono::seconds(2)); + + { + std::ofstream os(fileName); + os << test2Content; + } + + auto tpl2 = env.LoadTemplate(fileName).value(); + EXPECT_EQ(test2Content, tpl2.RenderAsString({}).value()); +} + +TEST_F(FilesystemHandlerTest, TestNoRFSCaching) +{ + const std::string test1Content = R"( +Line1 +Line2 +Line3 +)"; + const std::string test2Content = R"( +Line6 +Line7 +Line8 +)"; + const std::string fileName = "test_data/cached_content.j2tpl"; + + jinja2::RealFileSystem fs; + { + std::ofstream os(fileName); + os << test1Content; + } + + jinja2::TemplateEnv env; + env.GetSettings().cacheSize = 0; + + env.AddFilesystemHandler("", fs); + auto tpl1 = env.LoadTemplate(fileName).value(); + EXPECT_EQ(test1Content, tpl1.RenderAsString({}).value()); + + { + std::ofstream os(fileName); + os << test2Content; + } + + auto tpl2 = env.LoadTemplate(fileName).value(); + EXPECT_EQ(test2Content, tpl2.RenderAsString({}).value()); +} + diff --git a/test/filters_test.cpp b/test/filters_test.cpp index 987bc5b2..aa67bacb 100644 --- a/test/filters_test.cpp +++ b/test/filters_test.cpp @@ -1,12 +1,13 @@ #include #include +#include #include "test_tools.h" #include "jinja2cpp/template.h" using namespace jinja2; -SUBSTITUION_TEST_P(FilterGenericTest) +SUBSTITUTION_TEST_P(FilterGenericTest) struct ListIteratorTestTag; using ListIteratorTest = InputOutputPairTest; @@ -14,18 +15,15 @@ using ListIteratorTest = InputOutputPairTest; struct GroupByTestTag; using FilterGroupByTest = InputOutputPairTest; +struct ListSliceTestTag; +using ListSliceTest = InputOutputPairTest; + TEST_P(ListIteratorTest, Test) { auto& testParam = GetParam(); std::string source = "{% for i in " + testParam.tpl + " %}{{i}}{{', ' if not loop.last}}{% endfor %}"; - Template tpl; - ASSERT_TRUE(tpl.Load(source)); - - std::string result = tpl.RenderAsString(PrepareTestData()); - std::cout << result << std::endl; - std::string expectedResult = testParam.result; - EXPECT_EQ(expectedResult, result); + PerformBothTests(source, testParam.result); } TEST_P(FilterGroupByTest, Test) @@ -54,23 +52,62 @@ TEST_P(FilterGroupByTest, Test) jinja2::ValuesMap params {{"testData", std::move(testData)}}; std::string source = R"( -{% for grouper, list in )" + testParam.tpl + R"( -%}grouper: {{grouper | pprint }} -{% for i in list %} +{% for group in )" + testParam.tpl + R"( +%}{% set grouper=group.grouper %}{% set list=group.list %}grouper: {{grouper | pprint }} +{%- for i in list %} {'intValue': {{i.intValue}}, 'dblValue': {{i.dblValue}}, 'boolValue': {{i.boolValue}}, 'strValue': '{{i.strValue}}', 'wstrValue': ''} -{% endfor %} +{%- endfor %} {% endfor %})"; - Template tpl; - ASSERT_TRUE(tpl.Load(source)); + PerformBothTests(source, testParam.result, params); +} - std::string result = tpl.RenderAsString(params); - std::cout << result << std::endl; - std::string expectedResult = testParam.result; - EXPECT_EQ(expectedResult, result); +using FilterGenericTestSingle = BasicTemplateRenderer; + +MULTISTR_TEST(FilterGenericTestSingle, ApplyMacroTest, + R"( +{% macro test(str, extra='') %}{{ (str ~ extra) | upper }}{% endmacro %} +{{ 'Hello World!' | applymacro(macro='test') }} +{{ 'Hello' | applymacro(macro='test', ' World!') }} +{{ ['str1', 'str2', 'str3'] | map('applymacro', macro='test') | join(', ') }} +{{ ['str1', 'str2', 'str3'] | map('applymacro', macro='test', '-') | join(', ') }} +)", +//------------- + R"( + +HELLO WORLD! +HELLO WORLD! +STR1, STR2, STR3 +STR1-, STR2-, STR3- +)") +{ } -INSTANTIATE_TEST_CASE_P(StringJoin, FilterGenericTest, ::testing::Values( +MULTISTR_TEST(FilterGenericTestSingle, ApplyMacroWithCallbackTest, + R"( +{% macro joiner(list, delim) %}{{ list | map('applymacro', macro='caller') | join(delim) }}{% endmacro %} +{% call(item) joiner(['str1', 'str2', 'str3'], '->') %}{{item | upper}}{% endcall %} + +)", +//-------- +R"( + +STR1->STR2->STR3 + +)" +) +{ +} + +TEST_P(ListSliceTest, Test) +{ + auto& testParam = GetParam(); + std::string source = "{{ " + testParam.tpl + " }}"; + + PerformBothTests(source, testParam.result); +} + +INSTANTIATE_TEST_SUITE_P(StringJoin, FilterGenericTest, ::testing::Values( InputOutputPair{"['str1', 'str2', 'str3'] | join", "str1str2str3"}, InputOutputPair{"['str1', 'str2', 'str3'] | join(' ')", "str1 str2 str3"}, InputOutputPair{"['str1', 'str2', 'str3'] | join(d='-')", "str1-str2-str3"}, @@ -79,7 +116,7 @@ INSTANTIATE_TEST_CASE_P(StringJoin, FilterGenericTest, ::testing::Values( InputOutputPair{"reflectedList | join(attribute='strValue', d='-')", "test string 0-test string 1-test string 2-test string 3-test string 4-test string 5-test string 6-test string 7-test string 8-test string 9"} )); -INSTANTIATE_TEST_CASE_P(Sort, ListIteratorTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Sort, ListIteratorTest, ::testing::Values( InputOutputPair{"['str1', 'str2', 'str3'] | sort", "str1, str2, str3"}, InputOutputPair{"['str2', 'str1', 'str3'] | sort", "str1, str2, str3"}, InputOutputPair{"['Str2', 'str1', 'str3'] | sort", "str1, Str2, str3"}, @@ -90,7 +127,7 @@ INSTANTIATE_TEST_CASE_P(Sort, ListIteratorTest, ::testing::Values( InputOutputPair{"reflectedIntVector | sort", "0, 1, 2, 3, 4, 5, 6, 7, 8, 9"} )); -INSTANTIATE_TEST_CASE_P(Default, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Default, FilterGenericTest, ::testing::Values( InputOutputPair{"intValue | default(0)", "3"}, InputOutputPair{"integerValue | default(0)", "0"}, InputOutputPair{"integerValue | d(0)", "0"}, @@ -100,7 +137,7 @@ INSTANTIATE_TEST_CASE_P(Default, FilterGenericTest, ::testing::Values( InputOutputPair{"'Hello World!'|default('the string was empty', true)", "Hello World!"} )); -INSTANTIATE_TEST_CASE_P(First, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(First, FilterGenericTest, ::testing::Values( InputOutputPair{"[1, 2, 3, 4] | first", "1"}, InputOutputPair{"(1, 2, 3, 4) | first", "1"}, InputOutputPair{"intValue | first", ""}, @@ -109,7 +146,7 @@ INSTANTIATE_TEST_CASE_P(First, FilterGenericTest, ::testing::Values( InputOutputPair{"reflectedIntVector | first", "9"} )); -INSTANTIATE_TEST_CASE_P(Last, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Last, FilterGenericTest, ::testing::Values( InputOutputPair{"[1, 2, 3, 4] | last", "4"}, InputOutputPair{"(1, 2, 3, 4) | last", "4"}, InputOutputPair{"intValue | last", ""}, @@ -118,7 +155,7 @@ INSTANTIATE_TEST_CASE_P(Last, FilterGenericTest, ::testing::Values( InputOutputPair{"reflectedIntVector | last", "4"} )); -INSTANTIATE_TEST_CASE_P(Length, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Length, FilterGenericTest, ::testing::Values( InputOutputPair{"[1, 2, 3, 4, 5] | length", "5"}, InputOutputPair{"(1, 2, 3, 4, 5, 6) | length", "6"}, InputOutputPair{"intValue | length", ""}, @@ -127,7 +164,7 @@ INSTANTIATE_TEST_CASE_P(Length, FilterGenericTest, ::testing::Values( InputOutputPair{"reflectedIntVector | length", "10"} )); -INSTANTIATE_TEST_CASE_P(Min, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Min, FilterGenericTest, ::testing::Values( InputOutputPair{"[1, 2, 3, 4, 5] | min", "1"}, InputOutputPair{"(1, 2, 3, 4, 5, 6) | min", "1"}, InputOutputPair{"intValue | min", ""}, @@ -138,7 +175,7 @@ INSTANTIATE_TEST_CASE_P(Min, FilterGenericTest, ::testing::Values( InputOutputPair{"('str1', 'str2', 'str3', 'str4', 'str5', 'Str6') | min(case_sensitive=true)", "Str6"} )); -INSTANTIATE_TEST_CASE_P(Max, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Max, FilterGenericTest, ::testing::Values( InputOutputPair{"[1, 2, 3, 4, 5] | max", "5"}, InputOutputPair{"(1, 2, 3, 4, 5, 6) | max", "6"}, InputOutputPair{"intValue | max", ""}, @@ -149,13 +186,13 @@ INSTANTIATE_TEST_CASE_P(Max, FilterGenericTest, ::testing::Values( InputOutputPair{"('str1', 'str2', 'str3', 'str4', 'str5', 'Str6') | max(case_sensitive=true)", "str5"} )); -INSTANTIATE_TEST_CASE_P(Reverse, ListIteratorTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Reverse, ListIteratorTest, ::testing::Values( InputOutputPair{"['str1', 'str2', 'str3'] | reverse", "str3, str2, str1"}, InputOutputPair{"[3, 1, 2] | reverse", "2, 1, 3"}, InputOutputPair{"reflectedIntVector | reverse", "4, 5, 3, 6, 2, 7, 1, 8, 0, 9"} )); -INSTANTIATE_TEST_CASE_P(Sum, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Sum, FilterGenericTest, ::testing::Values( InputOutputPair{"[1, 2, 3, 4, 5] | sum", "15"}, InputOutputPair{"[] | sum(start=15)", "15"}, InputOutputPair{"intValue | sum", ""}, @@ -169,7 +206,7 @@ INSTANTIATE_TEST_CASE_P(Sum, FilterGenericTest, ::testing::Values( "test string 0test string 1test string 2test string 3test string 4test string 5test string 6test string 7test string 8test string 9"} )); -INSTANTIATE_TEST_CASE_P(Unique, ListIteratorTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Unique, ListIteratorTest, ::testing::Values( InputOutputPair{"['str1', 'str2', 'str3'] | unique", "str1, str2, str3"}, InputOutputPair{"['str3', 'str1', 'str1'] | unique", "str3, str1"}, InputOutputPair{"['Str2', 'str1', 'str3'] | unique", "Str2, str1, str3"}, @@ -184,9 +221,11 @@ INSTANTIATE_TEST_CASE_P(Unique, ListIteratorTest, ::testing::Values( "test string 0, test string 1"} )); -INSTANTIATE_TEST_CASE_P(Attr, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Attr, FilterGenericTest, ::testing::Values( InputOutputPair{"{'key'='itemName', 'value'='itemValue'} | attr('key')", "itemName"}, InputOutputPair{"mapValue | attr('intVal')", "10"}, + InputOutputPair{"mapValue | attr('intVal', default='99')", "10"}, + InputOutputPair{"mapValue | attr('nonexistent', default='99')", "99"}, InputOutputPair{"mapValue | attr(name='dblVal')", "100.5"}, InputOutputPair{"mapValue | attr('stringVal')", "string100.5"}, InputOutputPair{"mapValue | attr('boolValue')", "true"}, @@ -194,8 +233,16 @@ INSTANTIATE_TEST_CASE_P(Attr, FilterGenericTest, ::testing::Values( InputOutputPair{"filledReflectedPtrVal | attr('strValue')", "test string 0"} )); -INSTANTIATE_TEST_CASE_P(Map, ListIteratorTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Map, ListIteratorTest, ::testing::Values( InputOutputPair{"reflectedList | map(attribute='intValue')", "0, 1, 2, 3, 4, 5, 6, 7, 8, 9"}, + InputOutputPair{"reflectedList | map(attribute='intValue', default='99')", + "0, 1, 2, 3, 4, 5, 6, 7, 8, 9"}, + InputOutputPair{"reflectedList | map(attribute='intEvenValue')", "0, , 2, , 4, , 6, , 8, "}, + InputOutputPair{"reflectedList | map(attribute='intEvenValue', default='99')", + "0, 99, 2, 99, 4, 99, 6, 99, 8, 99"}, + InputOutputPair{"reflectedList | map(attribute='nonexistent')", ", , , , , , , , , "}, + InputOutputPair{"reflectedList | map(attribute='nonexistent', default='99')", + "99, 99, 99, 99, 99, 99, 99, 99, 99, 99"}, InputOutputPair{"[[0, 1], [1, 2], [2, 3], [3, 4]] | map('first')", "0, 1, 2, 3"}, InputOutputPair{"[['str1', 'Str2'], ['str2', 'Str3'], ['str3', 'Str4'], ['str4', 'Str5']] | map('min')", "str1, str2, str3, str4"}, @@ -204,7 +251,7 @@ INSTANTIATE_TEST_CASE_P(Map, ListIteratorTest, ::testing::Values( )); -INSTANTIATE_TEST_CASE_P(Reject, ListIteratorTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Reject, ListIteratorTest, ::testing::Values( InputOutputPair{"['', 'str1', '', 'str2', '', 'str3', '', 'str4'] | reject", ", , , "}, InputOutputPair{"['_str1', 'str1', '_str2', 'str2', '_str3', 'str3', '_str4', 'str4'] | reject('startsWith', '_')", "str1, str2, str3, str4"}, @@ -212,14 +259,14 @@ INSTANTIATE_TEST_CASE_P(Reject, ListIteratorTest, ::testing::Values( "str1, str2, str3, str4"} )); -INSTANTIATE_TEST_CASE_P(RejectAttr, ListIteratorTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(RejectAttr, ListIteratorTest, ::testing::Values( InputOutputPair{"reflectedList | rejectattr('boolValue') | map(attribute='strValue')", "test string 0, test string 2, test string 4, test string 6, test string 8"}, InputOutputPair{"reflectedList | rejectattr(attribute='boolValue') | map(attribute='strValue')", "test string 0, test string 2, test string 4, test string 6, test string 8"} )); -INSTANTIATE_TEST_CASE_P(Select, ListIteratorTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Select, ListIteratorTest, ::testing::Values( InputOutputPair{"['', 'str1', '', 'str2', '', 'str3', '', 'str4'] | select", "str1, str2, str3, str4"}, InputOutputPair{"['_str1', 'str1', '_str2', 'str2', '_str3', 'str3', '_str4', 'str4'] | select('startsWith', '_')", "_str1, _str2, _str3, _str4"}, @@ -227,7 +274,7 @@ INSTANTIATE_TEST_CASE_P(Select, ListIteratorTest, ::testing::Values( "_str1, _str2, _str3, _str4"} )); -INSTANTIATE_TEST_CASE_P(SelectAttr, ListIteratorTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(SelectAttr, ListIteratorTest, ::testing::Values( InputOutputPair{"reflectedList | selectattr('boolValue') | map(attribute='strValue')", "test string 1, test string 3, test string 5, test string 7, test string 9"}, InputOutputPair{"reflectedList | selectattr(attribute='boolValue') | map(attribute='strValue')", @@ -235,7 +282,7 @@ INSTANTIATE_TEST_CASE_P(SelectAttr, ListIteratorTest, ::testing::Values( )); -INSTANTIATE_TEST_CASE_P(PPrint, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(PPrint, FilterGenericTest, ::testing::Values( InputOutputPair{"10 | pprint", "10"}, InputOutputPair{"10.5 | pprint", "10.5"}, InputOutputPair{"intValue | pprint", "3"}, @@ -246,7 +293,7 @@ INSTANTIATE_TEST_CASE_P(PPrint, FilterGenericTest, ::testing::Values( InputOutputPair{"{'key'='itemName'} | pprint", "{'key': 'itemName'}"} )); -INSTANTIATE_TEST_CASE_P(GroupBy, FilterGroupByTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(GroupBy, FilterGroupByTest, ::testing::Values( InputOutputPair{"testData | groupby('intValue')", R"( grouper: 0 {'intValue': 0, 'dblValue': 0, 'boolValue': false, 'strValue': 'test string 0', 'wstrValue': ''} @@ -318,7 +365,7 @@ grouper: true } )); -INSTANTIATE_TEST_CASE_P(DictSort, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(DictSort, FilterGenericTest, ::testing::Values( InputOutputPair{"{'key'='itemName', 'Value'='ItemValue'} | dictsort | pprint", "['key': 'itemName', 'Value': 'ItemValue']"}, InputOutputPair{"{'key'='itemName', 'Value'='ItemValue'} | dictsort(by='value') | pprint", "['key': 'itemName', 'Value': 'ItemValue']"}, InputOutputPair{"{'key'='itemName', 'Value'='ItemValue'} | dictsort(reverse=true) | pprint", "['Value': 'ItemValue', 'key': 'itemName']"}, @@ -326,16 +373,23 @@ INSTANTIATE_TEST_CASE_P(DictSort, FilterGenericTest, ::testing::Values( InputOutputPair{"{'key'='itemName', 'Value'='ItemValue'} | dictsort(case_sensitive=true) | pprint", "['Value': 'ItemValue', 'key': 'itemName']"}, InputOutputPair{"{'key'='itemName', 'Value'='ItemValue'} | dictsort(case_sensitive=true, reverse=true) | pprint", "['key': 'itemName', 'Value': 'ItemValue']"}, InputOutputPair{"simpleMapValue | dictsort | pprint", "['boolValue': true, 'dblVal': 100.5, 'intVal': 10, 'stringVal': 'string100.5']"}, - InputOutputPair{"reflectedVal | dictsort | pprint", "['boolValue': false, 'dblValue': 0, 'intValue': 0, 'strValue': 'test string 0', 'wstrValue': '']"} - )); - -INSTANTIATE_TEST_CASE_P(UrlEncode, FilterGenericTest, ::testing::Values( + InputOutputPair{ + "reflectedVal | dictsort | pprint", + "['basicCallable': , 'boolValue': false, 'dblValue': 0, 'getInnerStruct': , 'getInnerStructValue': , 'innerStruct': " + "{'strValue': 'Hello World!'}, 'innerStructList': [{'strValue': 'Hello World!'}, {'strValue': 'Hello World!'}, {'strValue': 'Hello World!'}, " + "{'strValue': 'Hello World!'}, {'strValue': 'Hello World!'}, {'strValue': 'Hello World!'}, {'strValue': 'Hello World!'}, {'strValue': 'Hello World!'}, " + "{'strValue': 'Hello World!'}, {'strValue': 'Hello World!'}], 'intEvenValue': 0, 'intValue': 0, 'strValue': 'test string 0', 'strViewValue': 'test " + "string 0', 'tmpStructList': [{'strValue': 'Hello World!'}, {'strValue': 'Hello World!'}, {'strValue': 'Hello World!'}, {'strValue': 'Hello World!'}, " + "{'strValue': 'Hello World!'}, {'strValue': 'Hello World!'}, {'strValue': 'Hello World!'}, {'strValue': 'Hello World!'}, {'strValue': 'Hello World!'}, " + "{'strValue': 'Hello World!'}], 'wstrValue': 'test string 0', 'wstrViewValue': 'test string 0']" })); + +INSTANTIATE_TEST_SUITE_P(UrlEncode, FilterGenericTest, ::testing::Values( InputOutputPair{"'Hello World' | urlencode", "Hello+World"}, - InputOutputPair{"'Hello World\xD0\x9C\xD0\xBA' | urlencode", "Hello+World%D0%9C%D0%BA"}, + // InputOutputPair{"'Hello World\xD0\x9C\xD0\xBA' | urlencode", "Hello+World%D0%9C%D0%BA"}, InputOutputPair{"'! # $ & ( ) * + , / : ; = ? @ [ ] %' | urlencode", "%21+%23+%24+%26+%28+%29+%2A+%2B+%2C+%2F+%3A+%3B+%3D+%3F+%40+%5B+%5D+%25"} )); -INSTANTIATE_TEST_CASE_P(Abs, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Abs, FilterGenericTest, ::testing::Values( InputOutputPair{"10 | abs", "10"}, InputOutputPair{"-10 | abs", "10"}, InputOutputPair{"10.5 | abs", "10.5"}, @@ -343,7 +397,7 @@ INSTANTIATE_TEST_CASE_P(Abs, FilterGenericTest, ::testing::Values( InputOutputPair{"'10' | abs", ""} )); -INSTANTIATE_TEST_CASE_P(Round, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Round, FilterGenericTest, ::testing::Values( InputOutputPair{"10 | round", "10"}, InputOutputPair{"10 | round(1)", "10"}, InputOutputPair{"10.5 | round", "11"}, @@ -360,7 +414,7 @@ INSTANTIATE_TEST_CASE_P(Round, FilterGenericTest, ::testing::Values( InputOutputPair{"10.46 | round(precision=1)", "10.5"} )); -INSTANTIATE_TEST_CASE_P(Convert, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Convert, FilterGenericTest, ::testing::Values( InputOutputPair{"10 | int", "10"}, InputOutputPair{"10 | float", "10"}, InputOutputPair{"10.5 | int", "10"}, @@ -375,10 +429,10 @@ INSTANTIATE_TEST_CASE_P(Convert, FilterGenericTest, ::testing::Values( InputOutputPair{"'100' | int(10, base=8) | pprint", "64"}, InputOutputPair{"'100' | int(10, base=16) | pprint", "256"}, InputOutputPair{"'100' | list | pprint", "['1', '0', '0']"}, - InputOutputPair{"{'name'='itemName', 'val'='itemValue'} | list | pprint", "['name': 'itemName', 'val': 'itemValue']"} + InputOutputPair{"{'name'='itemName', 'val'='itemValue'} | list | sort | pprint", "['name', 'val']"} )); -INSTANTIATE_TEST_CASE_P(Trim, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Trim, FilterGenericTest, ::testing::Values( InputOutputPair{"'string' | trim | pprint", "'string'"}, InputOutputPair{"' string' | trim | pprint", "'string'"}, InputOutputPair{"'string ' | trim | pprint", "'string'"}, @@ -386,7 +440,7 @@ INSTANTIATE_TEST_CASE_P(Trim, FilterGenericTest, ::testing::Values( InputOutputPair{"wstringValue | trim", "'hello world'"}*/ )); -INSTANTIATE_TEST_CASE_P(Title, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Title, FilterGenericTest, ::testing::Values( InputOutputPair{"'string' | title | pprint", "'String'"}, InputOutputPair{"'1234string' | title | pprint", "'1234string'"}, InputOutputPair{"'hello world' | title | pprint", "'Hello World'"}, @@ -394,7 +448,7 @@ INSTANTIATE_TEST_CASE_P(Title, FilterGenericTest, ::testing::Values( InputOutputPair{"wstringValue | trim", "'hello world'"}*/ )); -INSTANTIATE_TEST_CASE_P(Upper, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Upper, FilterGenericTest, ::testing::Values( InputOutputPair{"'string' | upper | pprint", "'STRING'"}, InputOutputPair{"'1234string' | upper | pprint", "'1234STRING'"}, InputOutputPair{"'hello world' | upper | pprint", "'HELLO WORLD'"}, @@ -403,7 +457,7 @@ INSTANTIATE_TEST_CASE_P(Upper, FilterGenericTest, ::testing::Values( )); -INSTANTIATE_TEST_CASE_P(Lower, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Lower, FilterGenericTest, ::testing::Values( InputOutputPair{"'String' | lower | pprint", "'string'"}, InputOutputPair{"'1234String' | lower | pprint", "'1234string'"}, InputOutputPair{"'Hello World' | lower | pprint", "'hello world'"}, @@ -412,7 +466,7 @@ INSTANTIATE_TEST_CASE_P(Lower, FilterGenericTest, ::testing::Values( )); -INSTANTIATE_TEST_CASE_P(WordCount, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(WordCount, FilterGenericTest, ::testing::Values( InputOutputPair{"'string' | wordcount", "1"}, InputOutputPair{"'1234string' | wordcount", "1"}, InputOutputPair{"'hello world' | wordcount", "2"}, @@ -420,14 +474,14 @@ INSTANTIATE_TEST_CASE_P(WordCount, FilterGenericTest, ::testing::Values( InputOutputPair{"wstringValue | trim", "'hello world'"}*/ )); -INSTANTIATE_TEST_CASE_P(Replace, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Replace, FilterGenericTest, ::testing::Values( InputOutputPair{"'Hello World' | replace('Hello', 'Goodbye') | pprint", "'Goodbye World'"}, InputOutputPair{"'Hello World' | replace(old='l', new='L') | pprint", "'HeLLo WorLd'"}, InputOutputPair{"'Hello World' | replace(old='l', new='L', 2) | pprint", "'HeLLo World'"}, InputOutputPair{"'Hello World' | replace('l', 'L', count=1) | pprint", "'HeLlo World'"} )); -INSTANTIATE_TEST_CASE_P(Truncate, FilterGenericTest, ::testing::Values( +INSTANTIATE_TEST_SUITE_P(Truncate, FilterGenericTest, ::testing::Values( InputOutputPair{"'foo bar baz qux' | truncate(6, leeway=0) | pprint", "'foo...'"}, InputOutputPair{"'foo bar baz qux' | truncate(6, true) | pprint", "'foo ba...'"}, InputOutputPair{"'foo bar baz qux' | truncate(11, true) | pprint", "'foo bar baz qux'"}, @@ -437,3 +491,233 @@ INSTANTIATE_TEST_CASE_P(Truncate, FilterGenericTest, ::testing::Values( InputOutputPair{"'VeryVeryVeryLongWord' | truncate(16) | pprint", "'VeryVeryVeryLongWord'"}, InputOutputPair{"'foo bar baz qux' | truncate(6, end=' >>', leeway=0) | pprint", "'foo >>'"} )); + +INSTANTIATE_TEST_SUITE_P(Capitalize, FilterGenericTest, ::testing::Values( + InputOutputPair{"'String' | capitalize | pprint", "'String'"}, + InputOutputPair{"'string' | capitalize | pprint", "'String'"}, + InputOutputPair{"'1234string' | capitalize | pprint", "'1234string'"}, + InputOutputPair{"'1234String' | capitalize | pprint", "'1234string'"}, + InputOutputPair{"'Hello World' | capitalize | pprint", "'Hello world'"}, + InputOutputPair{"' Hello World' | capitalize | pprint", "' hello world'"}, + InputOutputPair{"'Hello123OOO, World!' | capitalize | pprint", "'Hello123ooo, world!'"} + )); + +INSTANTIATE_TEST_SUITE_P(Escape, FilterGenericTest, ::testing::Values( + InputOutputPair{"'' | escape | pprint", "''"}, + InputOutputPair{"'abcd&> baz ' | striptags | pprint", "'foo baz'" }, + InputOutputPair{"'ab&cd&><efgh' | striptags | pprint", "'ab&cd&>Foo & Bar' | striptags | pprint", "'Foo & Bar'"}, + InputOutputPair{"'&'><"'\""' | striptags | pprint", "'&\'><\"\'\"\"'"}, + InputOutputPair{"'"'' | striptags | pprint", "'\"\''"})); + + +INSTANTIATE_TEST_SUITE_P(Center, FilterGenericTest, ::testing::Values( + InputOutputPair{" 'x' | center | pprint", "' x '"}, + InputOutputPair{" 'x' | center(width=5) | pprint", "' x '"}, + InputOutputPair{" 'x' | center(width=0) | pprint", "'x'"}, + InputOutputPair{" ' x' | center(width=5) | pprint", "' x '"} + )); +struct XmlAttr : ::testing::Test +{ + template + using AttributeSet = std::set>; + + template + AttributeSet ExtractAttributeSet(const std::basic_string &attributeString) + { + using String = std::basic_string; + using Regex = std::basic_regex; + using RegexTokenIterator = std::regex_token_iterator; + + AttributeSet result; + const Regex pattern(ConvertString(std::string("(\\S+=[\"].*?[\"])"))); + std::copy(RegexTokenIterator(attributeString.begin(), attributeString.end(), pattern, 0), + RegexTokenIterator(), + std::inserter(result, result.begin())); + + return result; + } + + template + void PerformXmlAttrTest(const StringType& source, const StringType& expectedResult, const jinja2::ValuesMap& params) + { + TemplateType tpl; + ASSERT_TRUE(tpl.Load(source)); + const auto result = tpl.RenderAsString(params).value(); + + const auto resultAttributes = ExtractAttributeSet(result); + const auto expectedAttributes = ExtractAttributeSet(expectedResult); + + EXPECT_EQ(resultAttributes, expectedAttributes); + } + + void PerformBothXmlAttrTests(const std::string& source, const std::string& expectedResult, const jinja2::ValuesMap& params) + { + PerformXmlAttrTest