├── .Rbuildignore ├── .github ├── .gitignore └── workflows │ ├── R-CMD-check.yaml │ ├── pkgdown.yaml │ ├── pr-commands.yaml │ └── test-coverage.yaml ├── .gitignore ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── NEWS.md ├── R ├── check.R ├── condition.R ├── config.R ├── cpp11.R ├── encode.R ├── jinjar-package.R ├── knit.R ├── loader.R ├── parse.R ├── print.R ├── render.R └── zzz.R ├── README.Rmd ├── README.md ├── codecov.yml ├── cran-comments.md ├── jinjar.Rproj ├── man ├── figures │ ├── logo.png │ └── logo.svg ├── jinjar-package.Rd ├── jinjar_config.Rd ├── loader.Rd ├── parse.Rd ├── print.Rd └── render.Rd ├── pkgdown ├── _pkgdown.yml └── favicon │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-152x152.png │ ├── apple-touch-icon-180x180.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── favicon.ico ├── revdep ├── .gitignore ├── README.md ├── cran.md ├── email.yml ├── failures.md └── problems.md ├── src ├── .gitignore ├── Makevars ├── condition.cpp ├── condition.h ├── cpp11.cpp ├── inja │ ├── inja.hpp │ └── nlohmann │ │ └── json.hpp ├── jinjar_types.h ├── loader.cpp ├── loader.h ├── render.cpp ├── template.cpp └── template.h ├── tests ├── testthat.R └── testthat │ ├── _snaps │ ├── config.md │ ├── encode.md │ ├── loader.md │ ├── parse.md │ └── render.md │ ├── helper.R │ ├── knit-engine.Rmd │ ├── test-config.R │ ├── test-encode.R │ ├── test-knit.R │ ├── test-loader.R │ ├── test-parse.R │ └── test-render.R └── vignettes ├── .gitignore ├── auxiliary-templates.Rmd └── template-syntax.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^jinjar\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^LICENSE\.md$ 4 | ^README\.Rmd$ 5 | ^cran-comments\.md$ 6 | ^\.github$ 7 | ^codecov\.yml$ 8 | ^_pkgdown\.yml$ 9 | ^docs$ 10 | ^pkgdown$ 11 | ^CRAN-RELEASE$ 12 | ^revdep$ 13 | ^CRAN-SUBMISSION$ 14 | ^man/figures/logo\.svg$ 15 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | # 4 | # NOTE: This workflow is overkill for most R packages and 5 | # check-standard.yaml is likely a better choice. 6 | # usethis::use_github_action("check-standard") will install it. 7 | on: 8 | push: 9 | branches: [main, master] 10 | pull_request: 11 | 12 | name: R-CMD-check.yaml 13 | 14 | permissions: read-all 15 | 16 | jobs: 17 | R-CMD-check: 18 | runs-on: ${{ matrix.config.os }} 19 | 20 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | config: 26 | - {os: macos-latest, r: 'release'} 27 | 28 | - {os: windows-latest, r: 'release'} 29 | # use 4.0 or 4.1 to check with rtools40's older compiler 30 | - {os: windows-latest, r: 'oldrel-4'} 31 | 32 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 33 | - {os: ubuntu-latest, r: 'release'} 34 | - {os: ubuntu-latest, r: 'oldrel-1'} 35 | - {os: ubuntu-latest, r: 'oldrel-2'} 36 | - {os: ubuntu-latest, r: 'oldrel-3'} 37 | - {os: ubuntu-latest, r: 'oldrel-4'} 38 | 39 | env: 40 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 41 | R_KEEP_PKG_SOURCE: yes 42 | 43 | steps: 44 | - uses: actions/checkout@v4 45 | 46 | - uses: r-lib/actions/setup-pandoc@v2 47 | 48 | - uses: r-lib/actions/setup-r@v2 49 | with: 50 | r-version: ${{ matrix.config.r }} 51 | http-user-agent: ${{ matrix.config.http-user-agent }} 52 | use-public-rspm: true 53 | 54 | - uses: r-lib/actions/setup-r-dependencies@v2 55 | with: 56 | extra-packages: any::rcmdcheck 57 | needs: check 58 | 59 | - uses: r-lib/actions/check-r-package@v2 60 | with: 61 | upload-snapshots: true 62 | build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' 63 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | release: 8 | types: [published] 9 | workflow_dispatch: 10 | 11 | name: pkgdown.yaml 12 | 13 | permissions: read-all 14 | 15 | jobs: 16 | pkgdown: 17 | runs-on: ubuntu-latest 18 | # Only restrict concurrency for non-PR jobs 19 | concurrency: 20 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 21 | env: 22 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 23 | permissions: 24 | contents: write 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - uses: r-lib/actions/setup-pandoc@v2 29 | 30 | - uses: r-lib/actions/setup-r@v2 31 | with: 32 | use-public-rspm: true 33 | 34 | - uses: r-lib/actions/setup-r-dependencies@v2 35 | with: 36 | extra-packages: any::pkgdown, local::. 37 | needs: website 38 | 39 | - name: Build site 40 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 41 | shell: Rscript {0} 42 | 43 | - name: Deploy to GitHub pages 🚀 44 | if: github.event_name != 'pull_request' 45 | uses: JamesIves/github-pages-deploy-action@v4.5.0 46 | with: 47 | clean: false 48 | branch: gh-pages 49 | folder: docs 50 | -------------------------------------------------------------------------------- /.github/workflows/pr-commands.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | name: pr-commands.yaml 8 | 9 | permissions: read-all 10 | 11 | jobs: 12 | document: 13 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/document') }} 14 | name: document 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 18 | permissions: 19 | contents: write 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - uses: r-lib/actions/pr-fetch@v2 24 | with: 25 | repo-token: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | - uses: r-lib/actions/setup-r@v2 28 | with: 29 | use-public-rspm: true 30 | 31 | - uses: r-lib/actions/setup-r-dependencies@v2 32 | with: 33 | extra-packages: any::roxygen2 34 | needs: pr-document 35 | 36 | - name: Document 37 | run: roxygen2::roxygenise() 38 | shell: Rscript {0} 39 | 40 | - name: commit 41 | run: | 42 | git config --local user.name "$GITHUB_ACTOR" 43 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 44 | git add man/\* NAMESPACE 45 | git commit -m 'Document' 46 | 47 | - uses: r-lib/actions/pr-push@v2 48 | with: 49 | repo-token: ${{ secrets.GITHUB_TOKEN }} 50 | 51 | style: 52 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/style') }} 53 | name: style 54 | runs-on: ubuntu-latest 55 | env: 56 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 57 | permissions: 58 | contents: write 59 | steps: 60 | - uses: actions/checkout@v4 61 | 62 | - uses: r-lib/actions/pr-fetch@v2 63 | with: 64 | repo-token: ${{ secrets.GITHUB_TOKEN }} 65 | 66 | - uses: r-lib/actions/setup-r@v2 67 | 68 | - name: Install dependencies 69 | run: install.packages("styler") 70 | shell: Rscript {0} 71 | 72 | - name: Style 73 | run: styler::style_pkg() 74 | shell: Rscript {0} 75 | 76 | - name: commit 77 | run: | 78 | git config --local user.name "$GITHUB_ACTOR" 79 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 80 | git add \*.R 81 | git commit -m 'Style' 82 | 83 | - uses: r-lib/actions/pr-push@v2 84 | with: 85 | repo-token: ${{ secrets.GITHUB_TOKEN }} 86 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | 8 | name: test-coverage.yaml 9 | 10 | permissions: read-all 11 | 12 | jobs: 13 | test-coverage: 14 | runs-on: ubuntu-latest 15 | env: 16 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - uses: r-lib/actions/setup-r@v2 22 | with: 23 | use-public-rspm: true 24 | 25 | - uses: r-lib/actions/setup-r-dependencies@v2 26 | with: 27 | extra-packages: any::covr, any::xml2 28 | needs: coverage 29 | 30 | - name: Test coverage 31 | run: | 32 | cov <- covr::package_coverage( 33 | quiet = FALSE, 34 | clean = FALSE, 35 | install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") 36 | ) 37 | print(cov) 38 | covr::to_cobertura(cov) 39 | shell: Rscript {0} 40 | 41 | - uses: codecov/codecov-action@v4 42 | with: 43 | # Fail if error if not on PR, or if on PR and token is given 44 | fail_ci_if_error: ${{ github.event_name != 'pull_request' || secrets.CODECOV_TOKEN }} 45 | file: ./cobertura.xml 46 | plugin: noop 47 | disable_search: true 48 | token: ${{ secrets.CODECOV_TOKEN }} 49 | 50 | - name: Show testthat output 51 | if: always() 52 | run: | 53 | ## -------------------------------------------------------------------- 54 | find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true 55 | shell: bash 56 | 57 | - name: Upload test results 58 | if: failure() 59 | uses: actions/upload-artifact@v4 60 | with: 61 | name: coverage-test-failures 62 | path: ${{ runner.temp }}/package 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .Rdata 4 | .httr-oauth 5 | .DS_Store 6 | docs 7 | inst/doc 8 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: jinjar 2 | Title: Template Engine Inspired by 'Jinja' 3 | Version: 0.3.2.9000 4 | Authors@R: c( 5 | person("David", "Hall", , "david.hall.physics@gmail.com", role = c("aut", "cre", "cph"), 6 | comment = c(ORCID = "0000-0002-2193-0480")), 7 | person("Lars", "Berscheid", role = "cph", 8 | comment = "Author of bundled inja library"), 9 | person("Niels", "Lohmann", role = "cph", 10 | comment = "Author of bundled json library") 11 | ) 12 | Description: Template engine powered by the 'inja' C++ library. Users 13 | write a template document, using syntax inspired by the 'Jinja' Python 14 | package, and then render the final document by passing data from R. 15 | The template syntax supports features such as variables, loops, 16 | conditions and inheritance. 17 | License: MIT + file LICENSE 18 | URL: https://davidchall.github.io/jinjar/, 19 | https://github.com/davidchall/jinjar 20 | BugReports: https://github.com/davidchall/jinjar/issues 21 | Imports: 22 | cli, 23 | fs, 24 | jsonlite, 25 | rlang (>= 1.0.0) 26 | Suggests: 27 | knitr, 28 | rmarkdown, 29 | testthat 30 | LinkingTo: 31 | cpp11 32 | VignetteBuilder: 33 | knitr 34 | Config/testthat/edition: 3 35 | Encoding: UTF-8 36 | Language: en-US 37 | Roxygen: list(markdown = TRUE) 38 | RoxygenNote: 7.3.2 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2021 2 | COPYRIGHT HOLDER: jinjar authors 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2021 jinjar authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(parse_template,character) 4 | S3method(parse_template,fs_path) 5 | S3method(print,jinjar_config) 6 | S3method(print,jinjar_template) 7 | S3method(print,list_loader) 8 | S3method(print,package_loader) 9 | S3method(print,path_loader) 10 | S3method(render,character) 11 | S3method(render,fs_path) 12 | S3method(render,jinjar_template) 13 | export(default_config) 14 | export(jinjar_config) 15 | export(list_loader) 16 | export(package_loader) 17 | export(parse_template) 18 | export(path_loader) 19 | export(render) 20 | import(rlang) 21 | importFrom(utils,head) 22 | useDynLib(jinjar, .registration = TRUE) 23 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # jinjar (development version) 2 | 3 | # jinjar 0.3.2 4 | 5 | Fix for CRAN checks. 6 | 7 | 8 | # jinjar 0.3.1 9 | 10 | * Fixed a bug where disabling line statements could raise an error during template parsing. Since line statements are disabled by default, this error could be encountered quite easily (#31). 11 | * `quote_sql()` now escapes single-quotes using an additional single-quote (#30). 12 | * Fixed edge case in how error messages are formatted (#32). 13 | * Documented how `render()` can preserve length-1 vectors (#27). 14 | 15 | 16 | # jinjar 0.3.0 17 | 18 | This release provides several quality-of-life improvements. 19 | 20 | * `print.jinjar_template()` now highlights templating blocks using {cli} (#18). 21 | * Variables are green 22 | * Control blocks are blue 23 | * Comments are italic grey 24 | * `print.jinjar_template()` gains an `n` argument to control the number of lines displayed (#18). 25 | * `jinjar_config()` objects are now printed using {cli} (#22). 26 | * Exceptions raised by C++ code are now converted to R errors (#20). 27 | 28 | 29 | # jinjar 0.2.0 30 | 31 | * New `parse_template()` to parse a template once and `render()` it multiple times (#13). 32 | * New template function `quote_sql()` simplifies writing SQL queries. See `vignette("template-syntax")` for details (#14). 33 | 34 | # jinjar 0.1.1 35 | 36 | * `path_loader()` now correctly finds template files outside the working directory (#11). 37 | 38 | # jinjar 0.1.0 39 | 40 | Initial version on CRAN. 41 | -------------------------------------------------------------------------------- /R/check.R: -------------------------------------------------------------------------------- 1 | check_string <- function(x, arg = caller_arg(x), call = caller_env()) { 2 | if (!is_string(x)) { 3 | cli::cli_abort("{.arg {arg}} must be a string", arg = arg, call = call) 4 | } 5 | } 6 | 7 | check_bool <- function(x, arg = caller_arg(x), call = caller_env()) { 8 | if (!is_bool(x)) { 9 | cli::cli_abort("{.arg {arg}} must be TRUE or FALSE", arg = arg, call = call) 10 | } 11 | } 12 | 13 | check_count <- function(x, inf = FALSE, arg = caller_arg(x), call = caller_env()) { 14 | finite <- if (inf) NULL else TRUE 15 | if (!is_scalar_integerish(x, finite = finite) || is.na(x) || x <= 0) { 16 | cli::cli_abort("{.arg {arg}} must be a positive integer", arg = arg, call = call) 17 | } 18 | } 19 | 20 | check_inherits <- function(x, cls, arg = caller_arg(x), call = caller_env()) { 21 | if (!inherits(x, cls)) { 22 | cli::cli_abort("{.arg {arg}} must be a {.cls {cls}} object", arg = arg, call = call) 23 | } 24 | } 25 | 26 | check_file_exists <- function(x, arg = caller_arg(x), call = caller_env()) { 27 | if (!fs::file_exists(x) || fs::dir_exists(x)) { 28 | cli::cli_abort("File does not exist: {.file {x}}", arg = arg, call = call) 29 | } 30 | } 31 | 32 | check_dir_exists <- function(x, arg = caller_arg(x), call = caller_env()) { 33 | if (!fs::dir_exists(x)) { 34 | cli::cli_abort("Directory does not exist: {.path {x}}", arg = arg, call = call) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /R/condition.R: -------------------------------------------------------------------------------- 1 | to_sentence_case <- function(x) { 2 | paste0( 3 | toupper(substr(x, 1, 1)), 4 | substr(x, 2, nchar(x)), 5 | ifelse(substr(x, nchar(x), nchar(x)) == ".", "", ".") 6 | ) 7 | } 8 | 9 | stop_inja <- function(type, message, line, column) { 10 | cls <- c(paste0("jinjar_", type), "jinjar_error") 11 | message <- to_sentence_case(message) 12 | context <- "Error occurred on {.field line {line}} and {.field column {column}}." 13 | 14 | hypothesis <- if (message == "Object must be an array.") { 15 | "Have you forgotten to wrap a length-1 vector with I()?" 16 | } else { 17 | NULL 18 | } 19 | 20 | cli::cli_abort(c("{message}", "i" = hypothesis, "i" = context), class = cls, call = NULL) 21 | } 22 | 23 | stop_json <- function(message, data) { 24 | cls <- c("jinjar_json_error", "jinjar_error") 25 | context <- "JSON object: {.val {data}}" 26 | 27 | cli::cli_abort(c("{message}", "i" = context), class = cls, call = NULL) 28 | } 29 | 30 | with_catch_cpp_errors <- function(expr, call = caller_env()) { 31 | try_fetch( 32 | expr, 33 | jinjar_file_error = function(cnd) { 34 | abort("Problem encountered while reading template.", parent = cnd, call = call) 35 | }, 36 | jinjar_parser_error = function(cnd) { 37 | abort("Problem encountered while parsing template.", parent = cnd, call = call) 38 | }, 39 | jinjar_render_error = function(cnd) { 40 | abort("Problem encountered while rendering template.", parent = cnd, call = call) 41 | }, 42 | jinjar_json_error = function(cnd) { 43 | # use .internal because JSON was poorly encoded by jinjar 44 | abort("Problem encountered while decoding JSON data.", parent = cnd, call = call, .internal = TRUE) 45 | } 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /R/config.R: -------------------------------------------------------------------------------- 1 | #' Configure the templating engine 2 | #' 3 | #' Create an object to configure the templating engine behavior (e.g. customize 4 | #' the syntax). The default values have been chosen to match the Jinja defaults. 5 | #' 6 | #' @note The equivalent Jinja class is `Environment`, but this term has special 7 | #' significance in R (see [environment()]). 8 | #' 9 | #' @param loader How the engine discovers templates. Choices: 10 | #' * `NULL` (default), disables search for templates. 11 | #' * Path to template directory. 12 | #' * A [`loader`] object. 13 | #' @param block_open,block_close The opening and closing delimiters 14 | #' for control blocks. Default: \verb{"\{\%"} and \verb{"\%\}"}. 15 | #' @param variable_open,variable_close The opening and closing delimiters 16 | #' for print statements. Default: `"{{"` and `"}}"`. 17 | #' @param comment_open,comment_close The opening and closing delimiters 18 | #' for comments. Default: `"{#"` and `"#}"`. 19 | #' @param line_statement The prefix for an inline statement. If `NULL` (the 20 | #' default), inline statements are disabled. 21 | #' @param trim_blocks Remove first newline after a block. Default: `FALSE`. 22 | #' @param lstrip_blocks Remove inline whitespace before a block. Default: `FALSE`. 23 | #' @param ignore_missing_files Ignore `include` or `extends` statements when 24 | #' the auxiliary template cannot be found. If `FALSE` (default), then an error 25 | #' is raised. 26 | #' @return A `"jinjar_config"` object. 27 | #' 28 | #' @examples 29 | #' jinjar_config() 30 | #' @export 31 | jinjar_config <- function(loader = NULL, 32 | block_open = "{%", 33 | block_close = "%}", 34 | variable_open = "{{", 35 | variable_close = "}}", 36 | comment_open = "{#", 37 | comment_close = "#}", 38 | line_statement = NULL, 39 | trim_blocks = FALSE, 40 | lstrip_blocks = FALSE, 41 | ignore_missing_files = FALSE) { 42 | 43 | if (!(is_null(loader) || is_string(loader) || inherits(loader, "jinjar_loader"))) { 44 | cli::cli_abort("{.arg loader} must be NULL, a path, or a loader object", arg = "loader") 45 | } 46 | if (is.character(loader)) { 47 | loader <- path_loader(loader) 48 | } 49 | 50 | check_string(block_open) 51 | check_string(block_close) 52 | check_string(variable_open) 53 | check_string(variable_close) 54 | check_string(comment_open) 55 | check_string(comment_close) 56 | check_string(line_statement %||% "") 57 | check_bool(trim_blocks) 58 | check_bool(lstrip_blocks) 59 | check_bool(ignore_missing_files) 60 | 61 | delimiters <- c( 62 | variable_open = variable_open, 63 | variable_close = variable_close, 64 | block_open = block_open, 65 | block_close = block_close, 66 | line_statement = line_statement %||% "", 67 | comment_open = comment_open, 68 | comment_close = comment_close 69 | ) 70 | if (anyDuplicated(delimiters)) { 71 | conflicts <- delimiters[duplicated(delimiters) | duplicated(delimiters, fromLast = TRUE)] 72 | cli::cli_abort("Conflicting delimiters: {.arg {names(conflicts)}}") 73 | } 74 | 75 | structure(c(as.list(delimiters), list( 76 | loader = loader, 77 | trim_blocks = trim_blocks, 78 | lstrip_blocks = lstrip_blocks, 79 | ignore_missing_files = ignore_missing_files 80 | )), class = "jinjar_config") 81 | } 82 | 83 | #' @export 84 | print.jinjar_config <- function(x, ...) { 85 | cli::cli({ 86 | cli::cli_h1("Template configuration") 87 | 88 | if (is.null(x$loader)) { 89 | cli::cli_text("{.strong Loader:} disabled") 90 | } else { 91 | print(x$loader) 92 | } 93 | 94 | cli::cli_text( 95 | "{.strong Syntax:} ", 96 | style_block("{x$block_open} block {x$block_close}"), " ", 97 | style_variable("{x$variable_open} variable {x$variable_close}"), " ", 98 | style_comment("{x$comment_open} comment {x$comment_close}") 99 | ) 100 | }) 101 | 102 | invisible(x) 103 | } 104 | 105 | #' @rdname jinjar_config 106 | #' @export 107 | default_config <- function() { 108 | config <- getOption("jinjar.default_config") 109 | if (is.null(config)) { 110 | config <- jinjar_config() 111 | options("jinjar.default_config" = config) 112 | } 113 | 114 | config 115 | } 116 | -------------------------------------------------------------------------------- /R/cpp11.R: -------------------------------------------------------------------------------- 1 | # Generated by cpp11: do not edit by hand 2 | 3 | parse_ <- function(input, config) { 4 | .Call(`_jinjar_parse_`, input, config) 5 | } 6 | 7 | render_ <- function(input, data_json) { 8 | .Call(`_jinjar_render_`, input, data_json) 9 | } 10 | -------------------------------------------------------------------------------- /R/encode.R: -------------------------------------------------------------------------------- 1 | encode <- function(...) { 2 | data <- dots_list(..., .homonyms = "error", .check_assign = TRUE) 3 | 4 | if (!is_named(data)) { 5 | vars <- enexprs(...) 6 | unnamed <- vars[!have_name(vars)] 7 | 8 | cli::cli_abort(c( 9 | "All data variables must be named.", 10 | "x" = "Unnamed variables: {.var {unnamed}}" 11 | )) 12 | } 13 | 14 | jsonlite::toJSON( 15 | data, 16 | auto_unbox = TRUE, # length-1 vectors output as scalars 17 | no_dots = TRUE # dots reserved by template syntax 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /R/jinjar-package.R: -------------------------------------------------------------------------------- 1 | #' @import rlang 2 | #' @keywords internal 3 | "_PACKAGE" 4 | 5 | # The following block is used by usethis to automatically manage 6 | # roxygen namespace tags. Modify with care! 7 | ## usethis namespace: start 8 | #' @useDynLib jinjar, .registration = TRUE 9 | ## usethis namespace: end 10 | NULL 11 | -------------------------------------------------------------------------------- /R/knit.R: -------------------------------------------------------------------------------- 1 | #' knit engine 2 | #' 3 | #' You can include templates and their rendered output in an RMarkdown document. 4 | #' This is achieved using the `jinjar` RMarkdown engine, which reads chunk options: 5 | #' 6 | #' * `data`: A named list of variables to pass to the template. 7 | #' * `engine.opts`: A named list of engine options: 8 | #' * `lang`: A string (e.g. `"sql"`, `"html"`) to specify the syntax 9 | #' highlighting applied to the template and its rendered output. 10 | #' * `config`: An engine configuration object (see [jinjar_config()]). 11 | #' @noRd 12 | knit_jinjar <- function(options) { 13 | engine_output <- get("engine_output", envir = asNamespace("knitr")) 14 | 15 | if (identical(.Platform$GUI, "RStudio") && is.character(options$data)) { 16 | options$data <- get(options$data, envir = globalenv()) # nocov 17 | } 18 | 19 | code <- paste(options$code, collapse = "\n") 20 | out <- if (options$eval) { 21 | config <- options$engine.opts$config %||% default_config() 22 | render(code, !!!options$data, .config = config) 23 | } else "" 24 | 25 | # override styling and syntax highlighting 26 | lang <- options$engine.opts$lang 27 | options$class.source <- c(options$class.source, lang, "bg-info") 28 | options$class.output <- c(options$class.output, lang, "bg-success") 29 | options$comment <- NULL 30 | 31 | engine_output(options, code, out) 32 | } 33 | -------------------------------------------------------------------------------- /R/loader.R: -------------------------------------------------------------------------------- 1 | #' Template loaders 2 | #' 3 | #' Loaders are responsible for exposing templates to the templating engine. 4 | #' 5 | #' @return A `"jinjar_loader"` object. 6 | #' @seealso The loader is an argument to [jinjar_config()]. 7 | #' @examples 8 | #' path_loader(getwd()) 9 | #' 10 | #' package_loader("base", "demo") 11 | #' 12 | #' list_loader(list( 13 | #' header = "Title: {{ title }}", 14 | #' content = "Hello {{ person }}!" 15 | #' )) 16 | #' @name loader 17 | NULL 18 | 19 | new_loader <- function(..., .class = character()) { 20 | structure(list(...), class = c(.class, "jinjar_loader")) 21 | } 22 | 23 | #' @description `path_loader()` loads templates from a directory in the file system. 24 | #' @param ... Strings specifying path components. 25 | #' @rdname loader 26 | #' @export 27 | path_loader <- function(...) { 28 | path <- fs::path_abs(fs::path(...)) 29 | check_dir_exists(path) 30 | 31 | new_loader( 32 | path = path, 33 | .class = "path_loader" 34 | ) 35 | } 36 | 37 | #' @description `package_loader()` loads templates from a directory in an R package. 38 | #' @param package Name of the package in which to search. 39 | #' @rdname loader 40 | #' @export 41 | package_loader <- function(package, ...) { 42 | path <- fs::path_package(package, ...) 43 | check_dir_exists(path) 44 | 45 | new_loader( 46 | path = path, 47 | pkg = package, 48 | rel_path = fs::path(...), 49 | .class = c("package_loader", "path_loader") 50 | ) 51 | } 52 | 53 | #' @export 54 | print.path_loader <- function(x, ...) { 55 | cli::cli_text("{.strong Loader:} {.path {x$path}}") 56 | invisible(x) 57 | } 58 | 59 | #' @export 60 | print.package_loader <- function(x, ...) { 61 | cli::cli_text("{.strong Loader:} {.pkg {{{x$pkg}}}}/{.path {x$rel_path}}") 62 | invisible(x) 63 | } 64 | 65 | #' @description `list_loader()` loads templates from a named list. 66 | #' @param x Named list mapping template names to template sources. 67 | #' @rdname loader 68 | #' @export 69 | list_loader <- function(x) { 70 | if (!(is_bare_list(x) && is_named(x))) { 71 | cli::cli_abort("{.arg x} must be a named list", arg = "x") 72 | } 73 | 74 | do.call(new_loader, c(x, .class = "list_loader")) 75 | } 76 | 77 | #' @export 78 | print.list_loader <- function(x, ...) { 79 | csv_width <- sum(nchar(names(x))) 80 | 81 | if (csv_width <= 50) { 82 | cli::cli_text("{.strong Loader:} {.val {names(x)}}") 83 | } else { 84 | cli::cli_text("{.strong Loader:}") 85 | cli::cli_ul() 86 | for (name in names(x)) { 87 | cli::cli_li("{.val {name}}") 88 | } 89 | cli::cli_end() 90 | } 91 | 92 | invisible(x) 93 | } 94 | -------------------------------------------------------------------------------- /R/parse.R: -------------------------------------------------------------------------------- 1 | #' Parse a template 2 | #' 3 | #' Sometimes you want to render multiple copies of a template, using different 4 | #' sets of data variables. [parse_template()] returns an intermediate version 5 | #' of the template, so you can [render()] repeatedly without re-parsing the 6 | #' template syntax. 7 | #' 8 | #' @param .x The template. Choices: 9 | #' * A template string. 10 | #' * A path to a template file (use [fs::path()]). 11 | #' @param .config The engine configuration. The default matches Jinja defaults, 12 | #' but you can use [jinjar_config()] to customize things like syntax delimiters, 13 | #' whitespace control, and loading auxiliary templates. 14 | #' @return A `"jinjar_template"` object. 15 | #' 16 | #' @seealso 17 | #' * [render()] to render the final document using data variables. 18 | #' * [`print()`][print.jinjar_template()] for pretty printing. 19 | #' * `vignette("template-syntax")` describes how to write templates. 20 | #' @examples 21 | #' x <- parse_template("Hello {{ name }}!") 22 | #' 23 | #' render(x, name = "world") 24 | #' @name parse 25 | #' @export 26 | parse_template <- function(.x, .config) { 27 | UseMethod("parse_template") 28 | } 29 | 30 | #' @rdname parse 31 | #' @export 32 | parse_template.character <- function(.x, .config = default_config()) { 33 | check_string(.x) 34 | check_inherits(.config, "jinjar_config") 35 | 36 | with_catch_cpp_errors({ 37 | parsed <- parse_(.x, .config) 38 | }) 39 | 40 | structure( 41 | .x, 42 | config = .config, 43 | parsed = parsed, 44 | class = "jinjar_template" 45 | ) 46 | } 47 | 48 | #' @rdname parse 49 | #' @export 50 | parse_template.fs_path <- function(.x, .config = default_config()) { 51 | read_utf8 <- function(path) { 52 | paste(readLines(path, encoding = "UTF-8", warn = FALSE), collapse = "\n") 53 | } 54 | 55 | if (inherits(.config$loader, "path_loader")) { 56 | if (!fs::path_has_parent(.x, .config$loader$path)) { 57 | .x <- fs::path_abs(.x, .config$loader$path) 58 | } 59 | } 60 | 61 | check_file_exists(.x) 62 | 63 | parse_template(read_utf8(.x), .config) 64 | } 65 | -------------------------------------------------------------------------------- /R/print.R: -------------------------------------------------------------------------------- 1 | #' Print a template 2 | #' 3 | #' Once a template has been parsed, it can be printed with color highlighting of 4 | #' the templating blocks. 5 | #' 6 | #' @param x A parsed template (use [parse_template()]). 7 | #' @param n Number of lines to show. If `Inf`, will print all lines. Default: `10`. 8 | #' @inheritParams rlang::args_dots_empty 9 | #' @examples 10 | #' input <- ' 11 | #' 12 | #' 13 | #' {{ title }} 14 | #' 15 | #' 16 | #' 21 | #' {# a comment #} 22 | #' 23 | #' ' 24 | #' 25 | #' x <- parse_template(input) 26 | #' 27 | #' print(x) 28 | #' 29 | #' print(x, n = Inf) 30 | #' @rdname print 31 | #' @export 32 | print.jinjar_template <- function(x, ..., n = 10) { 33 | check_dots_empty() 34 | check_count(n, inf = TRUE) 35 | n_more <- 0L 36 | 37 | # truncate output 38 | if (is.finite(n)) { 39 | lines <- strsplit(x, "\n", fixed = TRUE)[[1]] 40 | n_found <- length(lines) 41 | 42 | if (n_found > n) { 43 | attrs <- attributes(x) 44 | x <- paste0(lines[1:n], collapse = "\n") 45 | attributes(x) <- attrs 46 | 47 | n_more <- n_found - n 48 | } 49 | } 50 | 51 | cli::cli_verbatim(style_template(x)) 52 | if (n_more > 0) { 53 | dots <- cli::symbol$ellipsis 54 | subtle <- cli::make_ansi_style("grey60") 55 | cli::cli_alert_info(subtle("{dots} with {n_more} more line{?s}")) 56 | } 57 | 58 | invisible(x) 59 | } 60 | 61 | #' @importFrom utils head 62 | style_template <- function(x) { 63 | if (cli::num_ansi_colors() == 1) { 64 | return(x) 65 | } 66 | 67 | config <- attr(x, "config") 68 | 69 | # find spans 70 | spans <- rbind( 71 | find_spans(x, "block", config$block_open, config$block_close), 72 | find_spans(x, "variable", config$variable_open, config$variable_close), 73 | find_spans(x, "comment", config$comment_open, config$comment_close) 74 | ) 75 | if (nchar(config$line_statement) > 0) { 76 | spans <- rbind(spans, find_spans(x, "block", config$line_statement, "\n")) 77 | } 78 | 79 | # sort spans in order of appearance 80 | spans <- spans[order(spans$ix_open),] 81 | 82 | # remove span overlaps 83 | latest_ix_close <- c(0, head(cummax(spans$ix_close), -1)) 84 | spans <- spans[spans$ix_close > latest_ix_close,] 85 | 86 | # gaps are text spans 87 | text_spans <- data.frame( 88 | type = "text", 89 | ix_open = c(1, spans$ix_close + 1), 90 | ix_close = c(spans$ix_open - 1, nchar(x)) 91 | ) 92 | text_spans <- text_spans[text_spans$ix_open <= text_spans$ix_close,] 93 | 94 | # add text spans and sort again 95 | spans <- rbind(spans, text_spans) 96 | spans <- spans[order(spans$ix_open),] 97 | 98 | # style spans 99 | output <- mapply(style_span, x, spans$type, spans$ix_open, spans$ix_close) 100 | 101 | # stitch spans 102 | paste0(output, collapse = "") 103 | } 104 | 105 | find_spans <- function(x, type, open, close) { 106 | ix_open <- gregexpr(open, x, fixed = TRUE)[[1]] 107 | ix_close <- gregexpr(close, x, fixed = TRUE)[[1]] 108 | 109 | # no spans found 110 | if (all(ix_open == -1L)) { 111 | return(data.frame(type = character(), open = integer(), close = integer())) 112 | } 113 | 114 | # ignore unmatched open/close patterns 115 | find_matching_close <- function(x) min(ix_close[ix_close > x]) 116 | ix_close_match <- vapply(ix_open, find_matching_close, FUN.VALUE = 0L) 117 | 118 | ix_open <- ix_open[c(1, diff(ix_close_match)) != 0] 119 | ix_close <- ix_close[ix_close %in% ix_close_match] 120 | ix_close <- ix_close + nchar(close) - 1 121 | 122 | data.frame(type, ix_open, ix_close) 123 | } 124 | 125 | style_span <- function(x, type, ix_open, ix_close) { 126 | txt_span <- substr(x, ix_open, ix_close) 127 | 128 | if (type == "comment") { 129 | style_comment(txt_span) 130 | } else if (type == "block") { 131 | style_block(txt_span) 132 | } else if (type == "variable") { 133 | style_variable(txt_span) 134 | } else { 135 | txt_span 136 | } 137 | } 138 | 139 | style_comment <- function(x) cli::style_italic(cli::col_grey(x)) 140 | style_block <- cli::col_blue 141 | style_variable <- cli::col_green 142 | -------------------------------------------------------------------------------- /R/render.R: -------------------------------------------------------------------------------- 1 | #' Render a template 2 | #' 3 | #' Data is passed to a template to render the final document. 4 | #' 5 | #' @param .x The template. Choices: 6 | #' * A template string. 7 | #' * A path to a template file (use [fs::path()]). 8 | #' * A parsed template (use [parse_template()]). 9 | #' @param ... <[`dynamic-dots`][rlang::dyn-dots]> Data passed to the template. 10 | #' 11 | #' By default, a length-1 vector is passed as a scalar variable. Use [I()] to 12 | #' declare that a vector should be passed as an array variable. This preserves 13 | #' a length-1 vector as an array. 14 | #' @inheritParams parse 15 | #' @return String containing rendered template. 16 | #' 17 | #' @seealso 18 | #' * [parse_template()] supports parsing a template once and rendering multiple 19 | #' times with different data variables. 20 | #' * `vignette("template-syntax")` describes how to write templates. 21 | #' @examples 22 | #' # pass data as arguments 23 | #' render("Hello {{ name }}!", name = "world") 24 | #' 25 | #' # pass length-1 vector as array 26 | #' render("Hello {{ name.0 }}!", name = I("world")) 27 | #' 28 | #' # pass data programmatically 29 | #' params <- list(name = "world") 30 | #' render("Hello {{ name }}!", !!!params) 31 | #' 32 | #' # render template file 33 | #' \dontrun{ 34 | #' render(fs::path("template.txt"), name = "world") 35 | #' } 36 | #' @export 37 | render <- function(.x, ...) { 38 | UseMethod("render") 39 | } 40 | 41 | #' @rdname render 42 | #' @export 43 | render.character <- function(.x, ..., .config = default_config()) { 44 | render(parse_template(.x, .config), ...) 45 | } 46 | 47 | #' @rdname render 48 | #' @export 49 | render.fs_path <- function(.x, ..., .config = default_config()) { 50 | render(parse_template(.x, .config), ...) 51 | } 52 | 53 | #' @rdname render 54 | #' @export 55 | render.jinjar_template <- function(.x, ...) { 56 | with_catch_cpp_errors({ 57 | render_(attr(.x, "parsed"), encode(...)) 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /R/zzz.R: -------------------------------------------------------------------------------- 1 | .onAttach <- function(...) { 2 | if (requireNamespace("knitr", quietly = TRUE)) { 3 | knit_engines <- get("knit_engines", envir = asNamespace("knitr")) 4 | knit_engines$set(jinjar = knit_jinjar) 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r, include = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | comment = "#>", 11 | fig.path = "man/figures/README-", 12 | out.width = "100%" 13 | ) 14 | ``` 15 | 16 | # jinjar 17 | 18 | 19 | [![CRAN status](https://www.r-pkg.org/badges/version/jinjar)](https://CRAN.R-project.org/package=jinjar) 20 | [![Codecov test coverage](https://codecov.io/gh/davidchall/jinjar/branch/master/graph/badge.svg)](https://app.codecov.io/gh/davidchall/jinjar?branch=master) 21 | [![R-CMD-check](https://github.com/davidchall/jinjar/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/davidchall/jinjar/actions/workflows/R-CMD-check.yaml) 22 | 23 | 24 | jinjar is a templating engine for R, inspired by the [Jinja](https://jinja.palletsprojects.com/) Python package and powered by the [inja](https://github.com/pantor/inja) C++ library. 25 | 26 | ## Installation 27 | 28 | You can install the released version of jinjar from [CRAN](https://CRAN.R-project.org) with: 29 | 30 | ``` r 31 | install.packages("jinjar") 32 | ``` 33 | 34 | Or you can install the development version from GitHub: 35 | 36 | ``` r 37 | # install.packages("remotes") 38 | remotes::install_github("davidchall/jinjar") 39 | ``` 40 | 41 | ## Usage 42 | 43 | ```{r} 44 | library(jinjar) 45 | 46 | render("Hello {{ name }}!", name = "world") 47 | ``` 48 | 49 | Here's a more advanced example using loops and conditional statements. 50 | The full list of supported syntax is described in `vignette("template-syntax")`. 51 | 52 | ```{r} 53 | template <- 'Humans of A New Hope 54 | 55 | {% for person in people -%} 56 | {% if "A New Hope" in person.films and default(person.species, "Unknown") == "Human" -%} 57 | * {{ person.name }} ({{ person.homeworld }}) 58 | {% endif -%} 59 | {% endfor -%} 60 | ' 61 | 62 | template |> 63 | render(people = dplyr::starwars) |> 64 | writeLines() 65 | ``` 66 | 67 | 68 | ## Related work 69 | 70 | An important characteristic of a templating engine is how much logic is supported. 71 | This spectrum ranges from **logic-less** templates (i.e. only variable substitution is supported) to **arbitrary code execution**. 72 | Generally speaking, logic-less templates are easier to maintain because their functionality is so restricted. 73 | But often the data doesn't align with how it should be rendered -- templating logic offers the flexibility to bridge this gap. 74 | 75 | Fortunately, we already have very popular R packages that fall on opposite ends of this spectrum: 76 | 77 | * [**whisker**](https://github.com/edwindj/whisker) -- Implements the [Mustache](https://mustache.github.io) templating syntax. This is nearly **logic-less**, though some simple control flow is supported. Mustache templates are language agnostic (i.e. can be rendered by other Mustache implementations). 78 | * [**knitr**](https://yihui.org/knitr/) and [**rmarkdown**](https://github.com/rstudio/rmarkdown) -- Allows **arbitrary code execution** to be knitted together with Markdown text content. It even supports [multiple language engines](https://bookdown.org/yihui/rmarkdown/language-engines.html) (e.g. R, Python, C++, SQL). 79 | 80 | In contrast, jinjar strikes a balance inspired by the [Jinja](https://jinja.palletsprojects.com/) Python package. 81 | It supports more complex logic than whisker, but without the arbitrary code execution of knitr. 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # jinjar 5 | 6 | 7 | 8 | [![CRAN 9 | status](https://www.r-pkg.org/badges/version/jinjar)](https://CRAN.R-project.org/package=jinjar) 10 | [![Codecov test 11 | coverage](https://codecov.io/gh/davidchall/jinjar/branch/master/graph/badge.svg)](https://app.codecov.io/gh/davidchall/jinjar?branch=master) 12 | [![R-CMD-check](https://github.com/davidchall/jinjar/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/davidchall/jinjar/actions/workflows/R-CMD-check.yaml) 13 | 14 | 15 | jinjar is a templating engine for R, inspired by the 16 | [Jinja](https://jinja.palletsprojects.com/) Python package and powered 17 | by the [inja](https://github.com/pantor/inja) C++ library. 18 | 19 | ## Installation 20 | 21 | You can install the released version of jinjar from 22 | [CRAN](https://CRAN.R-project.org) with: 23 | 24 | ``` r 25 | install.packages("jinjar") 26 | ``` 27 | 28 | Or you can install the development version from GitHub: 29 | 30 | ``` r 31 | # install.packages("remotes") 32 | remotes::install_github("davidchall/jinjar") 33 | ``` 34 | 35 | ## Usage 36 | 37 | ``` r 38 | library(jinjar) 39 | 40 | render("Hello {{ name }}!", name = "world") 41 | #> [1] "Hello world!" 42 | ``` 43 | 44 | Here’s a more advanced example using loops and conditional statements. 45 | The full list of supported syntax is described in 46 | `vignette("template-syntax")`. 47 | 48 | ``` r 49 | template <- 'Humans of A New Hope 50 | 51 | {% for person in people -%} 52 | {% if "A New Hope" in person.films and default(person.species, "Unknown") == "Human" -%} 53 | * {{ person.name }} ({{ person.homeworld }}) 54 | {% endif -%} 55 | {% endfor -%} 56 | ' 57 | 58 | template |> 59 | render(people = dplyr::starwars) |> 60 | writeLines() 61 | #> Humans of A New Hope 62 | #> 63 | #> * Luke Skywalker (Tatooine) 64 | #> * Darth Vader (Tatooine) 65 | #> * Leia Organa (Alderaan) 66 | #> * Owen Lars (Tatooine) 67 | #> * Beru Whitesun Lars (Tatooine) 68 | #> * Biggs Darklighter (Tatooine) 69 | #> * Obi-Wan Kenobi (Stewjon) 70 | #> * Wilhuff Tarkin (Eriadu) 71 | #> * Han Solo (Corellia) 72 | #> * Wedge Antilles (Corellia) 73 | #> * Raymus Antilles (Alderaan) 74 | ``` 75 | 76 | ## Related work 77 | 78 | An important characteristic of a templating engine is how much logic is 79 | supported. This spectrum ranges from **logic-less** templates (i.e. only 80 | variable substitution is supported) to **arbitrary code execution**. 81 | Generally speaking, logic-less templates are easier to maintain because 82 | their functionality is so restricted. But often the data doesn’t align 83 | with how it should be rendered – templating logic offers the flexibility 84 | to bridge this gap. 85 | 86 | Fortunately, we already have very popular R packages that fall on 87 | opposite ends of this spectrum: 88 | 89 | - [**whisker**](https://github.com/edwindj/whisker) – Implements the 90 | [Mustache](https://mustache.github.io) templating syntax. This is 91 | nearly **logic-less**, though some simple control flow is supported. 92 | Mustache templates are language agnostic (i.e. can be rendered by 93 | other Mustache implementations). 94 | - [**knitr**](https://yihui.org/knitr/) and 95 | [**rmarkdown**](https://github.com/rstudio/rmarkdown) – Allows 96 | **arbitrary code execution** to be knitted together with Markdown text 97 | content. It even supports [multiple language 98 | engines](https://bookdown.org/yihui/rmarkdown/language-engines.html) 99 | (e.g. R, Python, C++, SQL). 100 | 101 | In contrast, jinjar strikes a balance inspired by the 102 | [Jinja](https://jinja.palletsprojects.com/) Python package. It supports 103 | more complex logic than whisker, but without the arbitrary code 104 | execution of knitr. 105 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 1% 9 | informational: true 10 | patch: 11 | default: 12 | target: auto 13 | threshold: 1% 14 | informational: true 15 | 16 | ignore: 17 | - "src/inja" 18 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | ## Test environments 2 | 3 | * macOS 14 (local): 4.4 4 | * macOS 12 (GitHub Actions): 4.4 5 | * Ubuntu 22.04 (GitHub Actions): 4.0, 4.1, 4.2, 4.3, 4.4, devel 6 | * Windows Server 2022 (GitHub Actions): 4.0, 4.4 7 | * Windows Server 2022 (win-builder): devel 8 | 9 | ## R CMD check results 10 | 11 | 0 errors | 0 warnings | 0 notes 12 | 13 | ## revdepcheck results 14 | 15 | We checked 2 reverse dependencies, comparing R CMD check results across CRAN and dev versions of this package. 16 | 17 | * We saw 0 new problems 18 | * We failed to check 0 packages 19 | -------------------------------------------------------------------------------- /jinjar.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | ProjectId: 36fe346e-43c7-4a9f-9738-c3d5fde0ca9d 3 | 4 | RestoreWorkspace: No 5 | SaveWorkspace: No 6 | AlwaysSaveHistory: Default 7 | 8 | EnableCodeIndexing: Yes 9 | UseSpacesForTab: Yes 10 | NumSpacesForTab: 2 11 | Encoding: UTF-8 12 | 13 | RnwWeave: Sweave 14 | LaTeX: pdfLaTeX 15 | 16 | AutoAppendNewline: Yes 17 | StripTrailingWhitespace: Yes 18 | LineEndingConversion: Posix 19 | 20 | BuildType: Package 21 | PackageUseDevtools: Yes 22 | PackageInstallArgs: --no-multiarch --with-keep.source 23 | PackageRoxygenize: rd,collate,namespace 24 | -------------------------------------------------------------------------------- /man/figures/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/jinjar/20dd96a5477a4362c78a0afe22939e85c5e9c309/man/figures/logo.png -------------------------------------------------------------------------------- /man/figures/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | davidchall.github.io/jinjar 6 | jinjar 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /man/jinjar-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/jinjar-package.R 3 | \docType{package} 4 | \name{jinjar-package} 5 | \alias{jinjar} 6 | \alias{jinjar-package} 7 | \title{jinjar: Template Engine Inspired by 'Jinja'} 8 | \description{ 9 | \if{html}{\figure{logo.png}{options: style='float: right' alt='logo' width='120'}} 10 | 11 | Template engine powered by the 'inja' C++ library. Users write a template document, using syntax inspired by the 'Jinja' Python package, and then render the final document by passing data from R. The template syntax supports features such as variables, loops, conditions and inheritance. 12 | } 13 | \seealso{ 14 | Useful links: 15 | \itemize{ 16 | \item \url{https://davidchall.github.io/jinjar/} 17 | \item \url{https://github.com/davidchall/jinjar} 18 | \item Report bugs at \url{https://github.com/davidchall/jinjar/issues} 19 | } 20 | 21 | } 22 | \author{ 23 | \strong{Maintainer}: David Hall \email{david.hall.physics@gmail.com} (\href{https://orcid.org/0000-0002-2193-0480}{ORCID}) [copyright holder] 24 | 25 | Other contributors: 26 | \itemize{ 27 | \item Lars Berscheid (Author of bundled inja library) [copyright holder] 28 | \item Niels Lohmann (Author of bundled json library) [copyright holder] 29 | } 30 | 31 | } 32 | \keyword{internal} 33 | -------------------------------------------------------------------------------- /man/jinjar_config.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/config.R 3 | \name{jinjar_config} 4 | \alias{jinjar_config} 5 | \alias{default_config} 6 | \title{Configure the templating engine} 7 | \usage{ 8 | jinjar_config( 9 | loader = NULL, 10 | block_open = "{\%", 11 | block_close = "\%}", 12 | variable_open = "{{", 13 | variable_close = "}}", 14 | comment_open = "{#", 15 | comment_close = "#}", 16 | line_statement = NULL, 17 | trim_blocks = FALSE, 18 | lstrip_blocks = FALSE, 19 | ignore_missing_files = FALSE 20 | ) 21 | 22 | default_config() 23 | } 24 | \arguments{ 25 | \item{loader}{How the engine discovers templates. Choices: 26 | \itemize{ 27 | \item \code{NULL} (default), disables search for templates. 28 | \item Path to template directory. 29 | \item A \code{\link{loader}} object. 30 | }} 31 | 32 | \item{block_open, block_close}{The opening and closing delimiters 33 | for control blocks. Default: \verb{"\{\%"} and \verb{"\%\}"}.} 34 | 35 | \item{variable_open, variable_close}{The opening and closing delimiters 36 | for print statements. Default: \code{"{{"} and \code{"}}"}.} 37 | 38 | \item{comment_open, comment_close}{The opening and closing delimiters 39 | for comments. Default: \code{"{#"} and \code{"#}"}.} 40 | 41 | \item{line_statement}{The prefix for an inline statement. If \code{NULL} (the 42 | default), inline statements are disabled.} 43 | 44 | \item{trim_blocks}{Remove first newline after a block. Default: \code{FALSE}.} 45 | 46 | \item{lstrip_blocks}{Remove inline whitespace before a block. Default: \code{FALSE}.} 47 | 48 | \item{ignore_missing_files}{Ignore \code{include} or \code{extends} statements when 49 | the auxiliary template cannot be found. If \code{FALSE} (default), then an error 50 | is raised.} 51 | } 52 | \value{ 53 | A \code{"jinjar_config"} object. 54 | } 55 | \description{ 56 | Create an object to configure the templating engine behavior (e.g. customize 57 | the syntax). The default values have been chosen to match the Jinja defaults. 58 | } 59 | \note{ 60 | The equivalent Jinja class is \code{Environment}, but this term has special 61 | significance in R (see \code{\link[=environment]{environment()}}). 62 | } 63 | \examples{ 64 | jinjar_config() 65 | } 66 | -------------------------------------------------------------------------------- /man/loader.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/loader.R 3 | \name{loader} 4 | \alias{loader} 5 | \alias{path_loader} 6 | \alias{package_loader} 7 | \alias{list_loader} 8 | \title{Template loaders} 9 | \usage{ 10 | path_loader(...) 11 | 12 | package_loader(package, ...) 13 | 14 | list_loader(x) 15 | } 16 | \arguments{ 17 | \item{...}{Strings specifying path components.} 18 | 19 | \item{package}{Name of the package in which to search.} 20 | 21 | \item{x}{Named list mapping template names to template sources.} 22 | } 23 | \value{ 24 | A \code{"jinjar_loader"} object. 25 | } 26 | \description{ 27 | Loaders are responsible for exposing templates to the templating engine. 28 | 29 | \code{path_loader()} loads templates from a directory in the file system. 30 | 31 | \code{package_loader()} loads templates from a directory in an R package. 32 | 33 | \code{list_loader()} loads templates from a named list. 34 | } 35 | \examples{ 36 | path_loader(getwd()) 37 | 38 | package_loader("base", "demo") 39 | 40 | list_loader(list( 41 | header = "Title: {{ title }}", 42 | content = "Hello {{ person }}!" 43 | )) 44 | } 45 | \seealso{ 46 | The loader is an argument to \code{\link[=jinjar_config]{jinjar_config()}}. 47 | } 48 | -------------------------------------------------------------------------------- /man/parse.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/parse.R 3 | \name{parse} 4 | \alias{parse} 5 | \alias{parse_template} 6 | \alias{parse_template.character} 7 | \alias{parse_template.fs_path} 8 | \title{Parse a template} 9 | \usage{ 10 | parse_template(.x, .config) 11 | 12 | \method{parse_template}{character}(.x, .config = default_config()) 13 | 14 | \method{parse_template}{fs_path}(.x, .config = default_config()) 15 | } 16 | \arguments{ 17 | \item{.x}{The template. Choices: 18 | \itemize{ 19 | \item A template string. 20 | \item A path to a template file (use \code{\link[fs:path]{fs::path()}}). 21 | }} 22 | 23 | \item{.config}{The engine configuration. The default matches Jinja defaults, 24 | but you can use \code{\link[=jinjar_config]{jinjar_config()}} to customize things like syntax delimiters, 25 | whitespace control, and loading auxiliary templates.} 26 | } 27 | \value{ 28 | A \code{"jinjar_template"} object. 29 | } 30 | \description{ 31 | Sometimes you want to render multiple copies of a template, using different 32 | sets of data variables. \code{\link[=parse_template]{parse_template()}} returns an intermediate version 33 | of the template, so you can \code{\link[=render]{render()}} repeatedly without re-parsing the 34 | template syntax. 35 | } 36 | \examples{ 37 | x <- parse_template("Hello {{ name }}!") 38 | 39 | render(x, name = "world") 40 | } 41 | \seealso{ 42 | \itemize{ 43 | \item \code{\link[=render]{render()}} to render the final document using data variables. 44 | \item \code{\link[=print.jinjar_template]{print()}} for pretty printing. 45 | \item \code{vignette("template-syntax")} describes how to write templates. 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /man/print.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/print.R 3 | \name{print.jinjar_template} 4 | \alias{print.jinjar_template} 5 | \title{Print a template} 6 | \usage{ 7 | \method{print}{jinjar_template}(x, ..., n = 10) 8 | } 9 | \arguments{ 10 | \item{x}{A parsed template (use \code{\link[=parse_template]{parse_template()}}).} 11 | 12 | \item{...}{These dots are for future extensions and must be empty.} 13 | 14 | \item{n}{Number of lines to show. If \code{Inf}, will print all lines. Default: \code{10}.} 15 | } 16 | \description{ 17 | Once a template has been parsed, it can be printed with color highlighting of 18 | the templating blocks. 19 | } 20 | \examples{ 21 | input <- ' 22 | 23 | 24 | {{ title }} 25 | 26 | 27 | 32 | {# a comment #} 33 | 34 | ' 35 | 36 | x <- parse_template(input) 37 | 38 | print(x) 39 | 40 | print(x, n = Inf) 41 | } 42 | -------------------------------------------------------------------------------- /man/render.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/render.R 3 | \name{render} 4 | \alias{render} 5 | \alias{render.character} 6 | \alias{render.fs_path} 7 | \alias{render.jinjar_template} 8 | \title{Render a template} 9 | \usage{ 10 | render(.x, ...) 11 | 12 | \method{render}{character}(.x, ..., .config = default_config()) 13 | 14 | \method{render}{fs_path}(.x, ..., .config = default_config()) 15 | 16 | \method{render}{jinjar_template}(.x, ...) 17 | } 18 | \arguments{ 19 | \item{.x}{The template. Choices: 20 | \itemize{ 21 | \item A template string. 22 | \item A path to a template file (use \code{\link[fs:path]{fs::path()}}). 23 | \item A parsed template (use \code{\link[=parse_template]{parse_template()}}). 24 | }} 25 | 26 | \item{...}{<\code{\link[rlang:dyn-dots]{dynamic-dots}}> Data passed to the template. 27 | 28 | By default, a length-1 vector is passed as a scalar variable. Use \code{\link[=I]{I()}} to 29 | declare that a vector should be passed as an array variable. This preserves 30 | a length-1 vector as an array.} 31 | 32 | \item{.config}{The engine configuration. The default matches Jinja defaults, 33 | but you can use \code{\link[=jinjar_config]{jinjar_config()}} to customize things like syntax delimiters, 34 | whitespace control, and loading auxiliary templates.} 35 | } 36 | \value{ 37 | String containing rendered template. 38 | } 39 | \description{ 40 | Data is passed to a template to render the final document. 41 | } 42 | \examples{ 43 | # pass data as arguments 44 | render("Hello {{ name }}!", name = "world") 45 | 46 | # pass length-1 vector as array 47 | render("Hello {{ name.0 }}!", name = I("world")) 48 | 49 | # pass data programmatically 50 | params <- list(name = "world") 51 | render("Hello {{ name }}!", !!!params) 52 | 53 | # render template file 54 | \dontrun{ 55 | render(fs::path("template.txt"), name = "world") 56 | } 57 | } 58 | \seealso{ 59 | \itemize{ 60 | \item \code{\link[=parse_template]{parse_template()}} supports parsing a template once and rendering multiple 61 | times with different data variables. 62 | \item \code{vignette("template-syntax")} describes how to write templates. 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pkgdown/_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://davidchall.github.io/jinjar 2 | 3 | template: 4 | bootstrap: 5 5 | bslib: 6 | info: "#DDEAF4" 7 | success: "#E2EFDA" 8 | 9 | reference: 10 | - title: Templates 11 | - contents: 12 | - render 13 | - parse 14 | - print.jinjar_template 15 | - title: Configuration 16 | - contents: 17 | - jinjar_config 18 | - loader 19 | 20 | articles: 21 | - title: All vignettes 22 | navbar: ~ 23 | contents: 24 | - template-syntax 25 | - auxiliary-templates 26 | -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/jinjar/20dd96a5477a4362c78a0afe22939e85c5e9c309/pkgdown/favicon/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/jinjar/20dd96a5477a4362c78a0afe22939e85c5e9c309/pkgdown/favicon/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/jinjar/20dd96a5477a4362c78a0afe22939e85c5e9c309/pkgdown/favicon/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/jinjar/20dd96a5477a4362c78a0afe22939e85c5e9c309/pkgdown/favicon/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/jinjar/20dd96a5477a4362c78a0afe22939e85c5e9c309/pkgdown/favicon/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/jinjar/20dd96a5477a4362c78a0afe22939e85c5e9c309/pkgdown/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/jinjar/20dd96a5477a4362c78a0afe22939e85c5e9c309/pkgdown/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/jinjar/20dd96a5477a4362c78a0afe22939e85c5e9c309/pkgdown/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchall/jinjar/20dd96a5477a4362c78a0afe22939e85c5e9c309/pkgdown/favicon/favicon.ico -------------------------------------------------------------------------------- /revdep/.gitignore: -------------------------------------------------------------------------------- 1 | checks 2 | library 3 | checks.noindex 4 | library.noindex 5 | cloud.noindex 6 | data.sqlite 7 | *.html 8 | -------------------------------------------------------------------------------- /revdep/README.md: -------------------------------------------------------------------------------- 1 | # Platform 2 | 3 | |field |value | 4 | |:--------|:------------------------------------------------------------------------------------------------| 5 | |version |R version 4.4.1 (2024-06-14) | 6 | |os |macOS 15.3.1 | 7 | |system |aarch64, darwin20 | 8 | |ui |RStudio | 9 | |language |(EN) | 10 | |collate |en_US.UTF-8 | 11 | |ctype |en_US.UTF-8 | 12 | |tz |America/Chicago | 13 | |date |2025-03-12 | 14 | |rstudio |2024.12.1+563 Kousa Dogwood (desktop) | 15 | |pandoc |3.2 @ /Applications/RStudio.app/Contents/Resources/app/quarto/bin/tools/aarch64/ (via rmarkdown) | 16 | |quarto |1.5.57 @ /Applications/RStudio.app/Contents/Resources/app/quarto/bin/quarto | 17 | 18 | # Dependencies 19 | 20 | |package |old |new |Δ | 21 | |:-------|:-----|:----------|:--| 22 | |jinjar |0.3.1 |0.3.1.9000 |* | 23 | 24 | # Revdeps 25 | 26 | -------------------------------------------------------------------------------- /revdep/cran.md: -------------------------------------------------------------------------------- 1 | ## revdepcheck results 2 | 3 | We checked 2 reverse dependencies, comparing R CMD check results across CRAN and dev versions of this package. 4 | 5 | * We saw 0 new problems 6 | * We failed to check 0 packages 7 | 8 | -------------------------------------------------------------------------------- /revdep/email.yml: -------------------------------------------------------------------------------- 1 | release_date: ??? 2 | rel_release_date: ??? 3 | my_news_url: ??? 4 | release_version: ??? 5 | release_details: ??? 6 | -------------------------------------------------------------------------------- /revdep/failures.md: -------------------------------------------------------------------------------- 1 | *Wow, no problems at all. :)* -------------------------------------------------------------------------------- /revdep/problems.md: -------------------------------------------------------------------------------- 1 | *Wow, no problems at all. :)* -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | *.dll 4 | -------------------------------------------------------------------------------- /src/Makevars: -------------------------------------------------------------------------------- 1 | PKG_CPPFLAGS = -I./inja 2 | -------------------------------------------------------------------------------- /src/condition.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "condition.h" 3 | 4 | 5 | void stop_inja(const std::string& type, const std::string& message, const size_t line, const size_t column) { 6 | auto stop_inja = cpp11::package("jinjar")["stop_inja"]; 7 | stop_inja(type, message, line, column); 8 | } 9 | 10 | void stop_json(const std::string& message, const std::string& data_json) { 11 | auto stop_json = cpp11::package("jinjar")["stop_json"]; 12 | stop_json(message, data_json); 13 | } 14 | -------------------------------------------------------------------------------- /src/condition.h: -------------------------------------------------------------------------------- 1 | #ifndef __JINJAR_CONDITION__ 2 | #define __JINJAR_CONDITION__ 3 | 4 | #include 5 | 6 | void stop_inja(const std::string& type, const std::string& message, const size_t line, const size_t column); 7 | 8 | void stop_json(const std::string& message, const std::string& data_json); 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /src/cpp11.cpp: -------------------------------------------------------------------------------- 1 | // Generated by cpp11: do not edit by hand 2 | // clang-format off 3 | 4 | #include "jinjar_types.h" 5 | #include "cpp11/declarations.hpp" 6 | #include 7 | 8 | // render.cpp 9 | cpp11::external_pointer parse_(const cpp11::strings& input, const cpp11::list& config); 10 | extern "C" SEXP _jinjar_parse_(SEXP input, SEXP config) { 11 | BEGIN_CPP11 12 | return cpp11::as_sexp(parse_(cpp11::as_cpp>(input), cpp11::as_cpp>(config))); 13 | END_CPP11 14 | } 15 | // render.cpp 16 | cpp11::strings render_(cpp11::external_pointer input, const cpp11::strings& data_json); 17 | extern "C" SEXP _jinjar_render_(SEXP input, SEXP data_json) { 18 | BEGIN_CPP11 19 | return cpp11::as_sexp(render_(cpp11::as_cpp>>(input), cpp11::as_cpp>(data_json))); 20 | END_CPP11 21 | } 22 | 23 | extern "C" { 24 | static const R_CallMethodDef CallEntries[] = { 25 | {"_jinjar_parse_", (DL_FUNC) &_jinjar_parse_, 2}, 26 | {"_jinjar_render_", (DL_FUNC) &_jinjar_render_, 2}, 27 | {NULL, NULL, 0} 28 | }; 29 | } 30 | 31 | extern "C" attribute_visible void R_init_jinjar(DllInfo* dll){ 32 | R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); 33 | R_useDynamicSymbols(dll, FALSE); 34 | R_forceSymbols(dll, TRUE); 35 | } 36 | -------------------------------------------------------------------------------- /src/jinjar_types.h: -------------------------------------------------------------------------------- 1 | #include "template.h" 2 | -------------------------------------------------------------------------------- /src/loader.cpp: -------------------------------------------------------------------------------- 1 | #include "loader.h" 2 | #include 3 | #include 4 | 5 | 6 | // Loader factory ---------------------------------------------- 7 | Loader* Loader::make_loader(const cpp11::list& config) { 8 | if (Rf_isNull(config["loader"])) { 9 | return new NullLoader(); 10 | } 11 | 12 | const cpp11::list loader = config["loader"]; 13 | 14 | if (Rf_inherits(loader, "path_loader")) { 15 | return new PathLoader(loader); 16 | } else if (Rf_inherits(loader, "list_loader")) { 17 | return new ListLoader(loader); 18 | } else { 19 | cpp11::stop("Unrecognized loader object."); // # nocov 20 | } 21 | } 22 | 23 | 24 | // NullLoader --------------------------------------------------- 25 | inja::Environment NullLoader::init_environment() { 26 | inja::Environment env; 27 | env.set_search_included_templates_in_files(false); 28 | return(env); 29 | } 30 | 31 | 32 | // PathLoader --------------------------------------------------- 33 | PathLoader::PathLoader(const cpp11::list& loader) { 34 | path = cpp11::as_cpp(loader["path"]); 35 | } 36 | 37 | inja::Environment PathLoader::init_environment() { 38 | // inja expects trailing slash 39 | return(inja::Environment(path + "/")); 40 | } 41 | 42 | 43 | // ListLoader --------------------------------------------------- 44 | ListLoader::ListLoader(const cpp11::list& loader) { 45 | const cpp11::strings names = loader.names(); 46 | for (auto it=names.begin(); it!=names.end(); ++it) { 47 | templates.push_back(std::make_pair(*it, cpp11::as_cpp(loader[*it]))); 48 | } 49 | } 50 | 51 | inja::Environment ListLoader::init_environment() { 52 | inja::Environment env; 53 | env.set_search_included_templates_in_files(false); 54 | 55 | for (auto it=templates.begin(); it!=templates.end(); ++it) { 56 | inja::Template x = env.parse(it->second); 57 | env.include_template(it->first, x); 58 | } 59 | 60 | return(env); 61 | } 62 | -------------------------------------------------------------------------------- /src/loader.h: -------------------------------------------------------------------------------- 1 | #ifndef __JINJAR_LOADER__ 2 | #define __JINJAR_LOADER__ 3 | 4 | #include 5 | #include 6 | #include 7 | namespace inja { class Environment; } 8 | 9 | 10 | class Loader { 11 | public: 12 | virtual ~Loader() {}; 13 | virtual inja::Environment init_environment() = 0; 14 | static Loader* make_loader(const cpp11::list&); 15 | }; 16 | 17 | 18 | class NullLoader : public Loader { 19 | public: 20 | inja::Environment init_environment(); 21 | }; 22 | 23 | 24 | class PathLoader : public Loader { 25 | private: 26 | std::string path; 27 | public: 28 | PathLoader(const cpp11::list& loader); 29 | inja::Environment init_environment(); 30 | }; 31 | 32 | 33 | class ListLoader : public Loader { 34 | private: 35 | std::vector> templates; 36 | public: 37 | ListLoader(const cpp11::list& loader); 38 | inja::Environment init_environment(); 39 | }; 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /src/render.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "template.h" 3 | 4 | 5 | [[cpp11::register]] 6 | cpp11::external_pointer parse_(const cpp11::strings& input, 7 | const cpp11::list& config) { 8 | 9 | auto p_tmpl = new jinjar::Template(input, config); 10 | 11 | return cpp11::external_pointer(p_tmpl); 12 | } 13 | 14 | [[cpp11::register]] 15 | cpp11::strings render_(cpp11::external_pointer input, 16 | const cpp11::strings& data_json) { 17 | 18 | return input->render(data_json); 19 | } 20 | -------------------------------------------------------------------------------- /src/template.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "template.h" 3 | #include "loader.h" 4 | #include "condition.h" 5 | 6 | 7 | jinjar::Template::Template(const cpp11::strings& x, const cpp11::list& config): env(setup_environment(config)) { 8 | try { 9 | auto txt = cpp11::as_cpp(x); 10 | templ = env.parse(txt); 11 | } catch (const inja::InjaError& e) { 12 | stop_inja(e.type, e.message, e.location.line, e.location.column); 13 | } 14 | } 15 | 16 | const cpp11::strings jinjar::Template::render(const cpp11::strings& data_json) { 17 | auto data_json_str = cpp11::as_cpp(data_json); 18 | cpp11::writable::strings output; 19 | 20 | try { 21 | auto data = nlohmann::json::parse(data_json_str); 22 | auto result = env.render(templ, data); 23 | output.push_back(result); 24 | } catch (const nlohmann::json::parse_error& e) { 25 | stop_json(e.what(), data_json_str); 26 | } catch (const inja::InjaError& e) { 27 | stop_inja(e.type, e.message, e.location.line, e.location.column); 28 | } 29 | 30 | return output; 31 | } 32 | 33 | inja::Environment jinjar::Template::setup_environment(const cpp11::list& config) { 34 | if (!Rf_inherits(config, "jinjar_config")) { 35 | cpp11::stop("Found invalid engine config."); // # nocov 36 | } 37 | 38 | Loader* loader = Loader::make_loader(config); 39 | inja::Environment env = loader->init_environment(); 40 | delete loader; 41 | 42 | env.set_statement( 43 | cpp11::as_cpp(config["block_open"]), 44 | cpp11::as_cpp(config["block_close"]) 45 | ); 46 | env.set_line_statement( 47 | cpp11::as_cpp(config["line_statement"]) 48 | ); 49 | env.set_expression( 50 | cpp11::as_cpp(config["variable_open"]), 51 | cpp11::as_cpp(config["variable_close"]) 52 | ); 53 | env.set_comment( 54 | cpp11::as_cpp(config["comment_open"]), 55 | cpp11::as_cpp(config["comment_close"]) 56 | ); 57 | env.set_trim_blocks( 58 | cpp11::as_cpp(config["trim_blocks"]) 59 | ); 60 | env.set_lstrip_blocks( 61 | cpp11::as_cpp(config["lstrip_blocks"]) 62 | ); 63 | env.set_throw_at_missing_includes( 64 | !cpp11::as_cpp(config["ignore_missing_files"]) 65 | ); 66 | 67 | env.add_callback("escape_html", 1, [](inja::Arguments& args) { 68 | std::string s = args.at(0)->get(); 69 | inja::replace_substring(s, "&", "&"); 70 | inja::replace_substring(s, "<", "<"); 71 | inja::replace_substring(s, ">", ">"); 72 | inja::replace_substring(s, "\"", """); 73 | return s; 74 | }); 75 | 76 | env.add_callback("quote_sql", 1, [](inja::Arguments& args) { 77 | auto quote_sql = [](const nlohmann::json& x) { 78 | std::string out; 79 | if (x.is_string()) { 80 | out.push_back('\''); 81 | for (char c : x.get()) { 82 | if (c == '\'') { 83 | // escape single-quote with additional single-quote 84 | out.push_back('\''); 85 | } 86 | out.push_back(c); 87 | } 88 | out.push_back('\''); 89 | } else if (x.is_null()) { 90 | out = "NULL"; 91 | } else if (x.is_number()) { 92 | out = x.dump(); 93 | } else if (x.is_boolean()) { 94 | out = x.get() ? "TRUE" : "FALSE"; 95 | } else { 96 | std::string received = x.type_name(); 97 | cpp11::stop("quote_sql() expects string, numeric or boolean but received " + received); 98 | } 99 | return out; 100 | }; 101 | 102 | std::ostringstream os; 103 | const auto val = *args[0]; 104 | 105 | if (val.is_array()) { 106 | std::string sep; 107 | for (const auto& x : val) { 108 | os << sep; 109 | os << quote_sql(x); 110 | sep = ", "; 111 | } 112 | } else { 113 | os << quote_sql(val); 114 | } 115 | 116 | return os.str(); 117 | }); 118 | 119 | return env; 120 | } 121 | -------------------------------------------------------------------------------- /src/template.h: -------------------------------------------------------------------------------- 1 | #ifndef __JINJAR_TEMPLATE__ 2 | #define __JINJAR_TEMPLATE__ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | namespace jinjar { 10 | class Template { 11 | inja::Environment env; 12 | inja::Template templ; 13 | 14 | public: 15 | Template(const cpp11::strings& x, const cpp11::list& config); 16 | const cpp11::strings render(const cpp11::strings& data_json); 17 | 18 | private: 19 | static inja::Environment setup_environment(const cpp11::list& config); 20 | }; 21 | } 22 | 23 | #endif 24 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(jinjar) 3 | 4 | test_check("jinjar") 5 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/config.md: -------------------------------------------------------------------------------- 1 | # input validation works 2 | 3 | Code 4 | jinjar_config(block_open = "{{", variable_open = "{{") 5 | Condition 6 | Error in `jinjar_config()`: 7 | ! Conflicting delimiters: `variable_open` and `block_open` 8 | 9 | # default works [plain] 10 | 11 | Code 12 | print(x) 13 | Message 14 | 15 | -- Template configuration ------------------------------------------------------ 16 | Loader: disabled 17 | Syntax: {% block %} {{ variable }} {# comment #} 18 | 19 | # default works [ansi] 20 | 21 | Code 22 | print(x) 23 | Message 24 | 25 | -- Template configuration ------------------------------------------------------ 26 | Loader: disabled 27 | Syntax: {% block %} {{ variable }} {# comment #} 28 | 29 | # default works [unicode] 30 | 31 | Code 32 | print(x) 33 | Message 34 | 35 | ── Template configuration ────────────────────────────────────────────────────── 36 | Loader: disabled 37 | Syntax: {% block %} {{ variable }} {# comment #} 38 | 39 | # default works [fancy] 40 | 41 | Code 42 | print(x) 43 | Message 44 | 45 | ── Template configuration ────────────────────────────────────────────────────── 46 | Loader: disabled 47 | Syntax: {% block %} {{ variable }} {# comment #} 48 | 49 | # string loader works [plain] 50 | 51 | Code 52 | print(x) 53 | Message 54 | 55 | -- Template configuration ------------------------------------------------------ 56 | Loader: '/path/to/templates' 57 | Syntax: {% block %} {{ variable }} {# comment #} 58 | 59 | # string loader works [ansi] 60 | 61 | Code 62 | print(x) 63 | Message 64 | 65 | -- Template configuration ------------------------------------------------------ 66 | Loader: /path/to/templates 67 | Syntax: {% block %} {{ variable }} {# comment #} 68 | 69 | # string loader works [unicode] 70 | 71 | Code 72 | print(x) 73 | Message 74 | 75 | ── Template configuration ────────────────────────────────────────────────────── 76 | Loader: '/path/to/templates' 77 | Syntax: {% block %} {{ variable }} {# comment #} 78 | 79 | # string loader works [fancy] 80 | 81 | Code 82 | print(x) 83 | Message 84 | 85 | ── Template configuration ────────────────────────────────────────────────────── 86 | Loader: /path/to/templates 87 | Syntax: {% block %} {{ variable }} {# comment #} 88 | 89 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/encode.md: -------------------------------------------------------------------------------- 1 | # dynamic dots work 2 | 3 | Code 4 | encode(a = 1, a = "b") 5 | Condition 6 | Error in `encode()`: 7 | ! Arguments in `...` must have unique names. 8 | x Multiple arguments named `a` at positions 1 and 2. 9 | 10 | --- 11 | 12 | Code 13 | encode(a = 1, "b", mtcars) 14 | Condition 15 | Error in `encode()`: 16 | ! All data variables must be named. 17 | x Unnamed variables: `b` and `mtcars` 18 | 19 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/loader.md: -------------------------------------------------------------------------------- 1 | # path_loader works [plain] 2 | 3 | Code 4 | print(x) 5 | Message 6 | Loader: '/path/to/templates' 7 | 8 | # path_loader works [ansi] 9 | 10 | Code 11 | print(x) 12 | Message 13 | Loader: /path/to/templates 14 | 15 | # path_loader works [unicode] 16 | 17 | Code 18 | print(x) 19 | Message 20 | Loader: '/path/to/templates' 21 | 22 | # path_loader works [fancy] 23 | 24 | Code 25 | print(x) 26 | Message 27 | Loader: /path/to/templates 28 | 29 | # package_loader works [plain] 30 | 31 | Code 32 | print(x) 33 | Message 34 | Loader: {jinjar}/'R' 35 | 36 | # package_loader works [ansi] 37 | 38 | Code 39 | print(x) 40 | Message 41 | Loader: {jinjar}/R 42 | 43 | # package_loader works [unicode] 44 | 45 | Code 46 | print(x) 47 | Message 48 | Loader: {jinjar}/'R' 49 | 50 | # package_loader works [fancy] 51 | 52 | Code 53 | print(x) 54 | Message 55 | Loader: {jinjar}/R 56 | 57 | # list_loader works [plain] 58 | 59 | Code 60 | print(list_loader(short_names)) 61 | Message 62 | Loader: "x", "y", and "z" 63 | Code 64 | print(list_loader(long_names)) 65 | Message 66 | Loader: 67 | * "here_is_a_very_long_template_name" 68 | * "and_one_more_just_for_good_luck" 69 | 70 | # list_loader works [ansi] 71 | 72 | Code 73 | print(list_loader(short_names)) 74 | Message 75 | Loader: "x", "y", and "z" 76 | Code 77 | print(list_loader(long_names)) 78 | Message 79 | Loader: 80 | * "here_is_a_very_long_template_name" 81 | * "and_one_more_just_for_good_luck" 82 | 83 | # list_loader works [unicode] 84 | 85 | Code 86 | print(list_loader(short_names)) 87 | Message 88 | Loader: "x", "y", and "z" 89 | Code 90 | print(list_loader(long_names)) 91 | Message 92 | Loader: 93 | • "here_is_a_very_long_template_name" 94 | • "and_one_more_just_for_good_luck" 95 | 96 | # list_loader works [fancy] 97 | 98 | Code 99 | print(list_loader(short_names)) 100 | Message 101 | Loader: "x", "y", and "z" 102 | Code 103 | print(list_loader(long_names)) 104 | Message 105 | Loader: 106 | • "here_is_a_very_long_template_name" 107 | • "and_one_more_just_for_good_luck" 108 | 109 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/parse.md: -------------------------------------------------------------------------------- 1 | # printing parsed document works [plain] 2 | 3 | Code 4 | print(x, n = Inf) 5 | Message 6 | Humans of A New Hope 7 | {# put a comment here #} 8 | {% for person in people -%} 9 | {% if "A New Hope" in person.films and default(person.species, "Unknown") == "Human" -%} 10 | * {{ person.name }} ({{ person.homeworld }}) 11 | {% endif -%} 12 | {% endfor -%} 13 | 14 | --- 15 | 16 | Code 17 | print(x, n = 5) 18 | Message 19 | Humans of A New Hope 20 | {# put a comment here #} 21 | {% for person in people -%} 22 | {% if "A New Hope" in person.films and default(person.species, "Unknown") == "Human" -%} 23 | * {{ person.name }} ({{ person.homeworld }}) 24 | i ... with 2 more lines 25 | 26 | # printing parsed document works [ansi] 27 | 28 | Code 29 | print(x, n = Inf) 30 | Message 31 | Humans of A New Hope 32 | {# put a comment here #} 33 | {% for person in people -%} 34 | {% if "A New Hope" in person.films and default(person.species, "Unknown") == "Human" -%} 35 | * {{ person.name }} ({{ person.homeworld }}) 36 | {% endif -%} 37 | {% endfor -%} 38 | 39 | --- 40 | 41 | Code 42 | print(x, n = 5) 43 | Message 44 | Humans of A New Hope 45 | {# put a comment here #} 46 | {% for person in people -%} 47 | {% if "A New Hope" in person.films and default(person.species, "Unknown") == "Human" -%} 48 | * {{ person.name }} ({{ person.homeworld }}) 49 | i ... with 2 more lines 50 | 51 | # printing parsed document works [unicode] 52 | 53 | Code 54 | print(x, n = Inf) 55 | Message 56 | Humans of A New Hope 57 | {# put a comment here #} 58 | {% for person in people -%} 59 | {% if "A New Hope" in person.films and default(person.species, "Unknown") == "Human" -%} 60 | * {{ person.name }} ({{ person.homeworld }}) 61 | {% endif -%} 62 | {% endfor -%} 63 | 64 | --- 65 | 66 | Code 67 | print(x, n = 5) 68 | Message 69 | Humans of A New Hope 70 | {# put a comment here #} 71 | {% for person in people -%} 72 | {% if "A New Hope" in person.films and default(person.species, "Unknown") == "Human" -%} 73 | * {{ person.name }} ({{ person.homeworld }}) 74 | ℹ … with 2 more lines 75 | 76 | # printing parsed document works [fancy] 77 | 78 | Code 79 | print(x, n = Inf) 80 | Message 81 | Humans of A New Hope 82 | {# put a comment here #} 83 | {% for person in people -%} 84 | {% if "A New Hope" in person.films and default(person.species, "Unknown") == "Human" -%} 85 | * {{ person.name }} ({{ person.homeworld }}) 86 | {% endif -%} 87 | {% endfor -%} 88 | 89 | --- 90 | 91 | Code 92 | print(x, n = 5) 93 | Message 94 | Humans of A New Hope 95 | {# put a comment here #} 96 | {% for person in people -%} 97 | {% if "A New Hope" in person.films and default(person.species, "Unknown") == "Human" -%} 98 | * {{ person.name }} ({{ person.homeworld }}) 99 | ℹ … with 2 more lines 100 | 101 | # print spans with overlap works [plain] 102 | 103 | Code 104 | print(x) 105 | Message 106 | {# {{ this }} is a {{ comment }} #} 107 | 108 | # print spans with overlap works [ansi] 109 | 110 | Code 111 | print(x) 112 | Message 113 | {# {{ this }} is a {{ comment }} #} 114 | 115 | # print spans with overlap works [unicode] 116 | 117 | Code 118 | print(x) 119 | Message 120 | {# {{ this }} is a {{ comment }} #} 121 | 122 | # print spans with overlap works [fancy] 123 | 124 | Code 125 | print(x) 126 | Message 127 | {# {{ this }} is a {{ comment }} #} 128 | 129 | # parse error [plain] 130 | 131 | Code 132 | parse_template("Hello {{ name }!") 133 | Condition 134 | Error in `parse_template()`: 135 | ! Problem encountered while parsing template. 136 | Caused by error: 137 | ! Unexpected '}'. 138 | i Error occurred on line 1 and column 15. 139 | 140 | # parse error [ansi] 141 | 142 | Code 143 | parse_template("Hello {{ name }!") 144 | Condition 145 | Error in `parse_template()`: 146 | ! Problem encountered while parsing template. 147 | Caused by error: 148 | ! Unexpected '}'. 149 | i Error occurred on line 1 and column 15. 150 | 151 | # parse error [unicode] 152 | 153 | Code 154 | parse_template("Hello {{ name }!") 155 | Condition 156 | Error in `parse_template()`: 157 | ! Problem encountered while parsing template. 158 | Caused by error: 159 | ! Unexpected '}'. 160 | ℹ Error occurred on line 1 and column 15. 161 | 162 | # parse error [fancy] 163 | 164 | Code 165 | parse_template("Hello {{ name }!") 166 | Condition 167 | Error in `parse_template()`: 168 | ! Problem encountered while parsing template. 169 | Caused by error: 170 | ! Unexpected '}'. 171 | ℹ Error occurred on line 1 and column 15. 172 | 173 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/render.md: -------------------------------------------------------------------------------- 1 | # include tag 2 | 3 | Code 4 | render(fs::path("templates/foo"), name = "world", .config = jinjar_config( 5 | ignore_missing_files = TRUE)) 6 | Output 7 | [1] "Welcome: " 8 | 9 | --- 10 | 11 | Code 12 | render(fs::path("foo"), name = "world", .config = jinjar_config(path_loader(fs::path_wd( 13 | "templates")))) 14 | Output 15 | [1] "Welcome: Hello world!\n" 16 | 17 | --- 18 | 19 | Code 20 | render(src, name = "world", .config = list_config) 21 | Output 22 | [1] "Welcome: Hello world!" 23 | 24 | # extends tag 25 | 26 | Code 27 | render(fs::path("foo"), name = "world", .config = jinjar_config( 28 | ignore_missing_files = TRUE)) 29 | Output 30 | [1] "world" 31 | 32 | --- 33 | 34 | Code 35 | render(fs::path("foo"), name = "world", .config = jinjar_config(path_loader(fs::path_wd()))) 36 | Output 37 | [1] "Hello world!\n" 38 | 39 | --- 40 | 41 | Code 42 | render(src, name = "world", .config = list_config) 43 | Output 44 | [1] "Hello world!" 45 | 46 | # render error [plain] 47 | 48 | Code 49 | render("Hello {{ name }}!") 50 | Condition 51 | Error in `render()`: 52 | ! Problem encountered while rendering template. 53 | Caused by error: 54 | ! Variable 'name' not found. 55 | i Error occurred on line 1 and column 10. 56 | 57 | --- 58 | 59 | Code 60 | render("{% include \"missing.html\" %}") 61 | Condition 62 | Error in `render()`: 63 | ! Problem encountered while rendering template. 64 | Caused by error: 65 | ! Include 'missing.html' not found. 66 | i Error occurred on line 1 and column 12. 67 | 68 | --- 69 | 70 | Code 71 | render("{% for x in vec %}{{ x }}{% endfor %}", vec = "world") 72 | Condition 73 | Error in `render()`: 74 | ! Problem encountered while rendering template. 75 | Caused by error: 76 | ! Object must be an array. 77 | i Have you forgotten to wrap a length-1 vector with I()? 78 | i Error occurred on line 1 and column 10. 79 | 80 | # render error [ansi] 81 | 82 | Code 83 | render("Hello {{ name }}!") 84 | Condition 85 | Error in `render()`: 86 | ! Problem encountered while rendering template. 87 | Caused by error: 88 | ! Variable 'name' not found. 89 | i Error occurred on line 1 and column 10. 90 | 91 | --- 92 | 93 | Code 94 | render("{% include \"missing.html\" %}") 95 | Condition 96 | Error in `render()`: 97 | ! Problem encountered while rendering template. 98 | Caused by error: 99 | ! Include 'missing.html' not found. 100 | i Error occurred on line 1 and column 12. 101 | 102 | --- 103 | 104 | Code 105 | render("{% for x in vec %}{{ x }}{% endfor %}", vec = "world") 106 | Condition 107 | Error in `render()`: 108 | ! Problem encountered while rendering template. 109 | Caused by error: 110 | ! Object must be an array. 111 | i Have you forgotten to wrap a length-1 vector with I()? 112 | i Error occurred on line 1 and column 10. 113 | 114 | # render error [unicode] 115 | 116 | Code 117 | render("Hello {{ name }}!") 118 | Condition 119 | Error in `render()`: 120 | ! Problem encountered while rendering template. 121 | Caused by error: 122 | ! Variable 'name' not found. 123 | ℹ Error occurred on line 1 and column 10. 124 | 125 | --- 126 | 127 | Code 128 | render("{% include \"missing.html\" %}") 129 | Condition 130 | Error in `render()`: 131 | ! Problem encountered while rendering template. 132 | Caused by error: 133 | ! Include 'missing.html' not found. 134 | ℹ Error occurred on line 1 and column 12. 135 | 136 | --- 137 | 138 | Code 139 | render("{% for x in vec %}{{ x }}{% endfor %}", vec = "world") 140 | Condition 141 | Error in `render()`: 142 | ! Problem encountered while rendering template. 143 | Caused by error: 144 | ! Object must be an array. 145 | ℹ Have you forgotten to wrap a length-1 vector with I()? 146 | ℹ Error occurred on line 1 and column 10. 147 | 148 | # render error [fancy] 149 | 150 | Code 151 | render("Hello {{ name }}!") 152 | Condition 153 | Error in `render()`: 154 | ! Problem encountered while rendering template. 155 | Caused by error: 156 | ! Variable 'name' not found. 157 | ℹ Error occurred on line 1 and column 10. 158 | 159 | --- 160 | 161 | Code 162 | render("{% include \"missing.html\" %}") 163 | Condition 164 | Error in `render()`: 165 | ! Problem encountered while rendering template. 166 | Caused by error: 167 | ! Include 'missing.html' not found. 168 | ℹ Error occurred on line 1 and column 12. 169 | 170 | --- 171 | 172 | Code 173 | render("{% for x in vec %}{{ x }}{% endfor %}", vec = "world") 174 | Condition 175 | Error in `render()`: 176 | ! Problem encountered while rendering template. 177 | Caused by error: 178 | ! Object must be an array. 179 | ℹ Have you forgotten to wrap a length-1 vector with I()? 180 | ℹ Error occurred on line 1 and column 10. 181 | 182 | # JSON encoding error [plain] 183 | 184 | Code 185 | jinjar:::with_catch_cpp_errors({ 186 | jinjar:::render_(attr(x, "parsed"), "{\"name\": \"world\"]}") 187 | }) 188 | Condition 189 | Error: 190 | ! Problem encountered while decoding JSON data. 191 | i This is an internal error that was detected in the jinjar package. 192 | Please report it at with a reprex () and the full backtrace. 193 | Caused by error: 194 | ! [json.exception.parse_error.101] parse error at line 1, column 17: syntax error while parsing object - unexpected ']'; expected '}' 195 | i JSON object: "{\"name\": \"world\"]}" 196 | 197 | # JSON encoding error [ansi] 198 | 199 | Code 200 | jinjar:::with_catch_cpp_errors({ 201 | jinjar:::render_(attr(x, "parsed"), "{\"name\": \"world\"]}") 202 | }) 203 | Condition 204 | Error: 205 | ! Problem encountered while decoding JSON data. 206 | i This is an internal error that was detected in the jinjar package. 207 | Please report it at  with a reprex () and the full backtrace. 208 | Caused by error: 209 | ! [json.exception.parse_error.101] parse error at line 1, column 17: syntax error while parsing object - unexpected ']'; expected '}' 210 | i JSON object: "{\"name\": \"world\"]}" 211 | 212 | # JSON encoding error [unicode] 213 | 214 | Code 215 | jinjar:::with_catch_cpp_errors({ 216 | jinjar:::render_(attr(x, "parsed"), "{\"name\": \"world\"]}") 217 | }) 218 | Condition 219 | Error: 220 | ! Problem encountered while decoding JSON data. 221 | ℹ This is an internal error that was detected in the jinjar package. 222 | Please report it at with a reprex () and the full backtrace. 223 | Caused by error: 224 | ! [json.exception.parse_error.101] parse error at line 1, column 17: syntax error while parsing object - unexpected ']'; expected '}' 225 | ℹ JSON object: "{\"name\": \"world\"]}" 226 | 227 | # JSON encoding error [fancy] 228 | 229 | Code 230 | jinjar:::with_catch_cpp_errors({ 231 | jinjar:::render_(attr(x, "parsed"), "{\"name\": \"world\"]}") 232 | }) 233 | Condition 234 | Error: 235 | ! Problem encountered while decoding JSON data. 236 | ℹ This is an internal error that was detected in the jinjar package. 237 | Please report it at  with a reprex () and the full backtrace. 238 | Caused by error: 239 | ! [json.exception.parse_error.101] parse error at line 1, column 17: syntax error while parsing object - unexpected ']'; expected '}' 240 | ℹ JSON object: "{\"name\": \"world\"]}" 241 | 242 | -------------------------------------------------------------------------------- /tests/testthat/helper.R: -------------------------------------------------------------------------------- 1 | expect_success <- function(x) expect_error(x, NA) 2 | 3 | # copied from fs package 4 | with_dir_tree <- function(files, code, base = tempfile()) { 5 | if (is.null(names(files))) { 6 | names(files) <- rep("", length(files)) 7 | } 8 | dirs <- dirname(names(files)) 9 | unnamed <- dirs == "" 10 | dirs[unnamed] <- files[unnamed] 11 | files[unnamed] <- list(NULL) 12 | 13 | fs::dir_create(fs::path(base, dirs)) 14 | old_wd <- setwd(base) 15 | on.exit({ 16 | unlink(base, recursive = TRUE, force = TRUE) 17 | setwd(old_wd) 18 | }) 19 | 20 | for (i in seq_along(files)) { 21 | if (!is.null(files[[i]])) { 22 | writeLines(files[[i]], con = names(files)[[i]]) 23 | } 24 | } 25 | force(code) 26 | } 27 | -------------------------------------------------------------------------------- /tests/testthat/knit-engine.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "knit engine" 3 | output: html_document 4 | --- 5 | 6 | ```{r setup} 7 | library(jinjar) 8 | ``` 9 | 10 | ```{r} 11 | contents <- list( 12 | navigation = data.frame( 13 | caption = c("Home", "Blog"), 14 | href = c("index.html", "blog.html") 15 | ), 16 | title = "My Webpage" 17 | ) 18 | ``` 19 | 20 | ```{jinjar, jinjar_lang="html", data=contents} 21 | 22 | 23 | 24 | {{ title }} 25 | 26 | 27 | 32 | 33 | 34 | ``` 35 | -------------------------------------------------------------------------------- /tests/testthat/test-config.R: -------------------------------------------------------------------------------- 1 | test_that("input validation works", { 2 | expect_error(jinjar_config(loader = 2)) 3 | expect_error(jinjar_config(block_open = "")) 4 | expect_error(jinjar_config(block_open = NA_character_)) 5 | expect_error(jinjar_config(trim_blocks = NA)) 6 | expect_snapshot(jinjar_config(block_open = "{{", variable_open = "{{"), error = TRUE) 7 | }) 8 | 9 | cli::test_that_cli("default works", { 10 | x <- jinjar_config() 11 | expect_s3_class(x, "jinjar_config") 12 | expect_null(x$loader) 13 | expect_snapshot(print(x)) 14 | }) 15 | 16 | cli::test_that_cli("string loader works", { 17 | test_path <- fs::path_home_r() 18 | 19 | x <- jinjar_config(test_path) 20 | expect_s3_class(x, "jinjar_config") 21 | expect_equal(x$loader, path_loader(test_path)) 22 | expect_snapshot( 23 | print(x), 24 | transform = function(x) gsub(test_path, "/path/to/templates", x, fixed = TRUE) 25 | ) 26 | }) 27 | -------------------------------------------------------------------------------- /tests/testthat/test-encode.R: -------------------------------------------------------------------------------- 1 | test_that("dynamic dots work", { 2 | expect_snapshot(encode(a = 1, a = "b"), error = TRUE) 3 | expect_snapshot(encode(a = 1, "b", mtcars), error = TRUE) 4 | 5 | res <- jsonlite::toJSON(list(a = 1), auto_unbox = TRUE) 6 | expect_equal(encode(a = 1), res) 7 | expect_equal(encode(!!!list(a = 1)), res) 8 | }) 9 | -------------------------------------------------------------------------------- /tests/testthat/test-knit.R: -------------------------------------------------------------------------------- 1 | test_that("knit engine works", { 2 | if (!rmarkdown::pandoc_available()) 3 | skip("rmarkdown requires pandoc") 4 | 5 | expect_success(rmarkdown::render( 6 | "knit-engine.Rmd", 7 | output_file = tempfile(fileext = ".html"), 8 | quiet = TRUE 9 | )) 10 | }) 11 | -------------------------------------------------------------------------------- /tests/testthat/test-loader.R: -------------------------------------------------------------------------------- 1 | cli::test_that_cli("path_loader works", { 2 | expect_error(path_loader("unknown")) 3 | 4 | test_path <- fs::path_home_r() 5 | 6 | x <- path_loader(test_path) 7 | expect_s3_class(x, c("path_loader", "jinjar_loader")) 8 | expect_equal(x$path, test_path) 9 | expect_snapshot( 10 | print(x), 11 | transform = function(x) gsub(test_path, "/path/to/templates", x, fixed = TRUE) 12 | ) 13 | }) 14 | 15 | cli::test_that_cli("package_loader works", { 16 | expect_error(package_loader("unknown")) 17 | 18 | x <- package_loader("jinjar", "R") 19 | expect_s3_class(x, c("package_loader", "path_loader", "jinjar_loader")) 20 | expect_equal(x$path, fs::path_package("jinjar", "R")) 21 | expect_snapshot(print(x)) 22 | }) 23 | 24 | cli::test_that_cli("list_loader works", { 25 | expect_error(list_loader(list())) 26 | 27 | x <- list_loader(list("a" = "b")) 28 | expect_s3_class(x, c("list_loader", "jinjar_loader")) 29 | expect_equal(x$a, "b") 30 | 31 | short_names <- list(x = "a", y = "b", z = "c") 32 | long_names <- list( 33 | "here_is_a_very_long_template_name" = "a", 34 | "and_one_more_just_for_good_luck" = "b" 35 | ) 36 | expect_snapshot({ 37 | print(list_loader(short_names)) 38 | print(list_loader(long_names)) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /tests/testthat/test-parse.R: -------------------------------------------------------------------------------- 1 | test_that("input validation works", { 2 | expect_error(parse_template("Hey", .config = TRUE)) 3 | }) 4 | 5 | test_that("storing parsed document works", { 6 | x <- parse_template("Hello {{ name }}!") 7 | 8 | expect_s3_class(x, "jinjar_template") 9 | 10 | expect_equal(render(x, name = "world"), "Hello world!") 11 | expect_equal(render(x, name = "David"), "Hello David!") 12 | }) 13 | 14 | cli::test_that_cli("printing parsed document works", { 15 | template <- 'Humans of A New Hope 16 | {# put a comment here #} 17 | {% for person in people -%} 18 | {% if "A New Hope" in person.films and default(person.species, "Unknown") == "Human" -%} 19 | * {{ person.name }} ({{ person.homeworld }}) 20 | {% endif -%} 21 | {% endfor -%} 22 | ' 23 | 24 | x <- parse_template(template) 25 | expect_snapshot(print(x, n = Inf)) 26 | expect_snapshot(print(x, n = 5)) 27 | 28 | expect_error(print(x, n = 2.5)) 29 | }) 30 | 31 | cli::test_that_cli("print spans with overlap works", { 32 | tmpl <- "{# {{ this }} is a {{ comment }} #}" 33 | x <- parse_template(tmpl) 34 | expect_snapshot(print(x)) 35 | }) 36 | 37 | cli::test_that_cli("parse error", { 38 | expect_snapshot(parse_template("Hello {{ name }!"), error = TRUE) 39 | }) 40 | -------------------------------------------------------------------------------- /tests/testthat/test-render.R: -------------------------------------------------------------------------------- 1 | test_that("input validation works", { 2 | expect_error(render()) 3 | }) 4 | 5 | test_that("templating features work", { 6 | expect_equal( 7 | render("Hello {{ name }}!", name = "world"), 8 | "Hello world!" 9 | ) 10 | 11 | expect_equal( 12 | render("{Hello {{ name }}!}", name = "world"), 13 | "{Hello world!}" 14 | ) 15 | }) 16 | 17 | test_that("template files work", { 18 | with_dir_tree(list("foo" = "Hello {{ name }}!"), { 19 | path_config <- jinjar_config(fs::path_wd()) 20 | 21 | expect_equal( 22 | render(fs::path("foo"), name = "world"), 23 | "Hello world!" 24 | ) 25 | expect_equal( 26 | render(fs::path_wd("foo"), name = "world"), 27 | "Hello world!" 28 | ) 29 | expect_equal( 30 | render(fs::path("foo"), name = "world", .config = path_config), 31 | "Hello world!" 32 | ) 33 | expect_equal( 34 | render(fs::path_wd("foo"), name = "world", .config = path_config), 35 | "Hello world!" 36 | ) 37 | expect_error(render(fs::path_home_r("foo"))) 38 | }) 39 | }) 40 | 41 | test_that("include tag", { 42 | src <- "Welcome: {% include \"bar\" %}" 43 | aux <- "Hello {{ name }}!" 44 | 45 | with_dir_tree(list( 46 | "templates/foo" = src, 47 | "templates/bar" = aux 48 | ), { 49 | 50 | expect_error(render(fs::path("foo"), name = "world")) 51 | expect_snapshot(render( 52 | fs::path("templates/foo"), 53 | name = "world", 54 | .config = jinjar_config(ignore_missing_files = TRUE) 55 | )) 56 | expect_snapshot(render( 57 | fs::path("foo"), 58 | name = "world", 59 | .config = jinjar_config(path_loader(fs::path_wd("templates"))) 60 | )) 61 | }) 62 | 63 | list_config <- jinjar_config(list_loader(list( 64 | "bar" = aux 65 | ))) 66 | expect_error(render(src, name = "world")) 67 | expect_snapshot(render(src, name = "world", .config = list_config)) 68 | }) 69 | 70 | test_that("extends tag", { 71 | src <- "{% extends \"bar\" %}{% block name %}world{% endblock %}" 72 | aux <- "Hello {% block name %}{% endblock %}!" 73 | 74 | with_dir_tree(list( 75 | "foo" = src, 76 | "bar" = aux 77 | ), { 78 | expect_error(render(fs::path("foo"), name = "world")) 79 | expect_snapshot(render( 80 | fs::path("foo"), 81 | name = "world", 82 | .config = jinjar_config(ignore_missing_files = TRUE) 83 | )) 84 | expect_snapshot(render( 85 | fs::path("foo"), 86 | name = "world", 87 | .config = jinjar_config(path_loader(fs::path_wd())) 88 | )) 89 | }) 90 | 91 | list_config <- jinjar_config(list_loader(list( 92 | "bar" = aux 93 | ))) 94 | expect_error(render(src, name = "world")) 95 | expect_snapshot(render(src, name = "world", .config = list_config)) 96 | }) 97 | 98 | test_that("escape_html() works", { 99 | expect_equal( 100 | render("{{ escape_html(x) }}", x = '&<>"&'), 101 | "&<>"&" 102 | ) 103 | }) 104 | 105 | test_that("quote_sql() works", { 106 | expect_equal( 107 | render("WHERE x = {{ quote_sql(col) }}", col = "world"), 108 | "WHERE x = 'world'" 109 | ) 110 | expect_equal( 111 | render("WHERE x = {{ quote_sql(col) }}", col = "Wayne's World"), 112 | "WHERE x = 'Wayne''s World'" 113 | ) 114 | expect_equal( 115 | render("WHERE x = {{ quote_sql(col) }}", col = 1L), 116 | "WHERE x = 1" 117 | ) 118 | expect_equal( 119 | render("WHERE x = {{ quote_sql(col) }}", col = 2.5), 120 | "WHERE x = 2.5" 121 | ) 122 | expect_equal( 123 | render("WHERE x = {{ quote_sql(col) }}", col = TRUE), 124 | "WHERE x = TRUE" 125 | ) 126 | expect_equal( 127 | render("WHERE x = {{ quote_sql(col) }}", col = FALSE), 128 | "WHERE x = FALSE" 129 | ) 130 | expect_equal( 131 | render("WHERE x = {{ quote_sql(col) }}", col = NA), 132 | "WHERE x = NULL" 133 | ) 134 | expect_equal( 135 | render("WHERE x IN ({{ quote_sql(col) }})", col = c("world", "galaxy")), 136 | "WHERE x IN ('world', 'galaxy')" 137 | ) 138 | expect_equal( 139 | render("WHERE x IN ({{ quote_sql(col) }})", col = c(1, 4, 6)), 140 | "WHERE x IN (1, 4, 6)" 141 | ) 142 | }) 143 | 144 | cli::test_that_cli("render error", { 145 | expect_snapshot(render("Hello {{ name }}!"), error = TRUE) 146 | expect_snapshot(render('{% include "missing.html" %}'), error = TRUE) 147 | 148 | expect_snapshot( 149 | render("{% for x in vec %}{{ x }}{% endfor %}", vec = "world"), 150 | error = TRUE 151 | ) 152 | }) 153 | 154 | cli::test_that_cli("JSON encoding error", { 155 | x <- parse_template("Hello {{ name }}!") 156 | 157 | expect_snapshot(jinjar:::with_catch_cpp_errors({ 158 | jinjar:::render_(attr(x, "parsed"), '{"name": "world"]}') 159 | }), error = TRUE) 160 | }) 161 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | -------------------------------------------------------------------------------- /vignettes/auxiliary-templates.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Auxiliary Templates" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Auxiliary Templates} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include=FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>" 14 | ) 15 | ``` 16 | 17 | ```{r setup, include=FALSE} 18 | library(jinjar) 19 | ``` 20 | 21 | As the complexity of your templating project grows, it can be helpful to refactor common pieces into auxiliary templates so they can be reused. 22 | The [`{% include %}`](#include) and [`{% extends %}`](#extends) tags support two very different approaches to this, described below. 23 | 24 | 25 | ## Loading Auxiliary Templates 26 | 27 | When your main template makes reference to auxiliary templates, you'll need to specify how the templating engine can find these auxiliary templates. 28 | This is achieved using a template loader (see `help("loader")`). 29 | 30 | There are different types of loader, but `path_loader()` is the most commonly used. 31 | This allows you to specify the directory where auxiliary templates are stored in files. 32 | 33 | Imagine you have a main template that uses nested template inheritance (`content` inherits from `blog_post.html`, which in turns inherits from `base.html`). 34 | You might store these two auxiliary templates in a templates directory: 35 | 36 | ```shell 37 | /path/to/templates/ 38 | |-- base.html 39 | |-- blog_post.html 40 | ``` 41 | 42 | When rendering the main template, you create the loader object as part of the engine configuration: 43 | 44 | ```{r, eval=FALSE} 45 | config <- jinjar_config(loader = path_loader("path", "to", "templates")) 46 | output <- render(content, !!!data, .config = config) 47 | ``` 48 | 49 | 50 | 51 | ## Template Inclusion {#include} 52 | 53 | The `include` tag can be used to include an auxiliary template and return the rendered contents of that file into the main document. 54 | Included templates have access to the same variables as the main template. 55 | 56 | By default, an error is raised if the included template cannot be found. 57 | You can ignore these errors by setting the `ignore_missing_files` argument in `jinjar_config()`. 58 | 59 | As an example, we create an auxiliary file `header.html` with contents: 60 | 61 | ```{cat, engine.opts=list(file="header.html", lang="html")} 62 | 63 | 64 | 65 | My webpage 66 | 67 | ``` 68 | 69 | and an auxiliary file `footer.html` with contents: 70 | 71 | ```{cat, engine.opts=list(file="footer.html", lang="html")} 72 | 73 | ``` 74 | 75 | And then the main template is rendered as: 76 | 77 | ```{jinjar, engine.opts=list(lang="html", config=jinjar_config(getwd()))} 78 | {% include "header.html" %} 79 | 80 | Body 81 | 82 | {% include "footer.html" %} 83 | ``` 84 | 85 | ```{r clean_include, include=FALSE} 86 | unlink(c("header.html", "footer.html")) 87 | ``` 88 | 89 | 90 | ## Template Inheritance {#extends} 91 | 92 | Template inheritance allows you to build a base "skeleton" template that contains all the common elements of your document and defines _blocks_ that child templates can override. 93 | This is a very powerful technique. 94 | 95 | As an example, consider the following base template stored in `base.html`: 96 | 97 | ```{cat, engine.opts=list(file="base.html", lang="html")} 98 | 99 | 100 | 101 | {% block head -%} 102 | 103 | {% block title %}{% endblock %} - My Webpage 104 | {% endblock %} 105 | 106 | 107 |
{% block content %}{% endblock %}
108 | 109 | 110 | ``` 111 | 112 | This base template declares three `{% block %}` tags that child templates can fill in: `head`, `title` and `content`. 113 | Note that the base template itself defines some content for the `head` block -- we'll show how a child template can use this below. 114 | 115 | A child template uses the `{% extends %}` tag to declare which parent template it builds upon. 116 | This should be the first tag in the child template, so the templating engine knows it must locate the parent template when rendering. 117 | 118 | Building upon the base template example above, a child template might look like this: 119 | 120 | ```{jinjar, engine.opts=list(lang="html", config=jinjar_config(getwd()))} 121 | {% extends "base.html" %} 122 | {% block title %}Index{% endblock %} 123 | {% block head %} 124 | {{ super() }} 125 | 128 | {% endblock %} 129 | {% block content %} 130 |

