Package dali
1 Template Language
1.1 Variables
1.1.1 Lambdas
1.2 Sections
1.2.1 False Values or Empty Lists
1.2.2 Non-Empty Lists
1.2.3 Lambdas
1.2.4 Non-False Values
1.3 Inverted Sections
1.4 Paths
1.4.1 Parent Paths
1.4.2 Self-Reference
1.5 Comments
1.6 Partials
1.7 Set Delimiter
1.8 Helpers
1.9 Literals
2 Module dali
2.1 Parameters
partial-path
partial-cache
partial-extension
escape-replacements
2.2 Template Expansion
expand-file
expand-string
compile-string
load-partial
blank-missing-value-handler
error-missing-value-handler
3 License
1.0

Package dali🔗ℹ

Simon Johnston <johnstonskj@gmail.com>

Dali is a Racket implementation of a language similar to Mustache and Handlebars. It tries to be as faithful as possible to Mustache, providing a simple and high-level idiomatic module for callers. Not all features of Handlebars are implemented, or implemented in the same way as they are more JavaScript focused.

The first part of this page describes the supported template language, any deviations from Mustache, and any Racket-specific features. It then goes on to describe Module dali itself and the its operation.

The dali module provides compile-string, expand-string, and expand-file functions for template expansion. The expand functions rely on the compile function to read the template and convert it into a Racket function for performance and re-use.

1 Template Language🔗ℹ

Dali implements features from the languages defined by Mustache and Handlebars. The following section describes the language in more detail and use the same outline structure as the Mustache man page. As with Mustache, the template comprises plain text with embedded tags where these are indicated by bounding double mustaches, as in {{person}}. Tags have different processing depending on their specific meaning, as described in the sections below.

In Dali the context that provides tag replacement values is simply a hash? with string? keys and values that may be a nested hash, a list?, or a single value (symbol?, string?, char?, boolean?, or number?). This allows for simple and familiar construction of contexts and more readable code when invoking expansion functions.

1.1 Variables🔗ℹ

Variables are specified between {{ and }}. On encountering a variable the text of the tag is assumed to be a key present in the current context and the corresponding value is returned as a replacement. When the key is not found the expansion functions have a missing-value-handler parameter which is a function that takes the key and returns a string value. The dali module provides an implementation (blank-missing-value-handler) which simply returns an empty string, as well as another (error-missing-value-handler) which raises exn:fail.

Note, Dali does not search up the context for the tag name, it only considers the current context; see Paths and Parent Paths for alternative mechanisms.

All variables are HTML-escaped by default. Variables specified between {{{ and }}}, or with the tag prefix & (special characters between the opening mustache and key), will not HTML-escape their content.

Template:

* {{name}}

* {{age}}

* {{company}}

* {{{company}}}

* {{&company}}

Context:

(hash "name" "Chris"
      "company" "<b>GitHub</b>")

Output:

> (for-each displayln
            (string-split
             (expand-string
              "* {{name}}\n* {{age}}\n* {{company}}\n* {{{company}}}\n* {{&company}}"
              (hash "name" "Chris"
                    "company" "<b>GitHub</b>"))
             "\n"))

* Chris

*

* &lt;b&gt;GitHub&lt;/b&gt;

* <b>GitHub</b>

* <b>GitHub</b>

1.1.1 Lambdas🔗ℹ

Any value may be a be a lambda, specifically a lambda that takes one argument that holds the key used to select it, and a second optional argument that holds the current context hash. The lambda returns a string value that will be used as the replacement value.

Examples:
> (require racket/date)
> (expand-string "Hi {{name}}, today is {{date}}."
                 (hash "name" "Chris"
                       "date" (λ (k) (date->string (current-date)))))

"Hi Chris, today is Wednesday, February 7th, 2024."

The corresponding contract for the lambda is therefore:

[value-lambda (->* (string?) (hash?) string?)]

1.2 Sections🔗ℹ

