Files
typlisp/hiccup-prototype.md
2026-01-30 09:58:55 -10:00

4.4 KiB

Hiccup for Typst Content - Prototype

The Idea

Hiccup represents markup as nested data structures:

[:tag {attrs} & children]

Syntax Mapping

Clojure-Typst Hiccup → Typst Output

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

;; Inline formatting (paragraphs are implicit in Typst)
["Hello " [:strong "world"] " and " [:emph "goodbye"]]
;; => Hello #strong[world] and #emph[goodbye]

;; Lists (children are direct content, not :item tags)
[:list
  "First thing"
  "Second thing"
  [[:strong "Important"] " third thing"]]
;; => #list(
;;      [First thing],
;;      [Second thing],
;;      [#strong[Important] third thing]
;;    )

;; Tables (cells are direct content, not :cell tags)
[:table {:columns 3}
  "A" "B" "C"
  "1" "2" "3"]
;; => #table(columns: 3,
;;      [A], [B], [C],
;;      [1], [2], [3]
;;    )

;; Code blocks (use :raw for displayed code)
[:raw {:lang "python" :block true} "print('hello')"]
;; => #raw(lang: "python", block: true, "print('hello')")

;; Math (inline by default)
[:math "x^2 + y^2 = z^2"]
;; => $x^2 + y^2 = z^2$

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

;; Images (src in attrs, no children)
[:image {:src "diagram.png" :width "50%"}]
;; => #image("diagram.png", width: 50%)

;; Links (src in attrs, child is display text)
[:link {:src "https://typst.app"} "Typst"]
;; => #link("https://typst.app")[Typst]

Full Document Example

(def my-doc
  [[:heading {:level 1} "My Paper"]

   "This is the introduction. We discuss "
   [:emph "important things"] "."

   [:parbreak]

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

   "We used the following approach:"

   [:list
     "Step one"
     "Step two"
     "Step three"]

   [:heading {:level 2} "Results"]

   "The result is " [:math "x = 42"] "."

   [:table {:columns 2}
     [:strong "Input"] [:strong "Output"]
     "1" "1"
     "2" "4"
     "3" "9"]])

Compiles to:

#heading(level: 1)[My Paper]

This is the introduction. We discuss #emph[important things].

#heading(level: 2)[Methods]

We used the following approach:

#list(
  [Step one],
  [Step two],
  [Step three]
)

#heading(level: 2)[Results]

The result is $x = 42$.

#table(columns: 2,
  [#strong[Input]], [#strong[Output]],
  [1], [1],
  [2], [4],
  [3], [9]
)

Mixing Code and Content

;; Define a reusable component
(defn author-block [name institution]
  [:block {:align "center"}
    [:strong name]
    [:linebreak]
    [:emph institution]])

;; Use it with data
(def authors
  [{:name "Alice" :inst "MIT"}
   {:name "Bob" :inst "Stanford"}])

;; Generate content programmatically
[[:heading "Authors"]
 (for [{:keys [name inst]} authors]
   (author-block name inst))]

Special Forms for Content

;; Conditional content (strings become paragraphs naturally)
(if peer-reviewed?
  "This paper was peer reviewed."
  "Preprint - not yet reviewed.")

;; Loop over data (items are direct content in lists)
[:list
  (for [item items]
    (format-item item))]

;; Let bindings for intermediate content
(let [title (get-title data)
      abstract (get-abstract data)]
  [[:heading title]
   abstract])

The Alternative: Lisp for Code Only

If hiccup feels too heavy, we could just use Lisp for logic and escape to raw Typst:

(defn factorial [n]
  (if (<= n 1)
    1
    (* n (factorial (dec n)))))

;; Escape to typst content
#t"
= Results

The factorial of 5 is ~(factorial 5).

Here's a table of factorials:
~(for [i (range 1 11)]
   (str \"- \" i \"! = \" (factorial i) \"\\n\"))
"

Questions

  1. Implicit paragraphs? Should adjacent strings auto-wrap in [:p ...]?
  2. Shorthand syntax? Maybe :#strong instead of [:strong ...]?
  3. Component system? How fancy do we get with reusable components?
  4. Escaping? How to include literal [ or other special chars?
  5. Whitespace handling? Typst is whitespace-sensitive in content mode

Verdict?

Hiccup gives us:

  • Pure data representation of documents
  • Full programmatic control
  • Composable components
  • Familiar to Clojure devs

But:

  • More verbose than raw Typst markup
  • Loses Typst's nice content syntax
  • Another layer to learn

Hybrid approach might be best: Lisp for code/logic, with easy escape to Typst content syntax when you just want to write prose.