Index

131 |

132 | Welcome to my blog! 133 |

134 | {% endblock %} 135 | ``` 136 | 137 | This child template defines the three blocks declared by the parent template: `head`, `title` and `content`. 138 | In the case of the `head` block, it uses `{{ super() }}` to render the contents of the `head` block defined by the parent template. 139 | If we were using nested `extends` tags, we could pass an argument to skip levels in the inheritance tree (e.g. `{{ super(2) }}`). 140 | 141 | ```{r clean_extends, include=FALSE} 142 | unlink(c("base.html")) 143 | ``` 144 | -------------------------------------------------------------------------------- /vignettes/template-syntax.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Template Syntax" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Template Syntax} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include=FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>" 14 | ) 15 | ``` 16 | 17 | This vignette describes the template syntax supported by jinjar, following the structure of the Jinja [Template Designer Documentation](https://jinja.palletsprojects.com/templates/). 18 | It is designed to act as a reference when writing templates. 19 | 20 | The jinjar R package is powered by the [inja](https://github.com/pantor/inja) C++ library. 21 | The syntax is very similar to that of the Jinja Python package, but there are also many differences. 22 | Unfortunately, this means jinjar is not a drop-in replacement for Jinja -- you might need to adapt existing Jinja templates for the jinjar engine. 23 | 24 | The most fundamental difference between jinjar and Jinja is: 25 | 26 | * Jinja variables support direct interaction with the underlying Python objects. 27 | * jinjar variables are simple [JSON data types](https://www.w3schools.com/js/js_json_datatypes.asp). The underlying R objects are translated to JSON. 28 | 29 | This is described in more detail in the [Variables](#variables) section below. 30 | 31 | Before starting, let's create a few R objects for rendering example templates. 32 | 33 | ```{r setup} 34 | library(jinjar) 35 | 36 | # length-1 vector 37 | title <- "My Webpage" 38 | 39 | # vector 40 | users <- c("User A", "User B", "User C") 41 | 42 | # list 43 | godzilla <- list( 44 | Name = "Godzilla", 45 | Born = 1952, 46 | Birthplace = "Japan" 47 | ) 48 | 49 | # data frame 50 | navigation <- data.frame( 51 | caption = c("Home", "Blog"), 52 | href = c("index.html", "blog.html") 53 | ) 54 | 55 | # HTML special characters 56 | name <- 'Dwayne "The Rock" Johnson' 57 | ``` 58 | 59 | ```{r, include=FALSE} 60 | params <- list( 61 | title = title, 62 | users = users, 63 | godzilla = godzilla, 64 | navigation = navigation, 65 | name = name 66 | ) 67 | ``` 68 | 69 | 70 | ## Synopsis 71 | 72 | A jinjar template is simply a text file, and when rendered the output is also a text file (e.g. HTML, SQL, LaTeX). 73 | 74 | A template contains **variables** and/or **expressions**, which get replaced with values when a template is rendered; and **tags**, which control the logic of the template. 75 | 76 | Below is a minimal template that illustrates a few basics using the default jinjar configuration. 77 | We will cover the details later in this document: 78 | 79 | ```{jinjar, data=params, engine.opts=list(lang="html")} 80 | 81 | 82 | 83 | {{ title }} 84 | 85 | 86 | 91 | {# a comment #} 92 | 93 | 94 | ``` 95 | 96 | The following example shows the default configuration settings, but you can adjust the syntax configuration as desired using `jinjar_config()`. 97 | 98 | There are a few kinds of delimiters. The default delimiters are configured as follows: 99 | 100 | * `{% ... %}` for [Statements](#control-structures) 101 | * `{{ ... }}` for [Expressions](#expressions) to print to the template output 102 | * `{# ... #}` for [Comments](#comments) not included in the template output 103 | 104 | [Line Statements](#line-statements) are also possible, though they don’t have default prefix characters. 105 | To use them, set `line_statement` when creating the `jinjar_config()`. 106 | 107 | 108 | ## Variables {#variables} 109 | 110 | When writing a template, we refer to variables that act as data placeholders. 111 | We define their values when rendering the template. 112 | 113 | Although we pass R objects to `render()`, it is helpful to understand that these are encoded as JSON objects before the template is rendered. 114 | 115 | | R object | JSON object | Template example | 116 | |:----------------|:-----------------|:------------------| 117 | | Length-1 vector | Scalar | `{{ foo }}` | 118 | | Vector | Array | `{{ foo.1 }}` | 119 | | List | Object | `{{ foo.bar }}` | 120 | | Data frame | Array of objects | `{{ foo.1.bar }}` | 121 | 122 | You can use dot (`.`) notation to access data nested within a variable. 123 | An array element is accessed by its numeric **zero-based** index (e.g. `foo.1`) and an object value is accessed by its key (e.g. `foo.bar`). 124 | 125 | **Note:** In R, the dot is a valid character in an object name (e.g. `my.data`). 126 | However, this causes ambiguity when accessing nested data values. 127 | For this reason, each dot is replaced with an underscore when the data is encoded as JSON (e.g. `my.data` becomes `my_data`). 128 | 129 | **Note:** In R, a scalar is indistinguishable from a length-1 vector. 130 | This creates an ambiguity when passing R data to the template, because template variables support both scalars and arrays. 131 | You can explicitly pass a length-1 vector as an array using the `I()` operator (see `help("render")`). 132 | 133 | The double-brace syntax is used to print the value of the variable (e.g. `{{ foo }}`). 134 | To use the variable in other contexts (e.g. control structures), then these braces are omitted (e.g. `{% for bar in foo %}`). 135 | 136 | If a template variable has not been defined, then an error occurs. 137 | However, you can use the `default(foo, bar)` function to specify a fallback value. 138 | 139 | 140 | ## Comments {#comments} 141 | 142 | To comment-out some lines, preventing them from appearing in the rendered document, use the comment syntax (default: `{# ... #}`). 143 | This is useful for debugging or documenting the template. 144 | 145 | ```{jinjar} 146 | Hello{# TODO: update this #}! 147 | ``` 148 | 149 | 150 | ## Whitespace Control {#whitespace} 151 | 152 | In the default configuration, whitespace (e.g. spaces, tabs, newlines) is left unchanged in the rendered output. 153 | For example, in the default configuration we get: 154 | 155 | ```{jinjar, engine.opts=list(lang="html")} 156 |
157 | {% if true %} 158 | yay 159 | {% endif %} 160 |
161 | ``` 162 | 163 | By setting `trim_blocks = TRUE` when creating the `jinjar_config()`, the first newline after a control block is automatically removed. 164 | Setting `lstrip_blocks = TRUE` removes any whitespace from the beginning of the line until the start of each block. 165 | With both options enabled, the above example becomes: 166 | 167 | ```{jinjar, engine.opts=list(lang="html", config=jinjar_config(trim_blocks=TRUE, lstrip_blocks=TRUE))} 168 |
169 | {% if true %} 170 | yay 171 | {% endif %} 172 |
173 | ``` 174 | 175 | Instead of changing the global configuration, you can manually trim whitespace at a more finegrained level. 176 | 177 | * By putting a minus sign (`-`) after the opening delimiter, this removes any whitespace from the beginning of the line until the start of the block (i.e. the same as the `lstrip_blocks` feature). 178 | * By putting a minus sign (`-`) before the closing delimiter, this removes any whitespace (including newlines) until the next non-whitespace character (i.e. slightly different from the `trim_blocks` feature). 179 | 180 | This can be activated for control blocks, comments, or variable expressions: 181 | 182 | ```{jinjar, engine.opts=list(lang="html")} 183 |
184 | {% if true -%} 185 | yay 186 | {%- endif -%} 187 |
188 | ``` 189 | 190 | 191 | ## Line Statements {#line-statements} 192 | 193 | If line statements are enabled (see `jinjar_config()`), it’s possible to mark a line as a statement. 194 | For example, if the line statement prefix is configured to `#`, you can do: 195 | 196 | ```{jinjar, data=params, engine.opts=list(lang="html", config=jinjar_config(line_statement="#"))} 197 | 202 | ``` 203 | 204 | 205 | ## Control Structures {#control-structures} 206 | 207 | A control structure refers to all those things that control the flow of a program. 208 | With the default syntax, control structures appear inside `{% ... %}` blocks. 209 | 210 | 211 | ### For 212 | 213 | A for-loop allows you to iterate over each element in a vector: 214 | 215 | ```{jinjar, data=params, engine.opts=list(lang="markdown")} 216 | {% for user in users -%} 217 | {{ loop.index1 }}. {{ user }} 218 | {%- endfor -%} 219 | ``` 220 | 221 | or loop over key-value pairs in a named list: 222 | 223 | ```{jinjar, data=params, engine.opts=list(lang="html")} 224 |
225 | {% for key, value in godzilla %} 226 |
{{ key }}
227 |
{{ value }}
228 | {% endfor -%} 229 |
230 | ``` 231 | 232 | As described in [Variables](#variables), a data frame is translated to an array of JSON objects. 233 | Therefore a nested combination of the above two loops could theoretically be used. 234 | In practice, it is much more common to iterate over rows and access the individual elements by their attributes: 235 | 236 | ```{jinjar, data=params, engine.opts=list(lang="html", config=jinjar_config(line_statement="#"))} 237 | 242 | ``` 243 | 244 | While inside a for-loop block, you can access some special variables: 245 | 246 | | Variable | Description | 247 | |:----------------|:------------| 248 | | `loop.index` | The current iteration (0-based). | 249 | | `loop.index1` | The current iteration (1-based). | 250 | | `loop.is_first` | True if first iteration. | 251 | | `loop.is_last` | True if last iteration. | 252 | | `loop.parent` | In nested loops, the parent loop variable. | 253 | 254 | 255 | ### If 256 | 257 | Conditional branches are written using `if`, `else if` and `else` statements, which evaluate [Expressions](#expressions). 258 | 259 | ```{jinjar, data=params, engine.opts=list(lang="markdown")} 260 | {% if length(users) > 5 -%} 261 | {% for user in users -%} 262 | * {{ user }} 263 | {% endfor %} 264 | {% else if length(users) > 0 -%} 265 | {{ join(users, ", ") }}. 266 | {% else -%} 267 | No users found. 268 | {% endif %} 269 | ``` 270 | 271 | 272 | ### Assignments 273 | 274 | Using the `set` statement, you can assign values to variables. 275 | 276 | ```{jinjar} 277 | {% set name="world" -%} 278 | Hello {{ name }}! 279 | ``` 280 | 281 | 282 | ### Extends 283 | 284 | The `extends` tag can be used for template inheritance. 285 | See _Template Inheritance_ in `vignette("auxiliary-templates")`. 286 | 287 | 288 | ### Include 289 | 290 | The `include` tag inserts the rendered contents of an auxiliary template. 291 | See _Template Inclusion_ in `vignette("auxiliary-templates")`. 292 | 293 | 294 | ## Expressions {#expressions} 295 | 296 | Basic expressions are supported in templates. 297 | 298 | ### Literals 299 | 300 | The simplest form of expressions are literals, which represent fixed values. 301 | 302 | As described in [Variables](#variables), the template is rendered using data stored in JSON format. 303 | For this reason, literals must also be specified in [JSON format](https://www.w3schools.com/js/js_json_datatypes.asp). 304 | The following types of literals are supported: 305 | 306 | * **String:** characters between double quotation marks. 307 | * Double quotation marks in the string value must be escaped using a backslash. 308 | * **Integer:** whole numbers without decimal part. 309 | * **Numeric:** floating point numbers. 310 | * Specify in decimal or scientific format. 311 | * **Boolean:** either `true` or `false`. 312 | * Specify using lowercase characters. 313 | * **List:** array of values between square brackets. 314 | * **Object:** key-value data pairs between curly brackets 315 | * Keys must be string literals, but values can be any literal type. 316 | * **NULL:** missing data is represented by `null`. 317 | 318 | Here is example usage for each type: 319 | 320 | ```{jinjar} 321 | String: {{ "A string" }} 322 | Integer: {{ 3 }} 323 | Numeric: {{ 3.14 }} or {{ 1.6e-19 }} 324 | Boolean: {{ true }} or {{ false }} 325 | List: {{ [1, 2, 3] }} 326 | Object: {{ {"a": 1, "b": 2} }} 327 | Null: {{ null }} 328 | ``` 329 | 330 | 331 | ### Math 332 | 333 | You can perform simple arithmetic using standard operators: 334 | 335 | ```{jinjar} 336 | 1 + 1: {{ 1 + 1 }} 337 | 3 - 2: {{ 3 - 2 }} 338 | 2 * 2: {{ 2 * 2 }} 339 | 1 / 2: {{ 1 / 2 }} 340 | 2 ^ 3: {{ 2 ^ 3 }} 341 | 7 % 3: {{ 7 % 3 }} 342 | ``` 343 | 344 | 345 | ### Comparisons 346 | 347 | You can perform comparisons: 348 | 349 | ```{jinjar} 350 | 1 == 1: {{ 1 == 1 }} 351 | 1 != 1: {{ 1 != 1 }} 352 | 2 > 1: {{ 2 > 1 }} 353 | 2 >= 1: {{ 2 >= 1 }} 354 | 2 < 1: {{ 2 < 1 }} 355 | 2 <= 1: {{ 2 <= 1 }} 356 | ``` 357 | 358 | 359 | ### Logic 360 | 361 | Within expressions and control structures, you can use the Boolean operators: `and`, `or`, and `not`. 362 | 363 | ```{jinjar} 364 | true and false: {{ true and false }} 365 | true or false: {{ true or false }} 366 | not false: {{ not false }} 367 | ``` 368 | 369 | You can also check if a value is contained within a list using `in`: 370 | 371 | ```{jinjar} 372 | {{ 1 in [1, 2, 3] }} 373 | ``` 374 | 375 | 376 | ## Functions 377 | 378 | ### Data Checks 379 | 380 | You can check if a value exists by passing the variable name as a string: 381 | 382 | ```{jinjar, data=params} 383 | users does exist: {{ exists("users") }} 384 | abc doesn't exist: {{ exists("abc") }} 385 | ``` 386 | 387 | Similarly, you can check if a value exists within a JSON object, by passing the key as a string: 388 | 389 | ```{jinjar, data=params} 390 | Birthplace does exist: {{ existsIn(godzilla, "Birthplace") }} 391 | Weight doesn't exist: {{ existsIn(godzilla, "Weight") }} 392 | ``` 393 | 394 | Concisely handle missing values using the `default()` function: 395 | 396 | ```{jinjar, data=params} 397 | {{ default(godzilla.Weight, 20000) }} 398 | ``` 399 | 400 | You can also check the data type of a variable or literal: 401 | 402 | ```{jinjar} 403 | {{ isString("a string") }} 404 | {{ isInteger(3) }} 405 | {{ isFloat(3.14) }} 406 | {{ isNumber(3) }} and {{ isNumber(3.14) }} 407 | {{ isBoolean(false) }} 408 | {{ isArray([1, 2, 3]) }} 409 | {{ isObject({"a": 1, "b": 2}) }} 410 | ``` 411 | 412 | 413 | ### Data Conversion 414 | 415 | You can convert strings to numeric types, using the `int()` or `float()` functions: 416 | 417 | ```{jinjar} 418 | {{ int("2") }} 419 | {{ float("2.5") }} 420 | ``` 421 | 422 | 423 | ### HTML Escaping {#html-escaping} 424 | 425 | When generating HTML from templates, there’s always a risk that a variable will include characters that affect the resulting HTML. 426 | The special characters are: `<`, `>`, `&` and `"`. 427 | 428 | In jinjar, it's **your** responsibility to manually escape variables, using the `escape_html()` function. 429 | You should escape variables that _might_ contain any of the special characters. 430 | But if a variable is trusted to contain well-formed HTML, then it should not be escaped (otherwise you could accidentally double-escape the content). 431 | 432 | ```{jinjar, data=params, engine.opts=list(lang="html")} 433 | 434 | ``` 435 | 436 | 437 | ### SQL Quoting {#sql-quoting} 438 | 439 | SQL databases expect string literals to be wrapped in single-quotes, while other types of literals (e.g., numbers) are not quoted. 440 | This is cumbersome to achieve when writing a template, so the `quote_sql()` function provides this functionality. 441 | 442 | **Important:** `quote_sql()` does not provide any protection against SQL injection attacks. 443 | 444 | ```{jinjar, data=params, engine.opts=list(lang="sql")} 445 | WHERE title = {{ quote_sql(title) }} AND year = {{ quote_sql(godzilla.Born) }} 446 | ``` 447 | 448 | When passed an array, `quote_sql()` will quote each element and return a comma-separated list. 449 | This is particularly helpful when using the SQL `IN` operator. 450 | 451 | ```{jinjar, data=params, engine.opts=list(lang="sql")} 452 | WHERE user IN ({{ quote_sql(users) }}) 453 | ``` 454 | 455 | 456 | ### Numeric Data 457 | 458 | You can check if an integer is even or odd, or divisible by some other integer. 459 | This could be used to make alternating row colors. 460 | 461 | ```{jinjar} 462 | {{ even(42) }} 463 | {{ odd(42) }} 464 | {{ divisibleBy(42, 7) }} 465 | ``` 466 | 467 | You can round floating point numbers to a specific precision: 468 | 469 | ```{jinjar} 470 | {{ round(3.1415, 0) }} 471 | {{ round(3.1415, 3) }} 472 | ``` 473 | 474 | 475 | ### String Data 476 | 477 | Translate a string to lower case or upper case: 478 | 479 | ```{jinjar} 480 | {{ lower("Hello") }} 481 | {{ upper("Hello") }} 482 | ``` 483 | 484 | Escape special characters for use in HTML content (see [HTML Escaping](#html-escaping)): 485 | 486 | ```{jinjar, data=params, engine.opts=list(lang="html")} 487 | 488 | ``` 489 | 490 | Quote string data for use as string literals in a SQL query (see [SQL Quoting](#sql-quoting)): 491 | 492 | ```{jinjar, data=params, engine.opts=list(lang="sql")} 493 | WHERE user IN ({{ quote_sql(users) }}) 494 | ``` 495 | 496 | 497 | ### JSON Lists 498 | 499 | Get the number of list elements: 500 | 501 | ```{jinjar} 502 | length(): {{ length([3,1,2]) }} 503 | ``` 504 | 505 | Get the first or last elements: 506 | 507 | ```{jinjar} 508 | first(): {{ first([3,1,2]) }} 509 | last(): {{ last([3,1,2]) }} 510 | ``` 511 | 512 | Get the minimum or maximum elements: 513 | 514 | ```{jinjar} 515 | min(): {{ min([3,1,2]) }} 516 | max(): {{ max([3,1,2]) }} 517 | ``` 518 | 519 | Sort the list into ascending order: 520 | 521 | ```{jinjar} 522 | sort(): {{ sort([3,1,2]) }} 523 | ``` 524 | 525 | Join a list with a separator: 526 | 527 | ```{jinjar, data=params} 528 | {{ join([1,2,3], " + ") }} 529 | {{ join(users, ", ") }} 530 | ``` 531 | 532 | Generate a list as a range of integers: 533 | 534 | ```{jinjar} 535 | {% for i in range(4) %}{{ loop.index1 }}{% endfor %} 536 | ``` 537 | 538 | Access elements using a dynamic index with `at()`. 539 | Note that the index is **zero-based**. 540 | 541 | ```{jinjar} 542 | {% set x = [1,2,3] -%} 543 | {% set i = 2 -%} 544 | {{ x.2 }} 545 | {{ at(x, i) }} 546 | ``` 547 | 548 | 549 | ### JSON Objects 550 | 551 | Access values using a dynamic key with `at()`: 552 | 553 | ```{jinjar} 554 | {% set x = {"a": 1, "b": 2} -%} 555 | {% set key = "b" -%} 556 | {{ x.b }} 557 | {{ at(x, key) }} 558 | ``` 559 | 560 | --------------------------------------------------------------------------------