├── .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 | #'
17 | #' {% for item in navigation -%}
18 | #' {{ item.caption }}
19 | #' {% endfor -%}
20 | #'
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 | [](https://CRAN.R-project.org/package=jinjar)
20 | [](https://app.codecov.io/gh/davidchall/jinjar?branch=master)
21 | [](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 | [](https://CRAN.R-project.org/package=jinjar)
10 | [](https://app.codecov.io/gh/davidchall/jinjar?branch=master)
12 | [](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 | [36m--[39m [1mTemplate configuration[22m [36m------------------------------------------------------[39m
26 | [1mLoader:[22m disabled
27 | [1mSyntax:[22m [34m{% block %}[39m [32m{{ variable }}[39m [3m[90m{# comment #}[39m[23m
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 | [36m──[39m [1mTemplate configuration[22m [36m──────────────────────────────────────────────────────[39m
46 | [1mLoader:[22m disabled
47 | [1mSyntax:[22m [34m{% block %}[39m [32m{{ variable }}[39m [3m[90m{# comment #}[39m[23m
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 | [36m--[39m [1mTemplate configuration[22m [36m------------------------------------------------------[39m
66 | [1mLoader:[22m [34m/path/to/templates[39m
67 | [1mSyntax:[22m [34m{% block %}[39m [32m{{ variable }}[39m [3m[90m{# comment #}[39m[23m
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 | [36m──[39m [1mTemplate configuration[22m [36m──────────────────────────────────────────────────────[39m
86 | [1mLoader:[22m [34m/path/to/templates[39m
87 | [1mSyntax:[22m [34m{% block %}[39m [32m{{ variable }}[39m [3m[90m{# comment #}[39m[23m
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 | [1mLoader:[22m [34m/path/to/templates[39m
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 | [1mLoader:[22m [34m/path/to/templates[39m
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 | [1mLoader:[22m [34m{jinjar}[39m/[34mR[39m
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 | [1mLoader:[22m [34m{jinjar}[39m/[34mR[39m
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 | [1mLoader:[22m [34m"x"[39m, [34m"y"[39m, and [34m"z"[39m
76 | Code
77 | print(list_loader(long_names))
78 | Message
79 | [1mLoader:[22m
80 | * [34m"here_is_a_very_long_template_name"[39m
81 | * [34m"and_one_more_just_for_good_luck"[39m
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 | [1mLoader:[22m [34m"x"[39m, [34m"y"[39m, and [34m"z"[39m
102 | Code
103 | print(list_loader(long_names))
104 | Message
105 | [1mLoader:[22m
106 | • [34m"here_is_a_very_long_template_name"[39m
107 | • [34m"and_one_more_just_for_good_luck"[39m
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 | [3m[90m{# put a comment here #}[39m[23m
33 | [34m{% for person in people -%}[39m
34 | [34m{% if "A New Hope" in person.films and default(person.species, "Unknown") == "Human" -%}[39m
35 | * [32m{{ person.name }}[39m ([32m{{ person.homeworld }}[39m)
36 | [34m{% endif -%}[39m
37 | [34m{% endfor -%}[39m
38 |
39 | ---
40 |
41 | Code
42 | print(x, n = 5)
43 | Message
44 | Humans of A New Hope
45 | [3m[90m{# put a comment here #}[39m[23m
46 | [34m{% for person in people -%}[39m
47 | [34m{% if "A New Hope" in person.films and default(person.species, "Unknown") == "Human" -%}[39m
48 | * [32m{{ person.name }}[39m ([32m{{ person.homeworld }}[39m)
49 | [36mi[39m [90m... with 2 more lines[39m
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 | [3m[90m{# put a comment here #}[39m[23m
83 | [34m{% for person in people -%}[39m
84 | [34m{% if "A New Hope" in person.films and default(person.species, "Unknown") == "Human" -%}[39m
85 | * [32m{{ person.name }}[39m ([32m{{ person.homeworld }}[39m)
86 | [34m{% endif -%}[39m
87 | [34m{% endfor -%}[39m
88 |
89 | ---
90 |
91 | Code
92 | print(x, n = 5)
93 | Message
94 | Humans of A New Hope
95 | [3m[90m{# put a comment here #}[39m[23m
96 | [34m{% for person in people -%}[39m
97 | [34m{% if "A New Hope" in person.films and default(person.species, "Unknown") == "Human" -%}[39m
98 | * [32m{{ person.name }}[39m ([32m{{ person.homeworld }}[39m)
99 | [36mℹ[39m [90m… with 2 more lines[39m
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 | [3m[90m{# {{ this }} is a {{ comment }} #}[39m[23m
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 | [3m[90m{# {{ this }} is a {{ comment }} #}[39m[23m
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 | [1m[33mError[39m in `parse_template()`:[22m
146 | [33m![39m Problem encountered while parsing template.
147 | [1mCaused by error:[22m
148 | [1m[22m[33m![39m Unexpected '}'.
149 | [36mi[39m Error occurred on [32mline 1[39m and [32mcolumn 15[39m.
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 | [1m[33mError[39m in `parse_template()`:[22m
168 | [33m![39m Problem encountered while parsing template.
169 | [1mCaused by error:[22m
170 | [1m[22m[33m![39m Unexpected '}'.
171 | [36mℹ[39m Error occurred on [32mline 1[39m and [32mcolumn 15[39m.
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 | [1m[33mError[39m in `render()`:[22m
86 | [33m![39m Problem encountered while rendering template.
87 | [1mCaused by error:[22m
88 | [1m[22m[33m![39m Variable 'name' not found.
89 | [36mi[39m Error occurred on [32mline 1[39m and [32mcolumn 10[39m.
90 |
91 | ---
92 |
93 | Code
94 | render("{% include \"missing.html\" %}")
95 | Condition
96 | [1m[33mError[39m in `render()`:[22m
97 | [33m![39m Problem encountered while rendering template.
98 | [1mCaused by error:[22m
99 | [1m[22m[33m![39m Include 'missing.html' not found.
100 | [36mi[39m Error occurred on [32mline 1[39m and [32mcolumn 12[39m.
101 |
102 | ---
103 |
104 | Code
105 | render("{% for x in vec %}{{ x }}{% endfor %}", vec = "world")
106 | Condition
107 | [1m[33mError[39m in `render()`:[22m
108 | [33m![39m Problem encountered while rendering template.
109 | [1mCaused by error:[22m
110 | [1m[22m[33m![39m Object must be an array.
111 | [36mi[39m Have you forgotten to wrap a length-1 vector with I()?
112 | [36mi[39m Error occurred on [32mline 1[39m and [32mcolumn 10[39m.
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 | [1m[33mError[39m in `render()`:[22m
154 | [33m![39m Problem encountered while rendering template.
155 | [1mCaused by error:[22m
156 | [1m[22m[33m![39m Variable 'name' not found.
157 | [36mℹ[39m Error occurred on [32mline 1[39m and [32mcolumn 10[39m.
158 |
159 | ---
160 |
161 | Code
162 | render("{% include \"missing.html\" %}")
163 | Condition
164 | [1m[33mError[39m in `render()`:[22m
165 | [33m![39m Problem encountered while rendering template.
166 | [1mCaused by error:[22m
167 | [1m[22m[33m![39m Include 'missing.html' not found.
168 | [36mℹ[39m Error occurred on [32mline 1[39m and [32mcolumn 12[39m.
169 |
170 | ---
171 |
172 | Code
173 | render("{% for x in vec %}{{ x }}{% endfor %}", vec = "world")
174 | Condition
175 | [1m[33mError[39m in `render()`:[22m
176 | [33m![39m Problem encountered while rendering template.
177 | [1mCaused by error:[22m
178 | [1m[22m[33m![39m Object must be an array.
179 | [36mℹ[39m Have you forgotten to wrap a length-1 vector with I()?
180 | [36mℹ[39m Error occurred on [32mline 1[39m and [32mcolumn 10[39m.
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 | [1m[33mError[39m:[22m
205 | [33m![39m Problem encountered while decoding JSON data.
206 | [34mi[39m This is an internal error that was detected in the [34mjinjar[39m package.
207 | Please report it at [3m[34m[39m[23m with a reprex ([3m[34m [39m[23m) and the full backtrace.
208 | [1mCaused by error:[22m
209 | [1m[22m[33m![39m [json.exception.parse_error.101] parse error at line 1, column 17: syntax error while parsing object - unexpected ']'; expected '}'
210 | [36mi[39m JSON object: [34m"{\"name\": \"world\"]}"[39m
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 | [1m[33mError[39m:[22m
235 | [33m![39m Problem encountered while decoding JSON data.
236 | [34mℹ[39m This is an internal error that was detected in the [34mjinjar[39m package.
237 | Please report it at [3m[34m[39m[23m with a reprex ([3m[34m [39m[23m) and the full backtrace.
238 | [1mCaused by error:[22m
239 | [1m[22m[33m![39m [json.exception.parse_error.101] parse error at line 1, column 17: syntax error while parsing object - unexpected ']'; expected '}'
240 | [36mℹ[39m JSON object: [34m"{\"name\": \"world\"]}"[39m
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 |
--------------------------------------------------------------------------------