2026-01-30 10:09:18 -10:00
2026-01-30 10:09:18 -10:00

Clojure-Typst

A Clojure-inspired Lisp that compiles to Typst, bringing s-expressions, functional programming, macros, and REPL-driven development to modern typesetting.

Dual Mode: Hiccup + Raw Escapes

Two modes coexist. Use whichever fits the moment.

;; HICCUP - when you need programmatic structure
[:heading {:level 1} "Title"]

;; RAW - when you just want to write
#t"= Title"

Mixing Freely

(def title "My Document")
(def data [{:name "Alice" :value 100}
           {:name "Bob" :value 200}])

(defn calculate-total [rows]
  (reduce + (map :value rows)))

(defn render-table [rows]
  [:table {:columns 2}
    (for [row rows]
      (list (:name row) (:value row)))])

[;; Hiccup for structured parts
 [:heading {:level 1} title]

 ;; Raw escape for prose-heavy sections
 #t"
 This is just regular Typst content. I can write *bold* and _italic_
 without all the brackets. Much nicer for prose.

 - Bullet one
 - Bullet two
 "

 ;; Back to hiccup for programmatic stuff
 (render-table data)

 ;; Raw with interpolation
 #t"The total is ~(calculate-total data)."

 ;; Nested: raw inside hiccup
 [:block {:inset "1em" :fill "gray.lighten(90%)"}
   #t"This is a _sidebar_ with easy formatting."]

 ;; Nested: hiccup inside raw
 #t"
 = Section

 Here's a dynamic table:
 ~(render-table data)

 And back to prose.
 "]
#heading(level: 1)[My Document]

This is just regular Typst content. I can write *bold* and _italic_
without all the brackets. Much nicer for prose.

- Bullet one
- Bullet two

#table(columns: 2, [Alice], [100], [Bob], [200])

The total is 300.

#block(inset: 1em, fill: gray.lighten(90%))[This is a _sidebar_ with easy formatting.]

= Section

Here's a dynamic table:
#table(columns: 2, [Alice], [100], [Bob], [200])

And back to prose.

Raw Escape Syntax

;; Short string
#t"= Heading"

;; Multi-line block
#t"
Multiple lines
of typst content
"

;; Content with quotes (must escape)
#t"He said \"hello\" and *left*."

Interpolation Inside Raw

Using ~() - the unquote concept from Clojure's syntax-quote:

;; Simple expression
(def answer 42)
#t"The value is ~answer."
The value is 42.
;; Hiccup expression returns content
(def url "https://example.com")
(def label "here")
#t"Click ~([:link {:src url} label]) to continue."
Click #link("https://example.com")[here] to continue.
;; Conditional
(def done true)
#t"Status: ~(if done 'Complete' 'Pending')"
Status: Complete
;; Loop
(def items [{:name "Apples"} {:name "Oranges"}])
#t"
= Items
~(for [i items]
    (str '- ' (:name i) '\n'))
"
= Items
- Apples
- Oranges

Hiccup Syntax

All elements follow standard Hiccup:

[:tag {attrs} children...]
  • tag: Element type (keyword)
  • attrs: Optional map of attributes
  • children: Content (strings, nested elements, or expressions)

Element Categories

Category Examples Children
Content elements heading, strong, emph, block Display content
Source elements link, image, bibliography link has display text; others have none
List elements list, enum, table Each child = one item/cell
Container elements figure Single content child
Math math Math expression string
References ref, cite None
Code raw Code string to display
Rules set, show Configuration

Reserved Attributes

Attr Used by Purpose
:src link, image, bibliography Source URL/path
:label Any element Makes element referenceable
:caption figure Figure caption text
:target ref Label to reference
:keys cite Citation key(s)
:element set, show Element type to configure
:block math, raw Display as block (default: inline)
:lang raw Code language for syntax highlighting

Basic Elements

;; Heading
[:heading {:level 2} "My Section"]
;; => #heading(level: 2)[My Section]

;; Inline formatting
["Hello " [:strong "world"] " and " [:emph "goodbye"]]
;; => Hello #strong[world] and #emph[goodbye]

;; Link
[:link {:src "https://typst.app"} "Typst"]
;; => #link("https://typst.app")[Typst]

;; Image
[:image {:src "diagram.png" :width "50%"}]
;; => #image("diagram.png", width: 50%)

Lists and Tables

Children are direct content, not wrapped in :item or :cell tags:

;; List
[:list "First" "Second" "Third"]
;; => #list([First], [Second], [Third])

;; Table
[:table {:columns 2}
  [:strong "Header 1"] [:strong "Header 2"]
  "Data 1" "Data 2"]
;; => #table(columns: 2, [#strong[Header 1]], [#strong[Header 2]], [Data 1], [Data 2])

Figures

Caption is an attribute; child element is the content:

[:figure {:caption "Architecture diagram" :label :fig/arch}
  [:image {:src "arch.png" :width "80%"}]]
;; => #figure(image("arch.png", width: 80%), caption: [Architecture diagram]) <fig:arch>

Math

;; Inline math (default)
[:math "x^2 + y^2"]
;; => $x^2 + y^2$

;; Block/display math
[:math {:block true} "sum_(i=0)^n i"]
;; => $ sum_(i=0)^n i $

Code Display

;; Inline code
[:raw "let x = 1"]
;; => `let x = 1`

;; Code block with language
[:raw {:lang "python" :block true} "print('hello')"]
;; => ```python print('hello') ```

References and Citations

[:ref {:target :fig/arch}]        ;; => @fig:arch
[:cite {:keys [:smith2020]}]      ;; => @smith2020

Document Settings

;; Set rules
[:set {:element :page :paper "a4" :margin "2cm"}]
;; => #set page(paper: "a4", margin: 2cm)

[:set {:element :text :font "New Computer Modern" :size "11pt"}]
;; => #set text(font: "New Computer Modern", size: 11pt)

;; Show rules
[:show {:element :heading}
  [:set {:element :text :fill "blue"}]]
;; => #show heading: set text(fill: blue)

Spacing and Paragraphs

[:h "2em"]    ;; => #h(2em)
[:v "1em"]    ;; => #v(1em)
[:parbreak]   ;; paragraph break
[:linebreak]  ;; line break within paragraph

Visualization

Three levels of visualization, all mappable to hiccup.

Built-in Shapes

[:rect {:width "2cm" :height "1cm" :fill "blue"} "Label"]
;; => #rect(width: 2cm, height: 1cm, fill: blue)[Label]

[:circle {:radius "0.5cm" :fill "green"}]
;; => #circle(radius: 0.5cm, fill: green)

[:line {:start [0 0] :end [100 50] :stroke "2pt"}]
;; => #line(start: (0pt, 0pt), end: (100pt, 50pt), stroke: 2pt)

[:polygon {:fill "yellow"} [0 0] [1 0] [0.5 1]]
;; => #polygon(fill: yellow, (0pt, 0pt), (1pt, 0pt), (0.5pt, 1pt))

CeTZ - Canvas Drawing

For complex custom drawings. Like TikZ for LaTeX.

[:cetz/canvas
  [:cetz/line [0 0] [2 2]]
  [:cetz/circle [1 1] {:radius 0.5}]
  [:cetz/rect [0 0] [2 1] {:fill "blue"}]]

Compiles to:

#import "@preview/cetz:0.4.2"

#cetz.canvas({
  import cetz.draw: *

  line((0, 0), (2, 2))
  circle((1, 1), radius: 0.5)
  rect((0, 0), (2, 1), fill: blue)
})

Fletcher - Diagrams & Flowcharts

High-level diagram DSL for architecture, flowcharts, state machines.

[:fletcher/diagram {:node-stroke "1pt" :spacing "2em"}
  [:node [0 0] "Start"]
  [:edge "->"]
  [:node [1 0] "Process"]
  [:edge "->"]
  [:node [2 0] "End"]]

Node shapes: :circle, :diamond, :pill, :hexagon, :cylinder

Edge styles: "->", "->>", "--", "<->", "hook->"

;; Flowchart example
[:fletcher/diagram {:node-stroke "1pt" :node-fill "white" :spacing "3em"}
  [:node [0 0] "Start" {:shape :pill}]
  [:node [0 1] "Input Data"]
  [:node [0 2] "Valid?" {:shape :diamond}]
  [:node [1 2] "Error" {:fill "red.lighten(80%)"}]
  [:node [0 3] "Process"]
  [:node [0 4] "End" {:shape :pill}]

  [:edge [0 0] [0 1] "->"]
  [:edge [0 1] [0 2] "->"]
  [:edge [0 2] [1 2] "->" {:label "no"}]
  [:edge [0 2] [0 3] "->" {:label "yes"}]
  [:edge [1 2] [0 1] "->" {:bend -40}]
  [:edge [0 3] [0 4] "->"]]

