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
#t"..."- raw Typst content, returned as-is~(...)inside raw - interpolate Clojure expression[:tag ...]- hiccup, compiled to Typst function calls- Strings in hiccup - become Typst content directly
- 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.