2026-01-30 09:58:55 -10:00
2026-01-30 09:58:55 -10:00
2026-01-30 09:58:55 -10:00
2026-01-30 09:11:34 -10:00
2026-01-30 09:58:55 -10:00
2026-01-30 09:45:24 -10:00
2026-01-30 09:11:34 -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

[;; 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
 [:table {:columns 2}
   (for [row data]
     (list (:name row) (:value row)))]

 ;; Raw with interpolation
 #t"The answer is ~(calculate-answer 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 results)

 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
#t"The value is ~(expr)."

;; Hiccup expression returns content
#t"Click ~([:link {:src url} label]) to continue."

;; Conditional
#t"Status: ~(if done? 'Complete' 'Pending')"

;; Loop
#t"
= Items
~(for [i items]
    (str '- ' (:name i) '\n'))
"

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
(when-content show-abstract?
  [:heading {:level 2} "Abstract"]
  abstract-text)

DSL Macros

;; Math DSL
(defmacro math-block [& exprs]
  `[:math {:block true}
     ~(compile-math-dsl exprs)])

(math-block
  (sum :i 0 :n
    (frac (pow x i) (factorial i))))
;; => $ sum_(i=0)^n x^i / i! $

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

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

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

Template Macros

(deftemplate ieee-paper [title authors abstract]
  [[:set {:element :page :paper "us-letter" :columns 2}]
   [:heading {:level 1 :align "center"} ~title]
   (render-authors ~authors)
   (render-abstract ~abstract)
   ~'&body])

(ieee-paper
  "Neural Networks for Fun and Profit"
  [{:name "Alice" :affil "MIT"}]
  "We present..."

  [:heading {:level 2} "Introduction"]
  "..."
  [:heading {:level 2} "Methods"]
  "...")

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

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