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>
340 lines
11 KiB
Plaintext
340 lines
11 KiB
Plaintext
;; Pretext — DOM-free text layout (pure SX)
|
|
;;
|
|
;; Cheng Lou's insight: text measurement is IO, but text *layout* is
|
|
;; pure arithmetic. One `perform` for glyph metrics, everything else
|
|
;; is deterministic functions over numbers.
|
|
;;
|
|
;; Architecture:
|
|
;; (perform (text-measure :font font :size size :text text))
|
|
;; → {:widths (...) :height N :ascent N :descent N}
|
|
;;
|
|
;; break-lines, position-glyphs, hyphenate — all pure SX.
|
|
;;
|
|
;; Knuth-Plass optimal line breaking (DP over break candidates).
|
|
;; Liang's hyphenation (trie over character patterns).
|
|
|
|
(define-library
|
|
(sx text-layout)
|
|
(export
|
|
measure-text
|
|
line-badness
|
|
compute-demerits
|
|
sum-widths
|
|
find-breaks
|
|
break-lines
|
|
break-lines-greedy
|
|
position-line
|
|
position-lines
|
|
layout-paragraph
|
|
make-hyphenation-trie
|
|
find-hyphenation-points
|
|
hyphenate-word
|
|
typeset
|
|
typeset-plain)
|
|
(begin
|
|
(define
|
|
measure-text
|
|
(fn
|
|
(font size text)
|
|
(perform (list (quote text-measure) font size text))))
|
|
(define
|
|
line-badness
|
|
(fn
|
|
(used max-width)
|
|
(let
|
|
((slack (- max-width used)))
|
|
(if
|
|
(< slack 0)
|
|
100000
|
|
(let
|
|
((ratio (/ slack max-width)))
|
|
(* (* ratio ratio) (* ratio 1000)))))))
|
|
(define
|
|
compute-demerits
|
|
(fn
|
|
(badness penalty)
|
|
(let
|
|
((base (+ 1 badness)))
|
|
(+ (* base base) (* penalty penalty)))))
|
|
(define
|
|
sum-widths
|
|
(fn
|
|
(widths space-width from to)
|
|
(let
|
|
loop
|
|
((k from) (total 0))
|
|
(if
|
|
(>= k to)
|
|
(let
|
|
((spaces (max 0 (- (- to from) 1))))
|
|
(+ total (* spaces space-width)))
|
|
(loop (+ k 1) (+ total (nth widths k)))))))
|
|
(define
|
|
find-breaks
|
|
(fn
|
|
(widths space-width max-width)
|
|
(let
|
|
((n (len widths)))
|
|
(if
|
|
(<= n 0)
|
|
(list)
|
|
(let
|
|
((dp (map (fn (i) (if (= i 0) (list 0 -1) (list 999999999 -1))) (range (+ n 1)))))
|
|
(for-each
|
|
(fn
|
|
(i)
|
|
(let
|
|
((prev-cost (first (nth dp i))))
|
|
(let
|
|
try-j
|
|
((j i))
|
|
(when
|
|
(< j n)
|
|
(let
|
|
((used (sum-widths widths space-width i (+ j 1))))
|
|
(when
|
|
(<= used (* max-width 1.15))
|
|
(let
|
|
((bad (line-badness used max-width))
|
|
(penalty 0))
|
|
(let
|
|
((cost (+ prev-cost (compute-demerits bad penalty)))
|
|
(entry (nth dp (+ j 1))))
|
|
(when
|
|
(< cost (first entry))
|
|
(set!
|
|
dp
|
|
(map-indexed
|
|
(fn
|
|
(idx e)
|
|
(if (= idx (+ j 1)) (list cost i) e))
|
|
dp)))
|
|
(try-j (+ j 1))))))))))
|
|
(range n))
|
|
(let
|
|
trace
|
|
((pos n) (breaks (list)))
|
|
(let
|
|
((prev (nth (nth dp pos) 1)))
|
|
(if (<= prev 0) breaks (trace prev (cons prev breaks))))))))))
|
|
(define
|
|
break-lines
|
|
(fn
|
|
(widths space-width max-width)
|
|
(let
|
|
((breaks (find-breaks widths space-width max-width))
|
|
(n (len widths)))
|
|
(if
|
|
(= n 0)
|
|
(list)
|
|
(let
|
|
((starts (cons 0 breaks)))
|
|
(let
|
|
((ends (append (rest starts) (list n))))
|
|
(map (fn (s e) (list s e)) starts ends)))))))
|
|
(define
|
|
position-line
|
|
(fn
|
|
(words widths space-width x0 y)
|
|
(let
|
|
loop
|
|
((i 0) (x x0) (result (list)))
|
|
(if
|
|
(>= i (len words))
|
|
(reverse result)
|
|
(let
|
|
((w (nth words i)) (width (nth widths i)))
|
|
(loop
|
|
(+ i 1)
|
|
(+ x width space-width)
|
|
(cons {:width width :x x :y y :word w} result)))))))
|
|
(define
|
|
position-lines
|
|
(fn
|
|
(words widths line-ranges space-width line-height x0 y0)
|
|
(map-indexed
|
|
(fn
|
|
(line-idx range)
|
|
(let
|
|
((start (first range))
|
|
(end (nth range 1))
|
|
(y (+ y0 (* line-idx line-height))))
|
|
(let
|
|
((line-words (slice words start end))
|
|
(line-widths (slice widths start end)))
|
|
(position-line line-words line-widths space-width x0 y))))
|
|
line-ranges)))
|
|
(define
|
|
make-hyphenation-trie
|
|
(fn
|
|
(patterns)
|
|
(let
|
|
((trie {:children {} :levels (list)}))
|
|
(for-each
|
|
(fn
|
|
(pattern)
|
|
(let
|
|
((chars (list)) (levels (list)) (current-level 0))
|
|
(for-each
|
|
(fn
|
|
(ch)
|
|
(if
|
|
(and (>= (char-code ch) 48) (<= (char-code ch) 57))
|
|
(set! current-level (parse-number ch))
|
|
(do
|
|
(set! chars (append chars (list ch)))
|
|
(set! levels (append levels (list current-level)))
|
|
(set! current-level 0))))
|
|
(map (fn (i) (char-at pattern i)) (range (len pattern))))
|
|
(set! levels (append levels (list current-level)))
|
|
(let
|
|
insert
|
|
((node trie) (i 0))
|
|
(if
|
|
(>= i (len chars))
|
|
(dict-set! node :levels levels)
|
|
(let
|
|
((ch (nth chars i)) (children (get node :children)))
|
|
(when
|
|
(not (has-key? children ch))
|
|
(dict-set! children ch {:children {} :levels (list)}))
|
|
(insert (get children ch) (+ i 1)))))))
|
|
patterns)
|
|
trie)))
|
|
(define
|
|
find-hyphenation-points
|
|
(fn
|
|
(trie word)
|
|
(let
|
|
((n (len word))
|
|
(padded (str "." word "."))
|
|
(pn (+ n 2))
|
|
(levels (map (fn (_) 0) (range (+ n 1)))))
|
|
(for-each
|
|
(fn
|
|
(i)
|
|
(let
|
|
walk
|
|
((node trie) (j i))
|
|
(when
|
|
(< j pn)
|
|
(let
|
|
((ch (char-at padded j))
|
|
(children (get node :children)))
|
|
(when
|
|
(has-key? children ch)
|
|
(let
|
|
((child (get children ch)))
|
|
(when
|
|
(not (empty? (get child :levels)))
|
|
(let
|
|
((pat-levels (get child :levels)))
|
|
(for-each
|
|
(fn
|
|
(k)
|
|
(when
|
|
(< (+ i k) (+ n 1))
|
|
(let
|
|
((old (nth levels (+ i k)))
|
|
(new-val (nth pat-levels k)))
|
|
(when
|
|
(> new-val old)
|
|
(set!
|
|
levels
|
|
(map-indexed
|
|
(fn
|
|
(idx v)
|
|
(if (= idx (+ i k)) new-val v))
|
|
levels))))))
|
|
(range (len pat-levels)))))
|
|
(walk child (+ j 1))))))))
|
|
(range pn))
|
|
(filter
|
|
(fn
|
|
(i)
|
|
(and (> i 1) (< i (- n 1)) (= (mod (nth levels i) 2) 1)))
|
|
(range (+ n 1))))))
|
|
(define
|
|
hyphenate-word
|
|
(fn
|
|
(trie word)
|
|
(let
|
|
((points (find-hyphenation-points trie word)))
|
|
(if
|
|
(empty? points)
|
|
(list word)
|
|
(let
|
|
((starts (cons 0 points))
|
|
(ends (append points (list (len word)))))
|
|
(map (fn (s e) (slice word s e)) starts ends))))))
|
|
(define
|
|
layout-paragraph
|
|
(fn
|
|
(words font size max-width line-height-factor)
|
|
(let
|
|
((metrics (map (fn (w) (measure-text font size w)) words))
|
|
(widths (map (fn (m) (get m :width)) metrics))
|
|
(height
|
|
(if (empty? metrics) size (get (first metrics) :height)))
|
|
(line-height (* height (or line-height-factor 1.4)))
|
|
(space-metrics (measure-text font size " "))
|
|
(space-width (get space-metrics :width)))
|
|
(let
|
|
((line-ranges (break-lines widths space-width max-width))
|
|
(positioned
|
|
(position-lines
|
|
words
|
|
widths
|
|
(break-lines widths space-width max-width)
|
|
space-width
|
|
line-height
|
|
0
|
|
0)))
|
|
{:lines positioned :width max-width :line-height line-height :font-height height :line-ranges line-ranges :height (* (len line-ranges) line-height)}))))
|
|
(define
|
|
typeset-plain
|
|
(fn
|
|
(text font size max-width)
|
|
(let
|
|
((words (split text " ")))
|
|
(layout-paragraph words font size max-width 1.4))))
|
|
(define
|
|
typeset
|
|
(fn
|
|
(text &key font size max-width line-height)
|
|
(let
|
|
((words (split text " "))
|
|
(f (or font "serif"))
|
|
(s (or size 16))
|
|
(w (or max-width 600))
|
|
(lh (or line-height 1.4)))
|
|
(layout-paragraph words f s w lh))))))
|
|
|
|
(define
|
|
break-lines-greedy
|
|
(fn
|
|
(widths space-width max-width)
|
|
(let
|
|
((n (len widths)))
|
|
(if
|
|
(= n 0)
|
|
(list)
|
|
(let
|
|
((lines (list)) (start 0) (used 0))
|
|
(for-each
|
|
(fn
|
|
(i)
|
|
(let
|
|
((w (nth widths i))
|
|
(needed (if (= i start) w (+ used space-width w))))
|
|
(if
|
|
(and (> needed max-width) (not (= i start)))
|
|
(do
|
|
(set! lines (append lines (list (list start i))))
|
|
(set! start i)
|
|
(set! used w))
|
|
(set! used needed))))
|
|
(range n))
|
|
(append lines (list (list start n))))))))
|
|
|
|
(import (sx text-layout)) |