Files
typlisp/dual-mode.md
2026-01-30 09:45:24 -10:00

9.0 KiB

Dual Mode: Hiccup + Raw Escapes

Both systems coexist. Use whichever fits the moment.

The Two Modes

;; 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 (use :block for container)
 [: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.
 "]

Escape Syntax Options

;; Short string
#t"= Heading"

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

;; Triple-quote for content with quotes
#t"""
He said "hello" and *left*.
"""

;; With explicit delimiters (if we need)
#typst{
  Content here with {braces} works fine
}

;; Tagged for syntax highlighting in editors?
#typst/content "..."

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

;; Or loop returning hiccup (items are direct list children)
#t"
= Items
~[:list (for [i items] (:name i))]
"

Same mental model as macro unquote:

`(let [x ~expr] ...)   ; macro: escape to eval
#t"Value: ~(expr)"     ; template: escape to eval

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
  [[:heading {:level 1} "A Very Important Paper"]

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

    ;; Prose in raw mode - much nicer to write
    #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.

    In this work, we propose a new framework that addresses these
    limitations directly. Our key contributions are:

    + A novel architecture for processing data
    + Theoretical analysis of convergence properties
    + Extensive empirical evaluation

    = Methods
    "

    ;; Back to hiccup for the technical diagram
    [: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). The encoded representation
    is then passed to the decoder which produces the output.

    = Results
    "

    ;; Programmatic table
    (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").
    Future work will explore extensions to other domains.
    "

    ;; Bibliography could be generated
    [:bibliography {:src "refs.bib"}]])

;; Compile it
(compile-to-typst paper "paper.typ")

Macros

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

Standard Code Macros

;; Threading for cleaner data transforms
(defmacro -> [x & forms]
  ...)

(-> data
    (filter :active)
    (map :name)
    (sort))

;; Custom control flow
(defmacro when-let [[binding expr] & body]
  `(let [~binding ~expr]
     (when ~binding ~@body)))

Content Macros

Macros that expand to hiccup/content:

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

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

;; Use it
(when-content show-abstract?
  [:heading {:level 2} "Abstract"]
  abstract-text)

Document Structure Macros

;; Academic paper structure
(defmacro paper [& {:keys [title authors abstract] :as opts} & sections]
  `[[:heading {:level 1} ~title]
    (author-block ~authors)
    (when ~abstract
      (list
        [:heading {:level 2} "Abstract"]
        ~abstract))
    ~@sections])

;; Usage
(paper
  :title "My Great Paper"
  :authors ["Alice" "Bob"]
  :abstract "We did cool stuff."

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

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

DSL Macros

Build mini-languages for specific domains:

;; Math DSL that's easier to write than raw Typst math
(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! $

;; Table DSL
(defmacro deftable [name headers & row-specs]
  ...)

(deftable results-table
  ["Method" "Precision" "Recall" "F1"]
  :from results
  :row (fn [{:keys [method p r f1]}]
         [method (fmt p) (fmt r) (fmt f1)]))

;; Figure DSL with auto-numbering
(defmacro figure [id & {:keys [src caption width]}]
  `(do
     (register-figure! ~id)
     [:figure {:label ~id :caption ["Figure " (figure-num ~id) ": " ~caption]}
       [:image {:src ~src :width ~(or width "100%")}]]))

(figure :arch
  :src "architecture.png"
  :caption "System overview"
  :width "80%")

Syntax Extension Macros

;; Custom reader syntax for common patterns
;; (would require reader macro support)

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

;; Shorthand for citations
(defmacro cite [& keys]
  `[:cite {:keys [~@keys]}])

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

Template Macros

;; Define document templates
(defmacro deftemplate [name args & structure]
  `(defmacro ~name ~args
     ~@structure))

(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])  ; splice remaining content

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

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

Why Macros Matter Here

Pattern Without Macros With Macros
Repeated structure Copy-paste hiccup defcomponent
Conditional sections Verbose if expressions when-content
Domain languages Manual compilation DSL macros
Document templates Function composition Template macros
Boilerplate elimination Nowhere to hide it Macro it away

The power: Your document language grows to fit your domain.

Rules

  1. #t"..." - raw Typst content, returned as-is
  2. ~(...) inside raw - interpolate Clojure expression (like unquote)
  3. [:tag ...] - hiccup, compiled to Typst function calls
  4. Strings in hiccup - become Typst content directly
  5. Seqs in hiccup - flattened (so map/for just work)
  6. Sequences flatten - (for ...), (map ...), (list ...) results merge into parent

Why Both?

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
Quick one-off formatting Either, your preference

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