Skip to content

Templates

(kaappi template) — text and HTML template engine with variables, conditionals, loops, and auto-escaping.

thottam install kaappi-template

Pure Scheme — no dependencies.

Quick start

(import (kaappi template))

;; Plain text
(template-render "Hello, {{.name}}!" '(("name" . "Alice")))
;=> "Hello, Alice!"

;; HTML (auto-escapes <>&"')
(template-render-html "<p>{{.content}}</p>"
  '(("content" . "<script>alert('xss')</script>")))
;=> "<p>&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;</p>"

Template syntax

Variables

{{.name}}           key from the data alist
{{.user.email}}     nested dotted path
{{.}}               current value (inside range loops)

Undefined keys render as empty strings.

Conditionals

{{if .logged_in}}Welcome back!{{end}}

{{if .logged_in}}
  Welcome, {{.username}}!
{{else}}
  Please log in.
{{end}}

Falsy values: #f, 'null, "", (), 0. Everything else is truthy.

Loops

{{range .items}}
  {{.name}}: ${{.price}}
{{end}}

Inside range, {{.}} refers to the current item. Use {{.field}} to access fields when items are objects (alists).

Loop over a flat list

(template-render "{{range .names}}Hello {{.}}! {{end}}"
  '(("names" . ("Alice" "Bob" "Carol"))))
;=> "Hello Alice! Hello Bob! Hello Carol! "

Loop over objects

(template-render "{{range .users}}{{.name}} ({{.role}})\n{{end}}"
  '(("users" . ((("name" . "Alice") ("role" . "admin"))
                 (("name" . "Bob") ("role" . "user"))))))
;=> "Alice (admin)\nBob (user)\n"

HTML escaping

template-render-html escapes these characters in all variable output:

Character Escape
< &lt;
> &gt;
& &amp;
" &quot;
' &#39;

This prevents XSS attacks. Use template-render (plain text) only when the output is not HTML.

Data format

Data is passed as an alist. Nested data uses nested alists:

(define data
  '(("user" . (("name" . "Alice")
               ("email" . "alice@example.com")))
    ("items" . ((("name" . "Widget") ("price" . 9.99))
                (("name" . "Gadget") ("price" . 24.50))))
    ("show_footer" . #t)))

Template:

{{.user.name}} ({{.user.email}})

Items:
{{range .items}}
- {{.name}}: ${{.price}}
{{end}}

{{if .show_footer}}Generated by Kaappi{{end}}

Pre-parsing for performance

Parse a template once, render many times:

(define tmpl (template-parse "Hello, {{.name}}!"))

(template-execute tmpl '(("name" . "Alice")) values)
;=> "Hello, Alice!"

(template-execute tmpl '(("name" . "Bob")) values)
;=> "Hello, Bob!"

The third argument to template-execute is the escape function: - values — no escaping (identity function) - html-escape — HTML entity escaping

Performance tip

For templates rendered on every HTTP request, parse once at startup:

(define page-ast (template-parse page-template))

(GET "/"
  (lambda (req params)
    (html-response
      (template-execute page-ast data html-escape))))

HTML escaping utility

The html-escape function is available independently:

(html-escape "<script>alert('xss')</script>")
;=> "&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;"

(html-escape "safe text")
;=> "safe text"

Common patterns

Layout with content blocks

(define layout "<html><head><title>{{.title}}</title></head>
<body><nav>{{.nav}}</nav>{{.body}}</body></html>")

(define (render-page title body-html)
  (template-render layout
    `(("title" . ,(html-escape title))
      ("nav" . "<a href='/'>Home</a>")
      ("body" . ,body-html))))

The layout is rendered with template-render (no auto-escaping) because nav and body-html are markup that must land verbatim; plain data values like the title are escaped explicitly with html-escape. Render the body fragments themselves with template-render-html so their data stays auto-escaped.

Conditional CSS classes

<div class="alert {{if .error}}alert-error{{else}}alert-info{{end}}">
  {{.message}}
</div>

Empty state handling

{{if .items}}
<ul>
  {{range .items}}<li>{{.name}}</li>{{end}}
</ul>
{{else}}
<p>No items found.</p>
{{end}}

API reference

Procedure Description
(template-render tmpl data) Render with no escaping
(template-render-html tmpl data) Render with HTML escaping
(template-parse tmpl) Parse to reusable AST
(template-execute ast data escape-fn) Execute AST with custom escaping
(html-escape str) Escape HTML entities