Skip to content

ruroru/majavat

Repository files navigation

Majavat

A templating engine for Clojure

Installation

Add majavat to dependency list

[org.clojars.jj/majavat "2.2.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:

Option Default Value Supported Options
renderer StringRenderer Any Renderer implementation
cache? true true, false
template-resolver ResourceResolver TemplateResolver
pre-render {} Map
sanitizer nil Any Sanitizer implementation
environment {} Map (see environment options)
error-handler Reporting Any ErrorHandler Implementation

Environment

Option Default Value Supported Options
filters {} Map
sanitizers {} Keyword -> Sanitizer Map
dictionary nil Any 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
Filter Type Example
append String "hello" | append " world" → "hello world"
capitalize String "hello world" → "Hello world"
int String "123" → 123
long String "123" → 123L
lower-case String "HELLO WORLD" → "hello world"
prepend String "hello" | prepend " world" → "world hello"
slugify String "Foo Bar" → "foo-bar"
title-case String "hello world" → "Hello World"
trim String " hello " → "hello"
upper-case String "hello world" → "HELLO WORLD"
upper-roman String "iv" → "IV"
name Keyword :name → "name"
abs Number -1 → 1.0
ceil Number 1.99 → 2
dec Number 5 → 4
file-size Number 2048 → "2 KB"
floor Number 1.4 → 1.0
inc Number 5 → 6
round Number 1.99 → 2
first Sequential (list :foo :bar :baz) -> :foo
rest Sequential (list :foo :bar :baz) -> (list :bar :baz)
first Map {:foo :a :bar :b :baz :c} -> [:foo :a]
rest Map {:foo :a :bar :b :baz :c} -> {:bar :b :baz :c}
default "foo" nil nil → "foo"
date "yyyy" LocalDate Instance of LocalDate → "2025"
date "yyyy" LocalDateTime Instance of LocalDateTime → "2025"
date "hh/mm" LocalTime Instance of LocalTime → "11/11"
date "hh/mm" "Asia/Tokyo" Instant Instance of Instant → "11/11"
date "hh/mm" "Asia/Tokyo" ZonedDateTime Instance of ZonedDateTime → "11/11"
User Provided filters Filters

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

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

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 name args example
even - {% if value is even %}
odd - {% if value is odd %}

Comparison operators

operator example
== {% if value == 0 %}
{% if value == "value" %}

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) %}
(def render-fn (build-renderer "input-file"))

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

RenderTarget Protocol

render

Renders a template using the provided context.

  • template - template AST
  • context - Map of variables for template interpolation
  • sanitizer - A record that implements Sanitizer 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)
  • sanitizer - The sanitizer in use
(handle-error error-handler renderer template sanitizer)

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)}))

Performance

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

Engine Total Time Per Render Throughput vs Majavat (String)
Majavat (String) 10.8s 10.8μs 92,395/s 1x (baseline)
Majavat (InputStream) 16.3s 16.3μs 61,272/s 1.51x slower
Hiccup 22.1s 22.1μs 45,318/s 2.04x slower
Selmer 87.0s 87.0μs 11,499/s 8.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.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages