208 lines
4.3 KiB
Markdown
208 lines
4.3 KiB
Markdown
# Hiccup for Typst Content - Prototype
|
|
|
|
## The Idea
|
|
|
|
Hiccup represents markup as nested data structures:
|
|
```clojure
|
|
[:tag {attrs} & children]
|
|
```
|
|
|
|
## Syntax Mapping
|
|
|
|
### Clojure-Typst Hiccup → Typst Output
|
|
|
|
```clojure
|
|
;; 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
|
|
|
|
```clojure
|
|
(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:**
|
|
|
|
```typst
|
|
#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
|
|
|
|
```clojure
|
|
;; 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"]
|
|
(map #(author-block (:name %) (:inst %)) authors)]
|
|
```
|
|
|
|
## Special Forms for Content
|
|
|
|
```clojure
|
|
;; 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:
|
|
|
|
```clojure
|
|
(defn factorial [n]
|
|
(if (<= n 1)
|
|
1
|
|
(* n (factorial (dec n)))))
|
|
|
|
;; Escape to typst content
|
|
#typst{
|
|
= 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.
|