Liking cljdoc? Tell your friends :D

Majavat

A templating engine for Clojure

Installation

Add majavat to dependency list

[org.clojars.jj/majavat "2.4.0"]

Usage

Rendering templates

Direct rendering

(:require
  [jj.majavat :as majavat]
  [jj.majavat.renderer.sanitizer :refer [->Html]])

(def render-fn (majavat/build-renderer "index.html"))
;; or build html renderer, which will sanitize input
(def html-render-fn (majavat/build-html-renderer "index.html"))

(render-fn {:user "jj"})
(html-render-fn {:user "jj"})

Additional options can be passed with

(def render-fn (majavat/build-renderer "index.html" {:cache?      false
                                                     :pre-render  {:key "value"}
                                                     :environment {:filters {:reverse (fn [value]
                                                                                        (string/reverse value))}}
                                                     :renderer    (->StringRenderer)}))

(render-fn {:user "jj"})

Indirect rendering

It will cache all render-functions for user

(:require [jj.majavat.cache :as majavat-cache])

(majavat-cache/render-html "index.html" {:user "jj"})

All supported options:

OptionDefault ValueSupported Options
rendererStringRendererAny Renderer implementation
cache?truetrue, false
template-resolverResourceResolverTemplateResolver
pre-render{}Map
sanitizernilAny Sanitizer implementation
environment{}Map (see environment options)
error-handlerReportingAny ErrorHandler Implementation

Environment

OptionDefault ValueSupported Options
filters{}Map
sanitizers{}Keyword -> Sanitizer Map
dictionarynilAny Dictionary implementation

Creating templates

Inserting value

Rendering file.txt with content

Hello {{ user.name }}!
ID is: {{ user.`namespaced/user.id` }}!
(def render-fn (build-renderer "file.txt"))
(render-fn {:user {:name               "jj"
                   :namespaced/user.id "foo"}}) ;; => returns "Hello world!\nID is foo"

or with a filter

Hello {{ name | upper-case }}!
(def render-fn (build-renderer "file.txt"))

(render-fn {:name "world"}) ;; => returns Hello WORLD!
Built In Filters
FilterTypeInputOutput
absNumber-11.0
append(" world")String"hello""hello world"
capitalizeString"hello world""Hello world"
ceilNumber1.992
date("hh/mm", "Asia/Tokyo")Instant2011-11-11T02:11:00Z"11/11"
date("yyyy")LocalDate2025-01-15"2025"
date("yyyy")LocalDateTime2025-01-15T11:11"2025"
date("hh/mm")LocalTime11:11"11/11"
date("hh/mm", "Asia/Tokyo")ZonedDateTime2011-11-11T11:11+09:00[Asia/Tokyo]"11/11"
decNumber54
default("foo")nilnil"foo"
default("foo")not nil"bar""bar"
file-sizeNumber2048"2 KB"
firstMap{:foo :a :bar :b :baz :c}[:foo :a]
firstSequential(list :foo :bar :baz):foo
floorNumber1.41.0
incNumber56
indent(2, [first], [blank])String"a\nb""a\n b"
intString"123"123
joinSequential[1 2 3]"1, 2, 3" (default sep ", ")
join("-")Sequential[1 2 3]"1-2-3"
jsonAny{:a 1}{"a":1}
json(2)Any{:a 1}pretty-printed, 2-sp indent
lengthMap{:a 1 :b 2}2
lengthSequential[1 2 3]3
lengthString"hello"5
longString"123"123L
lower-caseString"HELLO WORLD""hello world"
nameKeyword:name"name"
prepend(" world")String"hello""world hello"
replace("a", "o")String"banana""bonono"
replace("a", "o", 2)String"banana""bonona"
restMap{:foo :a :bar :b :baz :c}{:bar :b :baz :c}
restSequential(list :foo :bar :baz)(list :bar :baz)
roundNumber1.992
slugifyString"Foo Bar""foo-bar"
strany1"1"
title-caseString"hello world""Hello World"
transKeyword:hello"hei" (via dictionary)
trimString" hello ""hello"
truncate(14, [kill], [end])String"The quick brown fox""The quick..."
upper-caseString"hello world""HELLO WORLD"
upper-romanString"iv""IV"

Arguments shown in [brackets] are optional. indent's width is required; first also indents the first line (off by default) and blank also indents blank lines. For truncate, kill cuts at the exact length instead of trimming back to a word boundary, and end sets the ellipsis (default "...").

User Provided filters Filters

Assoc :filter to option map, when building renderer, with this value

{:quote (fn [value author]
          (format "\"%s\" - %s" value author))}

Note: Tag a filter with ^{:context-aware true} to receive the full render context as the second argument, before any template arguments:

{:quote ^{:context-aware true}
        (fn [value context author]
          (format "\"%s\" - %s (%s)" value author (:locale context)))}

Conditionals

Rendering input file with content:

"Hello {% if name %}{{name}}{% elif id %}{% else %}world{% endif %}!"
(def render-fn (build-renderer "input-file"))

(render-fn {:name "jj"}) ;; returns "Hello jj!"
(render-fn {:id "JJ"}) ;; returns "Hello JJ!"
(render-fn {}) ;; returns "Hello world!"

or

"Hello {% if not name %}world{% else %}jj{% endif %}!"
(def render-fn (build-renderer "input-file"))

(render-fn {:name "foo"}) ;; returns "Hello jj!"
(render-fn {}) ;; returns "Hello world!"

or with tests

"Hello {% if value is even %}even{% else %}not even{% endif %}!"
(render-fn {:value 2}) ;; returns "Hello even!"
(render-fn {:value 1}) ;; returns "Hello not even!"

Available is tests:

test nameargsexample
even-{% if value is even %}
odd-{% if value is odd %}

Comparison operators

operatorexample
=={% if value == 0 %}
{% if value == "value" %}
<{% if value < 10 %}
<={% if value <= 10 %}
>{% if value > 10 %}
>={% if value >= 10 %}

<, <=, >, and >= compare numbers (a non-number value makes the condition false); == compares numbers and strings.

Looping

for

Rendering input file with content:

{% for item in items %}
- {{ item }} is {{ loop.index }} of {{ loop.total }}
{% endfor %}
(def render-fn (build-renderer "input-file"))

(render-fn {:items ["Apple" "Banana" "Orange"]}) ;; returns "- Apple is 0 of 3\n- Banana is 1 of 3\n- Orange is 2 of 3"

or default value

{% for item in items %}
- {{ item }} is {{ loop.index }} of {{ loop.total }}
{% empty %}
empty list
{% endfor %}
(def render-fn (build-renderer "input-file"))

(render-fn {}) ;; returns "empty list"

The loop context provides access to:

loop.total - total number of items in the collection

loop.index - current 0-based index position

loop.first? - true only for the first item

loop.last? - true only for the last item

In situations where loop context is not needed, only can be used

{% for item only in items %}
- {{ item }}
{% endfor %}
(def render-fn (build-renderer "input-file"))

(render-fn {:items ["Apple" "Banana" "Orange"]}) ;; returns "- Apple\n- Banana\n- Orange"

Including template

file.txt content

foo

Rendering input file with content:

included {% include "file.txt" %}
(def render-fn (build-renderer "input-file"))

(render-fn {}) ;; returns "included foo"

Setting value

You can set value within a template via:

hello {% let foo = "baz" %}{{ foo }}{% endlet %}
(def render-fn (build-renderer "input-file"))

(render-fn {}) ;; returns "hello baz"

or

hello {% let foo = bar %}{{ foo.baz }}{% endlet %}
(def render-fn (build-renderer "input-file"))

(render-fn {:bar {:baz "baz"}}) ;; returns "hello baz"

Extending template

file.txt content

foo
{% block %}
baz

Rendering input file with content:

{% extends "file.txt" %}
bar
(def render-fn (build-renderer "input-file"))

(render-fn {}) ;; returns "foo\nbar\nbaz"

Comments

input-file with content

