Dark Mode

Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

eeditiones/jinks-templates

Repository files navigation

Jinks Templates

A modern templating engine for eXist-db that brings the full power of XPath and XQuery to template processing. Built for flexibility and performance, Jinks Templates handles HTML, XML, CSS, XPath, XQuery, and plain text files with a unified syntax.

Overview

Jinks Templates was developed as the core templating engine for Jinks, the new app generator for TEI Publisher. It extends beyond eXist's older HTML templating capabilities to provide a comprehensive solution for any templating task in the eXist ecosystem.

Key features

Universal processing - Handle any file type with a single templating engine

Native XPath & XQuery - Use familiar XPath expressions directly in templates

Robust architecture - AST-based parsing and compilation for better performance

Front matter support - Extend template context with embedded configuration

Template inheritance - Create a hierarchy of templates with block-based inheritance

Developer experience - Familiar syntax inspired by Nunjucks and JSX

Architecture

Jinks Templates employs a sophisticated two-stage processing pipeline:

  1. Parser - Converts templates into an XML-based Abstract Syntax Tree (AST)
  2. Compiler - Transforms the AST into optimized XQuery code

This architecture delivers superior performance, comprehensive error handling, and enhanced debugging capabilities compared to traditional regex-based solutions.

Expressions

The template syntax is similar to Nunjucks or Jinja, but it uses eXist-db's host language, XQuery, for all expressions, giving users the full power of XPath & XQuery.

The templating is passed a context map, which should contain all the information necessary for processing the template expressions. The entire context map can be accessed via variable $context. Additionally, each top-level property in the context map is made available as an XQuery variable. So if you have a context map like:

map {
"title": "my title",
"theme": map {
"fonts": map {
"content": "serif"
}
}
}

You can either use a map lookup expression referencing entries in the $content variable [[ $context?title ]] or a convenient short form [[ $title ]] to output the title. And to insert the content font, use [[ $context?theme?fonts?content ]] or [[ $theme?fonts?content ]].

Note: Trying to access an undefined context property via the short form, e.g., [[ $author ]], will result in an error. So in case you are unsure if a property is defined, use the long form, i.e., [[ $context?author ]].

Supported template expressions are:

