Step 17b: Pretext — DOM-free text layout with otfm font measurement

Pure SX text layout library with one IO boundary (text-measure perform).
Knuth-Plass optimal line breaking, Liang's hyphenation, position calculation.

Library (lib/text-layout.sx):
- break-lines: Knuth-Plass DP over word widths
- break-lines-greedy: simple word-wrap for comparison
- hyphenate-word: Liang's trie algorithm
- position-line/position-lines: running x/y sums
- measure-text: single perform (text-measure IO)

Server font measurement (otfm):
- Reads OpenType cmap + hmtx tables from .ttf files
- DejaVu Serif/Sans bundled in shared/static/fonts/
- _cek_io_resolver hook: perform works inside aser/eval_expr
- JIT VM suspension inline resolution for IO in compiled code

~font component (shared/sx/templates/font.sx):
- Works like ~tw: emits @font-face CSS via cssx scope
- Sets font-family on parent via spread
- Deduplicates font declarations

Infrastructure fixes:
- stdin load command: per-expression error handling (was aborting on first error)
- cek_run IO hook: _cek_io_resolver in sx_types.ml
- JIT VmSuspended: inline IO resolution when resolver installed
- ListRef handling in IO resolver (perform creates ListRef, not List)

Demo page at /sx/(applications.(pretext)):
- Hero: justified paragraph with otfm-measured proportional widths
- Greedy vs Knuth-Plass side-by-side comparison
- Badness scoring visualization
- Hyphenation syllable decomposition

25 new tests (spec/tests/test-text-layout.sx), 3201/3201 passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 15:13:00 +00:00
parent f60d22e86e
commit 1eadefd0c1
9 changed files with 274 additions and 52 deletions

View File

@@ -1,7 +1,7 @@
;; Pretext demo — DOM-free text layout
;;
;; Visual-first: shows typeset text, then explains how.
;; Uses measure-text (perform) for real glyph measurement.
;; Uses measure-text (perform) for glyph measurement.
;; Compute positioned word data for one line.
(define
@@ -45,15 +45,6 @@
{:y y :words (pretext-position-line lw lwid gap)}))))))
(range n-lines)))))
;; Measure all words and return widths list
(define
pretext-measure-words
(fn
(words font size)
(map
(fn (w) (let ((m (measure-text font size w))) (get m :width)))
words)))
;; Render pre-computed positioned lines
(defcomp
~pretext-demo/render-paragraph
@@ -109,13 +100,23 @@
(font "serif")
(size 15))
(let
((sw (pretext-measure-words sample-words font size))
(space-m (measure-text font size " "))
(n-words (len sample-words)))
((sw (list)) (n-words (len sample-words)))
(for-each
(fn
(w)
(let
((m (measure-text font size w)))
(append! sw (get m :width))))
sample-words)
(let
((space-w (get space-m :width)))
((space-m (measure-text font size " "))
(space-w (get (measure-text font size " ") :width)))
(div
(~tw :tokens "space-y-10")
(begin
(~tw :tokens "space-y-10")
(~font
:family "Pretext Serif"
:src "/static/fonts/DejaVuSerif.ttf"))
(div
(~tw :tokens "space-y-4")
(div
@@ -166,11 +167,6 @@
(div
(~tw :tokens "grid grid-cols-1 md:grid-cols-2 gap-4")
(~pretext-demo/render-paragraph
:words sample-words
:widths sw
:space-width space-w
:max-width nm
:line-height 22
:lines (pretext-layout-lines
sample-words
sw
@@ -178,14 +174,11 @@
space-w
nm
22)
:max-width nm
:line-height 22
:n-words n-words
:label "Greedy (browser default)")
(~pretext-demo/render-paragraph
:words sample-words
:widths sw
:space-width space-w
:max-width nm
:line-height 22
:lines (pretext-layout-lines
sample-words
sw
@@ -193,6 +186,8 @@
space-w
nm
22)
:max-width nm
:line-height 22
:n-words n-words
:label "Knuth-Plass optimal"))))
(div