DSL Helpers

Build higher-level abstractions:

(defn service [id name & {:keys [x y color] :or {color "white"}}]
  [:node [x y] name {:id id :shape :pill :fill color}])

(defn database [id name & {:keys [x y color] :or {color "gray.lighten(80%)"}}]
  [:node [x y] name {:id id :shape :cylinder :fill color}])

(defn connects [from to & {:keys [label style] :or {style "->"}}]
  [:edge from to style (when label {:label label})])

[:fletcher/diagram {:spacing "3em"}
  (service :auth "Auth" :x 0 :y 0)
  (service :users "Users" :x 1 :y 0)
  (database :pg "PostgreSQL" :x 0 :y 1)
  (database :redis "Redis" :x 1 :y 1 :color "red.lighten(80%)")

  (connects :auth :pg)
  (connects :users :redis :label "cache")]

Macros

It's a Lisp - of course we have macros.

Content Macros

;; Define a reusable content pattern
(defmacro defcomponent [name args & body]
  `(defn ~name ~args
     (list ~@body)))

;; Conditional content sections
(defmacro when-content [test & body]
  `(when ~test
     (list ~@body)))

;; Use it
(def show-abstract true)
(def abstract-text "This paper presents...")

(when-content show-abstract
  [:heading {:level 2} "Abstract"]
  abstract-text)
== Abstract
This paper presents...

DSL Macros

;; Shorthand macros for references
(defmacro ref [id]
  `[:ref {:target ~id}])

(defmacro cite [& keys]
  `[:cite {:keys [~@keys]}])

(ref :fig/arch)
(cite :smith2020 :jones2021)
@fig:arch
@smith2020 @jones2021
;; Math helper functions
(defn sum [var from to body]
  (str "sum_(" var "=" from ")^" to " " body))

(defn frac [num denom]
  (str "(" num ") / (" denom ")"))

(defn pow [base exp]
  (str base "^" exp))