Sections render blocks of text one or more times, depending on the value of the key in the current context. Sections start with the # tag prefix and end with the / tag prefix. The following sub-sections outline the specific behavior of sections based on the type of the tag value.

1.2.1 False Values or Empty Lists🔗ℹ

If the key exists and has a false value (one of #f, "", 0, '(), or #hash()) the tag content will not be displayed.

Template:

Shown.

{{#person}}

  Never shown!

{{/person}}

Context:

(hash "person" #f)

Output:

> (for-each displayln
            (string-split
             (expand-string
              "Shown.\n{{#person}}\n  Never shown!\n{{/person}}"
              (hash "person" #f))
             "\n"))

Shown.

1.2.2 Non-Empty Lists🔗ℹ

If the key exists and is a non-empty list the tag content will be rendered once for each item in the list. In the case that the item is itself a hash value the context for each render will be reset to be the list item.

Template:

{{#repo}}

  <b>{{name}}</b>

{{/repo}}

Context:

(hash "repo" (list (hash "name" "resque")
                   (hash "name" "hub")
                   (hash "name" "rip")))

Output:

> (for-each displayln
            (string-split
             (expand-string
              "{{#repo}}\n  <b>{{name}}</b>\n{{/repo}}"
              (hash "repo" (list (hash "name" "resque")
                                 (hash "name" "hub")
                                 (hash "name" "rip"))))
             "\n"))

  <b>resque</b>

  <b>hub</b>

  <b>rip</b>

If, however, the list contains single valued items the context is not reset but a temporary key "_" is added to the context with the value of the list item (see Self-Reference).

Template:

{{#repo}}

  <b>{{_}}</b>

{{/repo}}

Context:

(hash "repo" (list "resque" "hub" "rip"))

Output:

> (for-each displayln
            (string-split
             (expand-string
              "{{#repo}}\n  <b>{{_}}</b>\n{{/repo}}"
              (hash "repo" (list  "resque" "hub" "rip")))
             "\n"))

  <b>resque</b>

  <b>hub</b>

  <b>rip</b>

1.2.3 Lambdas🔗ℹ

If the key exists and the value is a procedure?, and specifically one with a procedure-arity of 2, it will be called to return a replacement value. Unlike value Lambdas above, instead of being passed the key the lambda is passed the unexpanded content of the section. The second parameter is a function that when called will be able to render the provided text.

This is currently unsupported/untested.

Template:

{{#wrapped}}

  {{name}} is awesome.

{{/wrapped}}

Context:

(hash "name" "willy"
      "wrapped" (λ (text render)
                  (format "<b>~a</b>" (render))))

Expected Output:

<b>Willy is awesome.</b>

1.2.4 Non-False Values🔗ℹ

If the key exists, it is not a list, we assume it is a nested hash? and will be used as the context for rendering the section.

Template:

{{#person?}}

  Hi {{name}}!

{{/person?}}

Context:

(hash "person?" (hash "name" "Jon"))

Output:

> (for-each displayln
            (string-split
             (expand-string
              "{{#person?}}\n  Hi {{name}}!\n{{/person?}}"
              (hash "person?" (hash "name" "Jon")))
             "\n"))

  Hi Jon!

1.3 Inverted Sections🔗ℹ

If the tag prefix is a caret, ^, the section renders based on the inverse of the logical tests above. For example, such a section will render if the key does not exist, is a false value, the empty list or an empty hash.

Template:

{{#repo}}

  <b>{{name}}</b>

{{/repo}}

{{^repo}}

  No repos :(

{{/repo}}

Context:

(hash "repos" '())

Output:

> (for-each displayln
            (string-split
             (expand-string
              "{{#repo}}\n  <b>{{name}}</b>\n{{/repo}}\n{{^repo}}\n  No repos :(\n{{/repo}}"
              (hash "repos" '()))
             "\n"))

  <b></b>

  No repos :(

1.4 Paths🔗ℹ

A tag can reference nested values, i.e. a hash within a hash, using a dotted name such as parent-name.child-name (a Handlebars feature). Each name in the path is assumed to reference a hash value (except the last) and if not it will be treated as a missing value. Therefore the following template:

Template:

{{#name}}Hello {{person.name}}.{{/name}}

Context:

(hash "person" (hash "name" "Chris"))

Output:

> (for-each displayln
            (string-split
             (expand-string
              "{{#name}}Hello {{person.name}}.{{/name}}"
              (hash "person" (hash "name" "Chris")))
             "\n"))

Hello Chris.

1.4.1 Parent Paths🔗ℹ

Handlebars also supports references to the parent context via the use of relative path specifiers (../). For example, in the following template the #person} section will change the context within the section to the nested hash value but the salutation key exists in the parent context.

This is currently unsupported/untested.

Template:

{{#person}}{{../salutation}} {{name}}.{{/person}}

Context:

(hash "salutation" "Hello"
      "person" (hash "name" "Chris"))

Expected Output:

Hello Chris.

1.4.2 Self-Reference🔗ℹ

Sometimes, the logic for a template is such that we use a conditional section as a guard around a single value, for example:

Template:

{{#name}}Hello {{name}}.{{/name}}

Context:

(hash "name" "Chris")

Output:

> (for-each displayln
            (string-split
             (expand-string
              "{{#name}}Hello {{name}}.{{/name}}"
              (hash "name" "Chris"))
             "\n"))

Hello Chris.

While this is a perfectly reasonable approach, sometimes it feels verbose to re-type the name tag three times. Dali supports a shortcut for reference inside a section to the value of the section, the underscore character. Therefore, the following template is equivalent to the example above.

> (for-each displayln
            (string-split
             (expand-string
              "{{#name}}Hello {{_}}.{{/name}}"
              (hash "name" "Chris"))
             "\n"))

Hello Chris.

1.5 Comments🔗ℹ

Variables with a ! prefix character are treated as comments and ignored.

Template:

<h1>Today{{! ignore me }}.</h1>

Output:

> (for-each displayln
            (string-split
             (expand-string
              "<h1>Today{{! ignore me }}.</h1>"
              (hash))
             "\n"))

<h1>Today.</h1>

<h1>Today.</h1>

Handlebars provides the extended ! comment form, but as this shares the same prefix "!" it is supported by default.

1.6 Partials🔗ℹ

Variables with the prefix > are used to incorporate the content of a separate template file, a reusable part of the larger template. The key is not used to look up any value in the context but is taken to be the name of a file (with the default extension ".mustache") to be transcluded into the template at runtime.

Dali only loads a partial on its first reference, it compiles it and uses a cache to refer to it at render time. This allows a partial to be used in multiple places in a template, or even in multiple templates, without re-processing.

Partial (base.mustache):

<h2>Names</h2>

{{#names}}

  {{> user}}

{{/names}}

Template:

<strong>{{name}}</strong>

Will be combined as if it were a single expanded template:

<h2>Names</h2>

{{#names}}

  <strong>{{name}}</strong>

{{/names}}

1.7 Set Delimiter🔗ℹ

Currently Unsupported.

1.8 Helpers🔗ℹ

Handlebars supports a separate JavaScript function, Handlebars.registerHelper, to name a function that can then be used as a section name.

This is provided in part by value Lambdas.

1.9 Literals🔗ℹ

Handlebars supports the addition of literal values as a sequence of key=value pairs to add to the current context for a section.

This is currently unsupported.

Example Handlebars Template:

{{agree_button "My Text" class="my-class" visible=true counter=4}}

2 Module dali🔗ℹ

 (require dali) package: dali

This module implements the Dali template engine.

Examples:
> (require dali)
; high-level function: expand-string
> (define template "a list: {{#items}} {{item}}, {{/items}}and that's all")
> (define context (hash "items" (list (hash "item" "one")
                                      (hash "item" "two")
                                      (hash "item" "three"))))
> (expand-string template context)

"a list:  one,  two,  three, and that's all"

; lower-level function: compile-string
> (define compiled-template (compile-string "hello {{name}}!"))
> (define output (open-output-string))
> (compiled-template (hash "name" "simon") output)
> (get-output-string output)

"hello simon!"

2.1 Parameters🔗ℹ

The following parameters are all used during the processing of partial blocks, external templates that are incorporated into the parent. Primarily these affect the behavior of load-partial which finds, loads, and compiles external files and adds them to the partial-cache. This cache is then used by compile-string to fetch and include partials.

value

partial-path : (listof stting?)

A list of strings that are used to search for partial files to load.

A hash from partial name to the semi-compiled form (i.e. to a quoted list that will be incorporated into the final compiled form).

The file extension for partial files, by default this is .mustache.

This parameter affects the escaping performed during variable expansion. By default this is a common set of HTML entity replacements. However, if you wish to extend the HTML set or replace entirely for your own language or purpose, override this parameter. The actual value is a list of pairs where the first is a string to replace with the second.

> (escape-replacements)

'(("&" . "&amp;")

  ("<" . "&lt;")

  (">" . "&gt;")

  ("\"" . "&quot;")

  ("'" . "&#39;"))

2.2 Template Expansion🔗ℹ

procedure

(expand-file source    
  target    
  context    
  [missing-value-handler])  void?
  source : path-string?
  target : path-string?
  context : hash?
  missing-value-handler : (-> string? string?)
   = blank-missing-value-handler
This function will read the file source, process with expand-string, and write the result to the file target. It will raise exn:fail if the source file does not exist, or if the target file does exist.

procedure

(expand-string source    
  context    
  [missing-value-handler])  string?
  source : string?
  context : hash?
  missing-value-handler : (-> string? string?)
   = blank-missing-value-handler
This function will treat source as a template, compile it with compile-string, evaluate it with the provided context and return the result as a string.

A context is actually defined recursively as (hash/c string? (or/c string? list? hash?)) so that the top level is a hash with string keys and values which are either lists or hashes with the same contract.

The missing-value-handler is a function that will be called when the key in a template is not found in the context, it is provided the key content and any value it returns is used as the replacement text.

procedure

(compile-string source)

  (->* (hash? output-port?) ((-> string? string?)) void?)
  source : string?
This function will compile a template into a function that may be called with a context hash? and an output-port? to generate content.

The generated compiled form can be thought of as a new function with the following form.

(λ (context out [missing-value-handler blank-missing-value-handler])
  ...
  (void))

This function may raise exn:fail for the following conditions.

  • Invalid context structure, for example a value which is not a list, hash, string, boolean, symbol, or number.

  • A partial file could not be loaded (does not exist).

  • An unsupported feature (section Lambdas, Set Delimiter Literals, or Parent Paths).

procedure

(load-partial name)  boolean?

  name : string?
Find a file with name and extension partial-extension, in the search paths specified by partial-path. Load the file, compile it and add it to partial-cache. Returns #t if this is successful, or #f on error.

procedure

(blank-missing-value-handler name [context])  string?

  name : string?
  context : hash? = (hash)
This is the default missing-value-handler function, it simply returns a blank string ("") for any missing context key.

procedure

(error-missing-value-handler name [context])  string?

  name : string?
  context : hash? = (hash)
This handler can be used to raise exn:fail for any missing context key.

3 License🔗ℹ

MIT License

 

Copyright (c) 2018 Simon Johnston (johnstonskj@gmail.com).

 

Permission is hereby granted, free of charge, to any person obtaining a copy

of this software and associated documentation files (the "Software"), to deal

in the Software without restriction, including without limitation the rights

to use, copy, modify, merge, publish, distribute, sublicense, and/or sell

copies of the Software, and to permit persons to whom the Software is

furnished to do so, subject to the following conditions:

 

The above copyright notice and this permission notice shall be included in all

copies or substantial portions of the Software.

 

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR

IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE

AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER

LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,

OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE

SOFTWARE.