--- title: "Template Syntax" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Template Syntax} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include=FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` This vignette describes the template syntax supported by jinjar, following the structure of the Jinja [Template Designer Documentation](https://jinja.palletsprojects.com/templates/). It is designed to act as a reference when writing templates. The jinjar R package is powered by the [inja](https://github.com/pantor/inja) C++ library. The syntax is very similar to that of the Jinja Python package, but there are also many differences. Unfortunately, this means jinjar is not a drop-in replacement for Jinja -- you might need to adapt existing Jinja templates for the jinjar engine. The most fundamental difference between jinjar and Jinja is: * Jinja variables support direct interaction with the underlying Python objects. * jinjar variables are simple [JSON data types](https://www.w3schools.com/js/js_json_datatypes.asp). The underlying R objects are translated to JSON. This is described in more detail in the [Variables](#variables) section below. Before starting, let's create a few R objects for rendering example templates. ```{r setup} library(jinjar) # length-1 vector title <- "My Webpage" # vector users <- c("User A", "User B", "User C") # list godzilla <- list( Name = "Godzilla", Born = 1952, Birthplace = "Japan" ) # data frame navigation <- data.frame( caption = c("Home", "Blog"), href = c("index.html", "blog.html") ) # HTML special characters name <- 'Dwayne "The Rock" Johnson' ``` ```{r, include=FALSE} params <- list( title = title, users = users, godzilla = godzilla, navigation = navigation, name = name ) ``` ## Synopsis A jinjar template is simply a text file, and when rendered the output is also a text file (e.g. HTML, SQL, LaTeX). 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. Below is a minimal template that illustrates a few basics using the default jinjar configuration. We will cover the details later in this document: ```{jinjar, data=params, engine.opts=list(lang="html")} {{ title }} {# a comment #} ``` The following example shows the default configuration settings, but you can adjust the syntax configuration as desired using `jinjar_config()`. There are a few kinds of delimiters. The default delimiters are configured as follows: * `{% ... %}` for [Statements](#control-structures) * `{{ ... }}` for [Expressions](#expressions) to print to the template output * `{# ... #}` for [Comments](#comments) not included in the template output [Line Statements](#line-statements) are also possible, though they don’t have default prefix characters. To use them, set `line_statement` when creating the `jinjar_config()`. ## Variables {#variables} When writing a template, we refer to variables that act as data placeholders. We define their values when rendering the template. Although we pass R objects to `render()`, it is helpful to understand that these are encoded as JSON objects before the template is rendered. | R object | JSON object | Template example | |:----------------|:-----------------|:------------------| | Length-1 vector | Scalar | `{{ foo }}` | | Vector | Array | `{{ foo.1 }}` | | List | Object | `{{ foo.bar }}` | | Data frame | Array of objects | `{{ foo.1.bar }}` | You can use dot (`.`) notation to access data nested within a variable. 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`). **Note:** In R, the dot is a valid character in an object name (e.g. `my.data`). However, this causes ambiguity when accessing nested data values. For this reason, each dot is replaced with an underscore when the data is encoded as JSON (e.g. `my.data` becomes `my_data`). **Note:** In R, a scalar is indistinguishable from a length-1 vector. This creates an ambiguity when passing R data to the template, because template variables support both scalars and arrays. You can explicitly pass a length-1 vector as an array using the `I()` operator (see `help("render")`). The double-brace syntax is used to print the value of the variable (e.g. `{{ foo }}`). To use the variable in other contexts (e.g. control structures), then these braces are omitted (e.g. `{% for bar in foo %}`). If a template variable has not been defined, then an error occurs. However, you can use the `default(foo, bar)` function to specify a fallback value. ## Comments {#comments} To comment-out some lines, preventing them from appearing in the rendered document, use the comment syntax (default: `{# ... #}`). This is useful for debugging or documenting the template. ```{jinjar} Hello{# TODO: update this #}! ``` ## Whitespace Control {#whitespace} In the default configuration, whitespace (e.g. spaces, tabs, newlines) is left unchanged in the rendered output. For example, in the default configuration we get: ```{jinjar, engine.opts=list(lang="html")}
{% if true %} yay {% endif %}
``` By setting `trim_blocks = TRUE` when creating the `jinjar_config()`, the first newline after a control block is automatically removed. Setting `lstrip_blocks = TRUE` removes any whitespace from the beginning of the line until the start of each block. With both options enabled, the above example becomes: ```{jinjar, engine.opts=list(lang="html", config=jinjar_config(trim_blocks=TRUE, lstrip_blocks=TRUE))}
{% if true %} yay {% endif %}
``` Instead of changing the global configuration, you can manually trim whitespace at a more finegrained level. * 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). * 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). This can be activated for control blocks, comments, or variable expressions: ```{jinjar, engine.opts=list(lang="html")}
{% if true -%} yay {%- endif -%}
``` ## Line Statements {#line-statements} If line statements are enabled (see `jinjar_config()`), it’s possible to mark a line as a statement. For example, if the line statement prefix is configured to `#`, you can do: ```{jinjar, data=params, engine.opts=list(lang="html", config=jinjar_config(line_statement="#"))} ``` ## Control Structures {#control-structures} A control structure refers to all those things that control the flow of a program. With the default syntax, control structures appear inside `{% ... %}` blocks. ### For A for-loop allows you to iterate over each element in a vector: ```{jinjar, data=params, engine.opts=list(lang="markdown")} {% for user in users -%} {{ loop.index1 }}. {{ user }} {%- endfor -%} ``` or loop over key-value pairs in a named list: ```{jinjar, data=params, engine.opts=list(lang="html")}
{% for key, value in godzilla %}
{{ key }}
{{ value }}
{% endfor -%}
``` As described in [Variables](#variables), a data frame is translated to an array of JSON objects. Therefore a nested combination of the above two loops could theoretically be used. In practice, it is much more common to iterate over rows and access the individual elements by their attributes: ```{jinjar, data=params, engine.opts=list(lang="html", config=jinjar_config(line_statement="#"))} ``` While inside a for-loop block, you can access some special variables: | Variable | Description | |:----------------|:------------| | `loop.index` | The current iteration (0-based). | | `loop.index1` | The current iteration (1-based). | | `loop.is_first` | True if first iteration. | | `loop.is_last` | True if last iteration. | | `loop.parent` | In nested loops, the parent loop variable. | ### If Conditional branches are written using `if`, `else if` and `else` statements, which evaluate [Expressions](#expressions). ```{jinjar, data=params, engine.opts=list(lang="markdown")} {% if length(users) > 5 -%} {% for user in users -%} * {{ user }} {% endfor %} {% else if length(users) > 0 -%} {{ join(users, ", ") }}. {% else -%} No users found. {% endif %} ``` ### Assignments Using the `set` statement, you can assign values to variables. ```{jinjar} {% set name="world" -%} Hello {{ name }}! ``` ### Extends The `extends` tag can be used for template inheritance. See _Template Inheritance_ in `vignette("auxiliary-templates")`. ### Include The `include` tag inserts the rendered contents of an auxiliary template. See _Template Inclusion_ in `vignette("auxiliary-templates")`. ## Expressions {#expressions} Basic expressions are supported in templates. ### Literals The simplest form of expressions are literals, which represent fixed values. As described in [Variables](#variables), the template is rendered using data stored in JSON format. For this reason, literals must also be specified in [JSON format](https://www.w3schools.com/js/js_json_datatypes.asp). The following types of literals are supported: * **String:** characters between double quotation marks. * Double quotation marks in the string value must be escaped using a backslash. * **Integer:** whole numbers without decimal part. * **Numeric:** floating point numbers. * Specify in decimal or scientific format. * **Boolean:** either `true` or `false`. * Specify using lowercase characters. * **List:** array of values between square brackets. * **Object:** key-value data pairs between curly brackets * Keys must be string literals, but values can be any literal type. * **NULL:** missing data is represented by `null`. Here is example usage for each type: ```{jinjar} String: {{ "A string" }} Integer: {{ 3 }} Numeric: {{ 3.14 }} or {{ 1.6e-19 }} Boolean: {{ true }} or {{ false }} List: {{ [1, 2, 3] }} Object: {{ {"a": 1, "b": 2} }} Null: {{ null }} ``` ### Math You can perform simple arithmetic using standard operators: ```{jinjar} 1 + 1: {{ 1 + 1 }} 3 - 2: {{ 3 - 2 }} 2 * 2: {{ 2 * 2 }} 1 / 2: {{ 1 / 2 }} 2 ^ 3: {{ 2 ^ 3 }} 7 % 3: {{ 7 % 3 }} ``` ### Comparisons You can perform comparisons: ```{jinjar} 1 == 1: {{ 1 == 1 }} 1 != 1: {{ 1 != 1 }} 2 > 1: {{ 2 > 1 }} 2 >= 1: {{ 2 >= 1 }} 2 < 1: {{ 2 < 1 }} 2 <= 1: {{ 2 <= 1 }} ``` ### Logic Within expressions and control structures, you can use the Boolean operators: `and`, `or`, and `not`. ```{jinjar} true and false: {{ true and false }} true or false: {{ true or false }} not false: {{ not false }} ``` You can also check if a value is contained within a list using `in`: ```{jinjar} {{ 1 in [1, 2, 3] }} ``` ## Functions ### Data Checks You can check if a value exists by passing the variable name as a string: ```{jinjar, data=params} users does exist: {{ exists("users") }} abc doesn't exist: {{ exists("abc") }} ``` Similarly, you can check if a value exists within a JSON object, by passing the key as a string: ```{jinjar, data=params} Birthplace does exist: {{ existsIn(godzilla, "Birthplace") }} Weight doesn't exist: {{ existsIn(godzilla, "Weight") }} ``` Concisely handle missing values using the `default()` function: ```{jinjar, data=params} {{ default(godzilla.Weight, 20000) }} ``` You can also check the data type of a variable or literal: ```{jinjar} {{ isString("a string") }} {{ isInteger(3) }} {{ isFloat(3.14) }} {{ isNumber(3) }} and {{ isNumber(3.14) }} {{ isBoolean(false) }} {{ isArray([1, 2, 3]) }} {{ isObject({"a": 1, "b": 2}) }} ``` ### Data Conversion You can convert strings to numeric types, using the `int()` or `float()` functions: ```{jinjar} {{ int("2") }} {{ float("2.5") }} ``` ### HTML Escaping {#html-escaping} When generating HTML from templates, there’s always a risk that a variable will include characters that affect the resulting HTML. The special characters are: `<`, `>`, `&` and `"`. In jinjar, it's **your** responsibility to manually escape variables, using the `escape_html()` function. You should escape variables that _might_ contain any of the special characters. 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). ```{jinjar, data=params, engine.opts=list(lang="html")} ``` ### SQL Quoting {#sql-quoting} SQL databases expect string literals to be wrapped in single-quotes, while other types of literals (e.g., numbers) are not quoted. This is cumbersome to achieve when writing a template, so the `quote_sql()` function provides this functionality. **Important:** `quote_sql()` does not provide any protection against SQL injection attacks. ```{jinjar, data=params, engine.opts=list(lang="sql")} WHERE title = {{ quote_sql(title) }} AND year = {{ quote_sql(godzilla.Born) }} ``` When passed an array, `quote_sql()` will quote each element and return a comma-separated list. This is particularly helpful when using the SQL `IN` operator. ```{jinjar, data=params, engine.opts=list(lang="sql")} WHERE user IN ({{ quote_sql(users) }}) ``` ### Numeric Data You can check if an integer is even or odd, or divisible by some other integer. This could be used to make alternating row colors. ```{jinjar} {{ even(42) }} {{ odd(42) }} {{ divisibleBy(42, 7) }} ``` You can round floating point numbers to a specific precision: ```{jinjar} {{ round(3.1415, 0) }} {{ round(3.1415, 3) }} ``` ### String Data Translate a string to lower case or upper case: ```{jinjar} {{ lower("Hello") }} {{ upper("Hello") }} ``` Escape special characters for use in HTML content (see [HTML Escaping](#html-escaping)): ```{jinjar, data=params, engine.opts=list(lang="html")} ``` Quote string data for use as string literals in a SQL query (see [SQL Quoting](#sql-quoting)): ```{jinjar, data=params, engine.opts=list(lang="sql")} WHERE user IN ({{ quote_sql(users) }}) ``` ### JSON Lists Get the number of list elements: ```{jinjar} length(): {{ length([3,1,2]) }} ``` Get the first or last elements: ```{jinjar} first(): {{ first([3,1,2]) }} last(): {{ last([3,1,2]) }} ``` Get the minimum or maximum elements: ```{jinjar} min(): {{ min([3,1,2]) }} max(): {{ max([3,1,2]) }} ``` Sort the list into ascending order: ```{jinjar} sort(): {{ sort([3,1,2]) }} ``` Join a list with a separator: ```{jinjar, data=params} {{ join([1,2,3], " + ") }} {{ join(users, ", ") }} ``` Generate a list as a range of integers: ```{jinjar} {% for i in range(4) %}{{ loop.index1 }}{% endfor %} ``` Access elements using a dynamic index with `at()`. Note that the index is **zero-based**. ```{jinjar} {% set x = [1,2,3] -%} {% set i = 2 -%} {{ x.2 }} {{ at(x, i) }} ``` ### JSON Objects Access values using a dynamic key with `at()`: ```{jinjar} {% set x = {"a": 1, "b": 2} -%} {% set key = "b" -%} {{ x.b }} {{ at(x, key) }} ```