Parser: precedes/follows comparison operators in parse-cmp. Tokenizer: precedes, follows, ignoring, case keywords. Runtime: precedes?, follows? string comparison functions. 372/831 (45%) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
285 lines
11 KiB
Plaintext
285 lines
11 KiB
Plaintext
;; Pretext demo — DOM-free text layout
|
||
;;
|
||
;; Visual-first: shows typeset text, then explains how.
|
||
;; All layout computed as data, then rendered.
|
||
|
||
;; Compute positioned word data for one line.
|
||
;; Returns list of {:word :x :width} dicts.
|
||
(define
|
||
pretext-position-line
|
||
(fn
|
||
(words widths gap-w)
|
||
(let
|
||
loop
|
||
((i 0) (x 0) (acc (list)))
|
||
(if
|
||
(>= i (len words))
|
||
acc
|
||
(loop
|
||
(+ i 1)
|
||
(+ x (nth widths i) gap-w)
|
||
(append acc (list {:width (nth widths i) :x x :word (nth words i)})))))))
|
||
|
||
;; Compute all positioned lines for a paragraph.
|
||
;; Returns list of {:y :words [{:word :x :width}...]} dicts.
|
||
(define
|
||
pretext-layout-lines
|
||
(fn
|
||
(words widths ranges space-width max-width line-height)
|
||
(let
|
||
((n-lines (len ranges)))
|
||
(map
|
||
(fn
|
||
(line-idx)
|
||
(let
|
||
((range (nth ranges line-idx)) (y (* line-idx line-height)))
|
||
(let
|
||
((start (first range)) (end (nth range 1)))
|
||
(let
|
||
((lw (slice words start end))
|
||
(lwid (slice widths start end)))
|
||
(let
|
||
((total-w (reduce + 0 lwid))
|
||
(n-gaps (max 1 (- (len lw) 1)))
|
||
(is-last (= line-idx (- n-lines 1))))
|
||
(let
|
||
((gap (if is-last space-width (/ (- max-width total-w) n-gaps))))
|
||
{:y y :words (pretext-position-line lw lwid gap)}))))))
|
||
(range n-lines)))))
|
||
|
||
;; Render pre-computed positioned lines
|
||
(defcomp
|
||
~pretext-demo/render-paragraph
|
||
(&key lines max-width line-height n-words label)
|
||
(let
|
||
((lh (or line-height 24)) (n-lines (len lines)))
|
||
(div
|
||
:class "relative rounded-lg border border-stone-200 bg-white overflow-hidden"
|
||
(when
|
||
label
|
||
(div
|
||
:class "px-4 pt-3 pb-1"
|
||
(span
|
||
:class "text-xs font-medium uppercase tracking-wide text-stone-400"
|
||
label)))
|
||
(div
|
||
:style (str
|
||
"position:relative;height:"
|
||
(* n-lines lh)
|
||
"px;padding:12px 16px;")
|
||
(map
|
||
(fn
|
||
(line)
|
||
(let
|
||
((y (get line :y)))
|
||
(map
|
||
(fn
|
||
(pw)
|
||
(span
|
||
:style (str
|
||
"position:absolute;left:"
|
||
(+ (get pw :x) 16)
|
||
"px;top:"
|
||
(+ y 12)
|
||
"px;font-size:15px;line-height:"
|
||
lh
|
||
"px;white-space:nowrap;")
|
||
(get pw :word)))
|
||
(get line :words))))
|
||
lines))
|
||
(div
|
||
:class "px-4 py-2 border-t border-stone-100 bg-stone-50 flex justify-between"
|
||
(span
|
||
:class "text-xs text-stone-400"
|
||
(str n-lines " lines, " n-words " words"))
|
||
(span :class "text-xs text-stone-400" (str "width: " max-width "px"))))))
|
||
|
||
(defcomp
|
||
~pretext-demo/content
|
||
()
|
||
(let
|
||
((sample-words (split "In the beginning was the Word, and the Word was with God, and the Word was God. The same was in the beginning with God. All things were made by him; and without him was not any thing made that was made. In him was life; and the life was the light of men." " "))
|
||
(char-w 9.6)
|
||
(space-w 9.6))
|
||
(let
|
||
((sw (map (fn (w) (* (len w) char-w)) sample-words))
|
||
(n-words (len sample-words)))
|
||
(div
|
||
(~tw :tokens "space-y-10")
|
||
(div
|
||
(~tw :tokens "space-y-4")
|
||
(div
|
||
(h1
|
||
(~tw :tokens "text-3xl font-bold text-stone-900 tracking-tight")
|
||
"Pretext")
|
||
(p
|
||
(~tw :tokens "mt-1 text-lg text-stone-500")
|
||
"DOM-free text layout. One IO boundary. Pure arithmetic."))
|
||
(let
|
||
((hero-max 520) (hero-ranges (break-lines sw space-w 520)))
|
||
(div
|
||
(~tw :tokens "max-w-xl mx-auto mt-6")
|
||
(~pretext-demo/render-paragraph
|
||
:lines (pretext-layout-lines
|
||
sample-words
|
||
sw
|
||
hero-ranges
|
||
space-w
|
||
hero-max
|
||
24)
|
||
:max-width hero-max
|
||
:n-words n-words
|
||
:label "Knuth-Plass optimal line breaking — John 1:1–4"))))
|
||
(div
|
||
(~tw :tokens "rounded-lg border border-violet-200 bg-violet-50 p-5")
|
||
(p
|
||
(~tw :tokens "text-sm text-violet-800")
|
||
(strong "One ")
|
||
(code (~tw :tokens "bg-violet-100 px-1 rounded") "perform")
|
||
" for glyph measurement. Everything else — line breaking, positioning, hyphenation, justification — is pure SX functions over numbers. "
|
||
"Server renders with font-table lookups. Browser uses "
|
||
(code "canvas.measureText")
|
||
". Same algorithm, same output."))
|
||
(div
|
||
(~tw :tokens "space-y-3")
|
||
(h2
|
||
(~tw :tokens "text-xl font-semibold text-stone-800")
|
||
"Greedy vs optimal")
|
||
(p
|
||
(~tw :tokens "text-sm text-stone-500")
|
||
"Most web text uses greedy word wrap — break when the next word doesn't fit. "
|
||
"Knuth-Plass considers all possible breaks simultaneously, minimizing total raggedness.")
|
||
(let
|
||
((nw (map (fn (w) (* (len w) 7.8)) sample-words))
|
||
(ns 7.8)
|
||
(nm 340)
|
||
(nlh 22))
|
||
(div
|
||
(~tw :tokens "grid grid-cols-1 md:grid-cols-2 gap-4")
|
||
(~pretext-demo/render-paragraph
|
||
:lines (pretext-layout-lines
|
||
sample-words
|
||
nw
|
||
(break-lines-greedy nw ns nm)
|
||
ns
|
||
nm
|
||
nlh)
|
||
:max-width nm
|
||
:line-height nlh
|
||
:n-words n-words
|
||
:label "Greedy (browser default)")
|
||
(~pretext-demo/render-paragraph
|
||
:lines (pretext-layout-lines
|
||
sample-words
|
||
nw
|
||
(break-lines nw ns nm)
|
||
ns
|
||
nm
|
||
nlh)
|
||
:max-width nm
|
||
:line-height nlh
|
||
:n-words n-words
|
||
:label "Knuth-Plass optimal"))))
|
||
(div
|
||
(~tw :tokens "space-y-3")
|
||
(h2
|
||
(~tw :tokens "text-xl font-semibold text-stone-800")
|
||
"How lines are scored")
|
||
(p
|
||
(~tw :tokens "text-sm text-stone-500")
|
||
"Each line gets a badness score — how far it deviates from ideal width. "
|
||
"The algorithm minimizes total demerits (1 + badness)² across all lines.")
|
||
(div
|
||
(~tw :tokens "grid grid-cols-4 md:grid-cols-8 gap-2")
|
||
(map
|
||
(fn
|
||
(used)
|
||
(let
|
||
((bad (line-badness used 100))
|
||
(pct (str (min used 100) "%")))
|
||
(div
|
||
(~tw
|
||
:tokens "rounded border border-stone-200 p-2 text-center")
|
||
(div
|
||
:style (str
|
||
"height:4px;background:linear-gradient(90deg,hsl(263,70%,50%) "
|
||
pct
|
||
",#e7e5e4 "
|
||
pct
|
||
");border-radius:2px;margin-bottom:6px;")
|
||
"")
|
||
(div
|
||
(~tw :tokens "text-sm font-mono font-bold")
|
||
(if
|
||
(>= bad 100000)
|
||
(span (~tw :tokens "text-red-500") "∞")
|
||
(span (~tw :tokens "text-stone-700") (str bad))))
|
||
(div
|
||
(~tw :tokens "text-xs text-stone-400 mt-0.5")
|
||
(str used "%")))))
|
||
(list 100 95 90 85 80 70 50 110))))
|
||
(div
|
||
(~tw :tokens "space-y-3")
|
||
(h2
|
||
(~tw :tokens "text-xl font-semibold text-stone-800")
|
||
"Hyphenation")
|
||
(p
|
||
(~tw :tokens "text-sm text-stone-500")
|
||
"Liang's algorithm: a trie of character patterns with numeric levels. "
|
||
"Odd levels mark valid break points.")
|
||
(let
|
||
((trie (make-hyphenation-trie (list "hy1p" "he2n" "hen3at" "hena4t" "1na" "n2at" "1tio" "2io" "o2i" "1tic" "1mo" "4m1p" "1pu" "put1" "1er" "pro1g" "1gram" "2gra" "program5" "pro3" "ty1" "1graph" "2ph"))))
|
||
(div
|
||
(~tw :tokens "flex flex-wrap gap-3")
|
||
(map
|
||
(fn
|
||
(word)
|
||
(let
|
||
((syllables (hyphenate-word trie word)))
|
||
(div
|
||
(~tw
|
||
:tokens "rounded-lg border border-stone-200 bg-white px-4 py-3 text-center")
|
||
(div
|
||
(~tw
|
||
:tokens "text-lg font-mono font-semibold text-stone-800 tracking-wide")
|
||
(map-indexed
|
||
(fn
|
||
(i syl)
|
||
(if
|
||
(= i 0)
|
||
(span syl)
|
||
(list
|
||
(span
|
||
(~tw :tokens "text-violet-400 mx-0.5")
|
||
"·")
|
||
(span syl))))
|
||
syllables))
|
||
(div (~tw :tokens "text-xs text-stone-400 mt-1") word))))
|
||
(list "hyphen" "computation" "programming" "typography")))))
|
||
(div
|
||
(~tw
|
||
:tokens "rounded-lg border border-stone-200 bg-stone-50 p-5 space-y-2")
|
||
(h3
|
||
(~tw
|
||
:tokens "text-sm font-semibold text-stone-600 uppercase tracking-wide")
|
||
"The pipeline")
|
||
(ol
|
||
(~tw
|
||
:tokens "list-decimal list-inside text-sm text-stone-600 space-y-1")
|
||
(li
|
||
(code "measure-text")
|
||
" — the only IO. Server: font tables. Browser: "
|
||
(code "canvas.measureText"))
|
||
(li
|
||
(code "break-lines")
|
||
" — Knuth-Plass DP over word widths → optimal break points")
|
||
(li
|
||
(code "position-lines")
|
||
" — pure arithmetic: widths + breaks → x,y coordinates")
|
||
(li
|
||
(code "hyphenate-word")
|
||
" — Liang's trie: character patterns → syllable boundaries")
|
||
(li
|
||
"All layout is "
|
||
(strong "deterministic")
|
||
" — same widths → same positions, every time"))))))) |