4.4 KiB
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
- Implicit paragraphs? Should adjacent strings auto-wrap in
[:p ...]? - Shorthand syntax? Maybe
:#stronginstead of[:strong ...]? - Component system? How fancy do we get with reusable components?
- Escaping? How to include literal
[or other special chars? - 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.