;; Math block macro
(defmacro math-block [& body]
  `[:math {:block true} (str ~@body)])

(math-block (sum "i" 0 "n" (frac (pow "x" "i") "i!")))
$ sum_(i=0)^n (x^i) / (i!) $

Template Macros

;; Helper functions for template rendering
(defn render-author [{:keys [name affil]}]
  [:block {:align "center"}
    [:strong name] [:linebreak]
    [:emph affil]])

(defn render-authors [authors]
  (map render-author authors))

(defn render-abstract [text]
  [[:heading {:level 2} "Abstract"]
   [:block {:inset "1em"} [:emph text]]])

;; Template macro - captures body and prepends document setup
(defmacro deftemplate [name params & preamble]
  `(defmacro ~name [~@params & ~'body]
     (concat ~@preamble ~'body)))

(deftemplate ieee-paper [title authors abstract]
  [[:set {:element :page :paper "us-letter" :columns 2}]
   [:set {:element :text :size "10pt"}]
   [:heading {:level 1 :align "center"} title]
   (render-authors authors)
   (render-abstract abstract)])

;; Use the template
(ieee-paper
  "Neural Networks for Fun and Profit"
  [{:name "Alice" :affil "MIT"}]
  "We present a novel approach..."

  [:heading {:level 2} "Introduction"]
  "The problem of machine learning..."

  [:heading {:level 2} "Methods"]
  "Our method consists of...")
#set page(paper: "us-letter", columns: 2)
#set text(size: 10pt)
#heading(level: 1, align: center)[Neural Networks for Fun and Profit]
#block(align: center)[#strong[Alice] \ #emph[MIT]]
== Abstract
#block(inset: 1em)[#emph[We present a novel approach...]]
== Introduction
The problem of machine learning...
== Methods
Our method consists of...

Full Example

(ns my-paper.core
  (:require [clojure-typst.core :refer :all]))

(def authors
  [{:name "Alice Chen" :affiliation "MIT" :email "alice@mit.edu"}
   {:name "Bob Smith" :affiliation "Stanford" :email "bob@stanford.edu"}])

(def results
  [{:method "Baseline" :accuracy 0.72}
   {:method "Ours" :accuracy 0.94}])

(defn author-card [{:keys [name affiliation email]}]
  [:block {:inset "1em"}
    [:strong name] [:linebreak]
    [:emph affiliation] [:linebreak]
    [:link {:src (str "mailto:" email)} email]])

(defn results-table [data]
  [:table {:columns 2 :align ["left" "right"]}
    [:strong "Method"] [:strong "Accuracy"]
    (for [{:keys [method accuracy]} data]
      (list
        method
        (format "%.1f%%" (* 100 accuracy))))])

(def paper
  [[:set {:element :page :paper "a4"}]
   [:set {:element :text :font "Linux Libertine"}]

   [:heading {:level 1} "A Very Important Paper"]

   [:block {:align "center"}
     (interpose [:h "2em"]
       (map author-card authors))]

   #t"
   = Abstract

   We present a novel approach to solving important problems.
   Our method achieves *state-of-the-art* results on several
   benchmarks while maintaining computational efficiency.

   = Introduction

   The problem of _important things_ has long challenged researchers.
   Previous approaches @citation1 @citation2 have made progress but
   fundamental limitations remain.
   "

   [:figure {:caption "System architecture overview." :label :fig/arch}
     [:image {:src "architecture.png" :width "80%"}]]

   #t"
   Our method works by first processing the input through
   the encoder (see @fig:arch).

   = Results
   "

   (results-table results)

   #t"
   As shown in the table above, our method significantly
   outperforms the baseline.

   = Conclusion

   We have demonstrated that our approach is ~(if (> (:accuracy (last results)) 0.9)
                                                  \"highly effective\"
                                                  \"somewhat effective\").
   "

   [:bibliography {:src "refs.bib"}]])

(compile-to-typst paper "paper.typ")
#set page(paper: "a4")
#set text(font: "Linux Libertine")

#heading(level: 1)[A Very Important Paper]

#block(align: center)[
  #block(inset: 1em)[
    #strong[Alice Chen] \
    #emph[MIT] \
    #link("mailto:alice@mit.edu")[alice@mit.edu]
  ]
  #h(2em)
  #block(inset: 1em)[
    #strong[Bob Smith] \
    #emph[Stanford] \
    #link("mailto:bob@stanford.edu")[bob@stanford.edu]
  ]
]

= Abstract

We present a novel approach to solving important problems.
Our method achieves *state-of-the-art* results on several
benchmarks while maintaining computational efficiency.

= Introduction

The problem of _important things_ has long challenged researchers.
Previous approaches @citation1 @citation2 have made progress but
fundamental limitations remain.

#figure(
  image("architecture.png", width: 80%),
  caption: [System architecture overview.]
) <fig:arch>

Our method works by first processing the input through
the encoder (see @fig:arch).

= Results

#table(
  columns: 2,
  align: (left, right),
  [#strong[Method]], [#strong[Accuracy]],
  [Baseline], [72.0%],
  [Ours], [94.0%]
)

As shown in the table above, our method significantly
outperforms the baseline.

= Conclusion

We have demonstrated that our approach is highly effective.

#bibliography("refs.bib")

Rules Summary

  1. #t"..." - raw Typst content, returned as-is
  2. ~(...) inside raw - interpolate Clojure expression
  3. [:tag ...] - hiccup, compiled to Typst function calls
  4. Strings in hiccup - become Typst content directly
  5. Sequences flatten - (for ...), (map ...), (list ...) results merge into parent
Situation Use
Prose, paragraphs, natural writing Raw #t"..."
Tables, figures, structured layout Hiccup
Loops, conditionals, data-driven Hiccup or interpolated raw
Components, reusable parts Hiccup functions

The principle: Structure when you need it, prose when you don't.

Description
a clojure dialect and set of tools for writing typst documents
Readme 70 KiB