Expression Description
[[ expr ]] Insert result of evaluating expr
[% if expr %] ... [% endif %] Conditional evaluation of block
... [% elif expr %] ... else if block after if
... [% else %] ... [% endif %] else block after if or else if
[% for $var in expr %] ... [% endfor %] Loop $var over sequence returned by expr
[% include expr %] Include a partial. expr should resolve to relative path.
[% block name %] ... [% endblock %] Defines a named block, optionally containing default content to be displayed if there's no template addressing this block.
[% template name order? %] ... [% endtemplate %] Contains content to be appended to the block with the same name. The optional order parameter is an integer by which blocks will be sorted
[% template! name %] ... [% endtemplate %] Replace content of a block. This overwrites all other templates targeting the same block.
[% import "uri" as "prefix" at "path" %] Import an XQuery module so its functions/variables can be used in template expressions.
[% raw %]...[% endraw %] Include the contained text as is, without parsing for templating expressions
[# ... #] Single or multi-line comment: content will be discarded

Here, expr must be a valid XPath expression.

For some real pages built with Jinks Templates, check the app manager of TEI Publisher, jinks. This app also includes a playground and demo for jinks-templates.

Output modes

The Jinks Templates library supports two output modes: XML/HTML and plain text. They differ in the XQuery code that templates are compiled into. While the first will always return XML - and fails if the result is not well-formed, the second uses XQuery string templates.

Use in XQuery

The Jinks Templates library exposes one main function, tmpl:process, which takes 3 arguments:

  1. $input (xs:string): the template to process as a string
  2. $context (map(*)): the context providing the information to be passed to templating expressions
  3. $config (map(*)): a configuration map with the following properties:
    1. plainText (xs:boolean?): should be true for plain text processing (default is false)
    2. resolver (function(xs:string)?): the resolver function to use (see below)
    3. modules (map(*)?): sequence of modules to import (see below)
    4. namespaces (map(*)?): namespace mappings (see below)
    5. debug (xs:boolean?): if true, tmpl:process returns a map with the result, ast and generated XQuery code (default is false)

A simple example:

xquery version "3.1";

import module namespace tmpl="http://e-editiones.org/xquery/templates";

let $input :=

[[$title]]


You are running eXist [[system:get-version()]]



=> serialize()
let $context := map {
"title": "My app"
}
return
tmpl:process($input, $context, map { "plainText": false() })

The input is constructed as XML, but serialized into a string for the call to tmpl:process. The context map in this example contains a single property, which will become available as variable $title (corresponding to its entry's name) within template expressions.

Specifying a resolver

Defining a resolver function in the configuration map is needed if you would like to use the [% include %], [% extends %] or [% import %] template expressions in your templates. Its value should be a function item that takes one parameter - the relative path to the resource - and that returns a map with two entries:

  • path: the absolute path to the resource
  • content: the content of the resource as a string

If the resource cannot be resolved, the empty sequence should be returned. In the following example, we're prepending the assumed application root ($config:app-root) to the supplied relative path to get an absolute path and load the resource:

import module namespace tmpl="http://e-editiones.org/xquery/templates";
import module namespace config=...;

declare function local:resolver($relPath as xs:string) as map(*)? {
let $path := $config:app-root || "/" || $relPath
let $content :=
if (util:binary-doc-available($path)) then
util:binary-doc($path) => util:binary-to-string()
else if (doc-available($path)) then
doc($path) => serialize()
else
()
return
if ($content) then
map {
"path": $path,
"content": $content
}
else
()
};

let $input :=

[[ $title ]]


You are running eXist [[ system:get-version() ]]



=> serialize()
let $context := map {
"title": "My app"
}
let $config := map {
"resolver": local:resolver#1
}
return
tmpl:process($input, $context, $config)

Importing XQuery modules

To make the variables and functions of specific XQuery modules available in your templates, you have to explicitly list those in the configuration's modules entry. This is a map in which the key of each entry corresponds to the URI of the module and the value is a map with two properties: prefix and at, specifying the prefix to use and the location from which the module can be loaded:

let $config := map {
"resolver": local:resolver#1,
"modules": map {
"http://www.tei-c.org/tei-simple/config": map {
"prefix": "config",
"at": $config:app-root || "/modules/config.xqm"
}
},
"namespaces": map {
"tei": "http://www.tei-c.org/ns/1.0"
}
}
return
tmpl:process($input, $context, $config)

This example also shows how namespaces can be declared in the namespaces entry, using the prefix as key and the namespace URI as value.

Use front matter to extend the context

Templates may start with a front matter block enclosed in ---. The purpose of front matter is to extend or overwrite the static context map provided in the second argument to tmpl:process. Currently only JSON syntax is supported. The front matter block will be parsed into an JSON object and merged with the static context passed to tmpl:process. For example, take the following template:

---json
{
"title": "Lorem ipsum dolor sit amet",
"author": "Hans"
}
---
<article>
<h1>[[ $title ]]h1>

<p>Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.p>

<footer>Published [[ format-date(current-dateTime(), "[MNn] [D], [Y]", "en", (), ()) ]] by [[ $author ]].footer>
article>

This will overwrite the title and author properties of the static context map.

The front matter block should come first in the file with a newline after each of the two separators. However, to allow for well-formed XML, the front matter may come after one or more surrounding elements, e.g.:

<article>
---json
{
"title": "Lorem ipsum dolor sit amet",
"author": "Hans"
}
---
<h1>[[ $title ]]h1>
article>

Configuring templating parameters in front matter

Some of the configuration parameters for the templating can also be set via the front matter instead of providing them to the tmpl:process XQuery function. These include declaring XQuery modules and namespaces that you want to reference within template expressions.

Additionally, you can enable template inheritance in the front matter using extends (see next section).

These templating configuration parameters should go into the front matter's block in a top-level entry named templating:

---json
{
"templating": {
"extends": "pages/demo/base.html",
"namespaces": {
"tei": "http://www.tei-c.org/ns/1.0"
},
"modules": {
"https://tei-publisher.com/jinks/xquery/demo": {
"prefix": "demo",
"at": "modules/demo.xql"
}
}
}
}
---

<article>
[% let $data = demo:tei() %]
<h1>[[ $data//tei:title/text() ]]h1>
<p>[[ $data//tei:body/tei:p/text() ]]p>
[% endlet %]
article>

Template inheritance

Template inheritance allows you to create a hierarchy of templates where child templates can extend and customize parent templates. This is particularly useful for creating consistent layouts across multiple pages.

How it works

When a template extends another template:

  • Named templates in the child fill the corresponding blocks in the parent
  • Remaining content is injected into the content block of the parent
  • Multiple levels of inheritance are supported

Example: Multi-level template hierarchy

Here's a complete example using the test application templates:

1. Base layout (pages/page.html)

<div>
<header>
<nav>
<ul>
<li>page.htmlli>
[% block menu %][% endblock %]
ul>
nav>
header>
<main>
[% block content %][% endblock %]
main>
[% include "pages/footer.html" %]
div>

2. Intermediate template (pages/base.html)

<article>
---json
{
"templating": {
"extends": "pages/page.html"
}
}
---
[% template menu %]
<li>base.htmlli>
[% endtemplate %]
<section>
[% block content %][% endblock %]
<p>This paragraph was imported from the parent template.p>
section>
article>

3. Child template with additional blocks

---json
{
"templating": {
"extends": "pages/base.html",
"use": ["pages/blocks.html"]
}
}
---
[% template menu %]
<li>Extra menu itemli>
[% endtemplate %]

[% template copyright %]
<p>(c) e-editionesp>
[% endtemplate %]

<div>
<p>This is the main content of the page.p>
[% block foo %]
<p>A block with default content not referenced by a template.p>
[% endblock %]
div>

4. Footer template (pages/footer.html)

<footer style="border-top: 1px solid #a0a0a0; margin-top: 1rem;">
<p>Generated by [[$context?app]] running on [[system:get-product-name()]] v[[system:get-version()]].p>
[% block copyright %][% endblock %]
footer>

5. Additional blocks (pages/blocks.html)

<template>
[% template menu %]
<li>blocks.htmlli>
[% endtemplate %]
template>

This is a file which contains only templates, no other content (see section on the use directive below).

Result

The final rendered output combines all templates:

<div>
<header>
<nav>
<ul>
<li>page.htmlli>
<li>base.htmlli>
<li>Extra menu itemli>
<li>blocks.htmlli>
ul>
nav>
header>
<main>
<article>
<section>
<div>
<p>This is the main content of the page.p>
<p>A block with default content not referenced by a template.p>
div>
<p>This paragraph was imported from the parent template.p>
section>
article>
main>
<footer style="border-top: 1px solid #a0a0a0; margin-top: 1rem;">
<p>Generated by TEI Publisher running on eXist v6.2.0.p>
<p>(c) e-editionesp>
footer>
div>

Key concepts

  • [% block name %] - Defines a named block that can be overridden
  • [% template name order? %] - Provides content for a specific block
  • [% include "path" %] - Includes another template file
  • "use": ["path"] - Imports additional template files for block definitions
  • Front matter - Configures inheritance and other templating options

Overwriting blocks

If there is more than one [% template %] with the same name, the content of all of them will be concatenated into the corresponding [% block %] placeholder. However, sometimes you may want to override content provided by earlier templates in the inheritance chain. To do so, use the special [% template! name %] directive.

Determining the position of templates in a block

By default, multiple templates will be appended to the target block in the sequence they appear in the inheritance hierarchy. To move a template's content to a specific position, use the optional order parameter to [% template %]. This should be an integer, which will be used to sort the templates before insertion. Templates with an order parameter will be output before templates without one. The latter are still rendered in the sequence they appear in.

The use front matter directive

The use front matter directive allows you to import additional files containing only template definitions, but no other content. This is particularly useful for adding features without modifying existing templates.

Use this directive to dynamically inject content into existing blocks, without having to specify an explicit include in the target template. If blocks are configured in the main context, additional templates will be picked up by any page which has the corresponding block placeholder. It does not need to know if additional templates are available or not.

TEI Publisher features use this to dynamically load specific views into the sidebars. For example, if the iiif feature is enabled, it will load a IIIF viewer into one of the sidebars.

How use works

When you specify "use": ["pages/blocks.html"] in your front matter:

  1. Template loading - The specified template file is loaded and parsed
  2. Block registration - Any [% template name %] blocks in the imported file become available
  3. Content injection - These blocks can then be used to fill corresponding [% block name %] placeholders in the inheritance chain

Example: Using pages/blocks.html

In our example, pages/blocks.html contains:

<template>
[% template menu %]
<li>blocks.htmlli>
[% endtemplate %]
template>

When referenced with "use": ["pages/blocks.html"], the menu template becomes available and gets injected into the menu block in the inheritance chain, resulting in an additional menu item.

Note that we use the HTML