foo{# bar baz #}
(def render-fn (build-renderer "input-file"))

(render-fn {}) ;; returns "foo"

csrf

CSRF token can be added via

{% csrf-token %}

and when rendering file :csrf-token has to be provided

(def render-fn (build-renderer "input-file"))

(render-fn {:csrf-token "foobarbaz"}) ;; returns <input type="hidden" name="csrf_token" value="foobarbaz"> 

Query string

input-file with content

/foo{% query-string foo %}
(def render-fn (build-renderer "input-file"))

(render-fn {:foo {:count 2}}) ;; returns "/foo?count=2"

Now

input-file with content

default format {% now %}
formatted {% now "yyyy-MM-dd" %}
formatted with tz {% now "yyyy-MM-dd hh:mm " "Asia/Tokyo" %}
(def render-fn (build-renderer "input-file"))

(render-fn {}) ;; returns "default format 2011/11/11 11:11\nformatted 2011-11-11\ntormatted with tz 2011-11-11 23:11"

Verbatim

input-file with content

{% verbatim %}foo{{bar}}{%baz%}{#qux#}quux{% endverbatim %}
(def render-fn (build-renderer "input-file"))

(render-fn {}) ;; returns "foo{{bar}}{%baz%}{#qux#}quux"

Debug

Currennt context can be printed out with debug tag

{% debug %}
(def render-fn (build-renderer "input-file"))

(render-fn {:number 1}) ;; prints out "{:number 1}" to console   

or if you want to write to custom Writer

{% debug writer-imp%}

and render file

(def render-fn (build-renderer "input-file"))

(render-fn {:number 1 :writer-imp (java.io.StringWriter.)}) ;; prints out "{:number 1}" to console   

Escape

If needed, Sanitizer implementation can be set/overridden via escape tag.

{% escape html %}foo{{bar}}{% endescape %}
(def render-fn (build-renderer "input-file"))

(render-fn {:bar "<div/>"}) ;; returns "&lt;div/&gt;"

Available values:

  • none
  • html
  • json

or ones provided under :environment :sanitizers

Translation

The trans tag translates a key using the configured Dictionary. The language is determined by the :locale key in the context.

{% trans hello %}
(def render-fn (build-renderer "input-file" {:environment {:dictionary my-dictionary}}))

(render-fn {:locale "fi"}) ;; returns the Finnish translation for :hello
(render-fn {:locale "en"}) ;; returns the English translation for :hello

Macro

{% macro foo %}foobar{{baz}}{% endmacro %}{% foo() %}{% foo() %}
{% macro greet(who) %}hello {{who}}!{% endmacro %}{% greet(user.name) %}
{% macro welcome(greeting, who) %}{{greeting}} {{who}}!{% endmacro %}{% welcome("hi", user.name) %}
(def render-fn (build-renderer "input-file"))

(render-fn {:baz "baz"}) ;; returns "foobarbazfoobarbaz"
(render-fn {:user {:name "alice"}}) ;; returns "hello alice!"
(render-fn {:user {:name "alice"}}) ;; returns "hi alice!"

Importing macros

Macros defined in another file can be imported with {% import "macros/macros.mjvt" %}:

{% macro greet(who) %}hello {{who}}!{% endmacro %}
{% import "macros/macros.mjvt" %}{% greet(user.name) %}
(def render-fn (build-renderer "input-file"))

(render-fn {:user {:name "alice"}}) ;; returns "hello alice!"

Defining a macro with a name that already exists — whether imported or defined in the current file — is a syntax error.

RenderTarget Protocol

render

Renders a template using the provided context.

  • template - template AST
  • context - Map of variables for template interpolation
  • error-handler - A record that implements ErrorHandler protocol

Returns - Rendered output

Built-in Implementations

StringRenderer

Returns rendered output as a String clojure

(->StringRenderer)

InputStreamRenderer

Returns rendered output as an InputStream for streaming large content

(->InputStreamRenderer)

PartialRenderer

Returns a partially rendered AST.

(->PartialRenderer)

TemplateResolver

The TemplateResolver protocol provides a uniform interface for accessing template content from different sources.

Protocol Methods

open-reader

Returns reader for that template, or nil if not found.

(open-reader "/templates/header.html")

template-exists?

Check if template exists at a path.

(template-exists? resolver "/templates/footer.html") ;; => true

Built-in Implementations

  • ResourceResolver (default) - Reads from classpath
  • FsResolver - Reads from filesystem

Sanitizer

Sanitizer protocol provides a way to sanitize and cleanup values.

Usage

(sanitize (->Html) "<foo>bar</baz>") ;; => &lt;foo&gt;bar&lt;/baz&gt;

Built-in Implementations

  • Html - implementation for html pages
  • Json - implementation for Json
  • None - Implementation that does not sanitize

Dictionary

The Dictionary protocol provides translation support for templates via the {% trans %} tag. The locale is read from the :locale key in the rendering context.

Protocol Methods

translate

Translates a word for the given language. Returns the translated string, or nil if no translation is found.

(translate dictionary locale word)

Example Implementation

(defrecord MapDictionary [translations]
  Dictionary
  (translate [_ language word]
    (get-in translations [language word])))

(def my-dictionary
  (->MapDictionary {"en" {:hello "hello" :world "world"}
                    "fi" {:hello "hei" :world "maailma"}}))

Pass it via the environment when building a renderer:

(def render-fn (build-renderer "input-file" {:environment {:dictionary my-dictionary}}))

(render-fn {:locale "en"}) ;; uses English translations
(render-fn {:locale "fi"}) ;; uses Finnish translations

ErrorHandler

The ErrorHandler protocol determines how template errors (syntax errors, missing files, unsupported filters) are handled during rendering.

Protocol Methods

handle-error

Handles a template error. Called when the parser returns an error map instead of a valid AST.

  • renderer - The renderer that encountered the error
  • template - A map containing error details (:type, :error-message, and optionally :line)
(handle-error error-handler renderer template)

Built-in Implementations

  • Reporting (default) - Renders the error as an HTML page showing the error type, message, and line number
  • FailFast - Throws an ExceptionInfo with the error details

Usage

(:require [jj.majavat.error-handler.fail-fast :refer [->FailFast]]
  [jj.majavat.error-handler.reporting :refer [->Reporting]])

(def render-fn (build-renderer "input-file" {:error-handler (->FailFast)}))
(def render-fn (build-renderer "input-file" {:error-handler (->Reporting)}))

Json

The Json protocol controls how the json filter turns a value into a JSON string. Majavat ships a built-in serializer, but you can supply your own (for example one backed by Jackson or Cheshire) and the filter will call it instead.

Protocol Methods

to-json

Serializes a value to a JSON string.

  • value - the value being serialized
  • opts - a map of options (may be nil); the built-in serializer honours {:indent n} for pretty-printing (the json(n) filter argument), custom implementations are free to ignore it
(to-json serializer value opts)

Built-in Implementation

  • DefaultJsonSerializer (default) - Compact JSON with optional pretty-printing via json(n). Handles nil, booleans, numbers (ratios as doubles, NaN/Infinity as null), strings, keywords, maps, and sequential/set collections.

Example Implementation

(:require [jj.majavat.protocol.json :refer [Json]])

(defrecord JacksonSerializer [mapper]
  Json
  (to-json [_ value _opts]
    (.writeValueAsString mapper value)))

Pass it via the environment when building a renderer:

(def render-fn (build-renderer "input-file" {:environment {:json-serializer (->JacksonSerializer object-mapper)}}))

Performance

Stress test was conducted rendering template 1000000 times using a standard web page with navigation, conditionals, loops, and nested data access.

EngineTotal TimePer RenderThroughputvs Majavat (String)
Majavat (String)10.8s10.8μs92,395/s1x (baseline)
Majavat (InputStream)16.3s16.3μs61,272/s1.51x slower
Hiccup22.1s22.1μs45,318/s2.04x slower
Selmer87.0s87.0μs11,499/s8.04x slower

Available Extensions

TODOS

  • Whitespace control using {%- -%} and {{- -}}
  • Boolean and and or expressions

License

Copyright © 2025 ruroru

This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which is available at https://www.eclipse.org/legal/epl-2.0/.

This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version, with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html.

Can you improve this documentation? These fine people already did:
jj, ruroru & Jj
Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close