Hyperscript: precedes/follows comparisons, tokenizer keywords

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>
This commit is contained in:
2026-04-12 12:20:13 +00:00
parent ae32254dfb
commit 7d798be14f
14 changed files with 218 additions and 187 deletions

View File

@@ -699,6 +699,19 @@
var scrollY = (state && state.scrollY) ? state.scrollY : 0;
K.eval("(handle-popstate " + scrollY + ")");
});
// Wire up streaming suspense resolution
Sx.resolveSuspense = function(id, sx) {
try { K.eval('(resolve-suspense "' + id.replace(/"/g, '\\"') + '" "' + sx.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '")'); }
catch(e) { console.error("[sx] resolveSuspense error:", e); }
};
// Drain any pending resolves that arrived before boot
if (window.__sxPending) {
for (var pi = 0; pi < window.__sxPending.length; pi++) {
Sx.resolveSuspense(window.__sxPending[pi].id, window.__sxPending[pi].sx);
}
window.__sxPending = null;
}
window.__sxResolve = function(id, sx) { Sx.resolveSuspense(id, sx); };
// Signal boot complete
document.documentElement.setAttribute("data-sx-ready", "true");
console.log("[sx] boot done");

View File

@@ -586,6 +586,10 @@
(list (quote strict-eq) left (parse-expr))))
((and (= typ "keyword") (or (= val "contain") (= val "include") (= val "includes")))
(do (adv!) (list (quote contains?) left (parse-expr))))
((and (= typ "keyword") (= val "precedes"))
(do (adv!) (list (quote precedes?) left (parse-atom))))
((and (= typ "keyword") (= val "follows"))
(do (adv!) (list (quote follows?) left (parse-atom))))
(true left)))))
(define
parse-collection

View File

@@ -415,6 +415,10 @@
(hs-contains? (rest collection) item)))))
(true false))))
;; Method dispatch — obj.method(args)
(define precedes? (fn (a b) (< (str a) (str b))))
;; ── 0.9.90 features ─────────────────────────────────────────────
;; beep! — debug logging, returns value unchanged
(define
hs-empty?
(fn
@@ -425,13 +429,11 @@
((list? v) (= (len v) 0))
((dict? v) (= (len (keys v)) 0))
(true false))))
;; ── 0.9.90 features ─────────────────────────────────────────────
;; beep! — debug logging, returns value unchanged
(define hs-first (fn (lst) (first lst)))
;; Property-based is — check obj.key truthiness
(define hs-last (fn (lst) (last lst)))
(define hs-first (fn (lst) (first lst)))
;; Array slicing (inclusive both ends)
(define hs-last (fn (lst) (last lst)))
;; Collection: sorted by
(define
hs-template
(fn
@@ -517,7 +519,7 @@
(set! i (+ i 1))
(tpl-loop)))))))
(do (tpl-loop) result))))
;; Collection: sorted by
;; Collection: sorted by descending
(define
hs-make-object
(fn
@@ -529,7 +531,7 @@
(fn (pair) (dict-set! d (first pair) (nth pair 1)))
pairs)
d))))
;; Collection: sorted by descending
;; Collection: split by
(define
hs-method-call
(fn
@@ -552,9 +554,9 @@
(if (= (first lst) item) i (idx-loop (rest lst) (+ i 1))))))
(idx-loop obj 0)))
(true nil))))
;; Collection: split by
(define hs-beep (fn (v) v))
;; Collection: joined by
(define hs-beep (fn (v) v))
(define hs-prop-is (fn (obj key) (not (hs-falsy? (host-get obj key)))))
(define

View File

@@ -166,7 +166,11 @@
"select"
"reset"
"default"
"halt"))
"halt"
"precedes"
"follows"
"ignoring"
"case"))
(define hs-keyword? (fn (word) (some (fn (k) (= k word)) hs-keywords)))

View File

@@ -22,6 +22,7 @@
sum-widths
find-breaks
break-lines
break-lines-greedy
position-line
position-lines
layout-paragraph
@@ -309,4 +310,31 @@
(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))

View File

@@ -699,6 +699,19 @@
var scrollY = (state && state.scrollY) ? state.scrollY : 0;
K.eval("(handle-popstate " + scrollY + ")");
});
// Wire up streaming suspense resolution
Sx.resolveSuspense = function(id, sx) {
try { K.eval('(resolve-suspense "' + id.replace(/"/g, '\\"') + '" "' + sx.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '")'); }
catch(e) { console.error("[sx] resolveSuspense error:", e); }
};
// Drain any pending resolves that arrived before boot
if (window.__sxPending) {
for (var pi = 0; pi < window.__sxPending.length; pi++) {
Sx.resolveSuspense(window.__sxPending[pi].id, window.__sxPending[pi].sx);
}
window.__sxPending = null;
}
window.__sxResolve = function(id, sx) { Sx.resolveSuspense(id, sx); };
// Signal boot complete
document.documentElement.setAttribute("data-sx-ready", "true");
console.log("[sx] boot done");

View File

@@ -586,6 +586,10 @@
(list (quote strict-eq) left (parse-expr))))
((and (= typ "keyword") (or (= val "contain") (= val "include") (= val "includes")))
(do (adv!) (list (quote contains?) left (parse-expr))))
((and (= typ "keyword") (= val "precedes"))
(do (adv!) (list (quote precedes?) left (parse-atom))))
((and (= typ "keyword") (= val "follows"))
(do (adv!) (list (quote follows?) left (parse-atom))))
(true left)))))
(define
parse-collection

File diff suppressed because one or more lines are too long

View File

@@ -415,6 +415,10 @@
(hs-contains? (rest collection) item)))))
(true false))))
;; Method dispatch — obj.method(args)
(define precedes? (fn (a b) (< (str a) (str b))))
;; ── 0.9.90 features ─────────────────────────────────────────────
;; beep! — debug logging, returns value unchanged
(define
hs-empty?
(fn
@@ -425,13 +429,11 @@
((list? v) (= (len v) 0))
((dict? v) (= (len (keys v)) 0))
(true false))))
;; ── 0.9.90 features ─────────────────────────────────────────────
;; beep! — debug logging, returns value unchanged
(define hs-first (fn (lst) (first lst)))
;; Property-based is — check obj.key truthiness
(define hs-last (fn (lst) (last lst)))
(define hs-first (fn (lst) (first lst)))
;; Array slicing (inclusive both ends)
(define hs-last (fn (lst) (last lst)))
;; Collection: sorted by
(define
hs-template
(fn
@@ -517,7 +519,7 @@
(set! i (+ i 1))
(tpl-loop)))))))
(do (tpl-loop) result))))
;; Collection: sorted by
;; Collection: sorted by descending
(define
hs-make-object
(fn
@@ -529,7 +531,7 @@
(fn (pair) (dict-set! d (first pair) (nth pair 1)))
pairs)
d))))
;; Collection: sorted by descending
;; Collection: split by
(define
hs-method-call
(fn
@@ -552,9 +554,9 @@
(if (= (first lst) item) i (idx-loop (rest lst) (+ i 1))))))
(idx-loop obj 0)))
(true nil))))
;; Collection: split by
(define hs-beep (fn (v) v))
;; Collection: joined by
(define hs-beep (fn (v) v))
(define hs-prop-is (fn (obj key) (not (hs-falsy? (host-get obj key)))))
(define

File diff suppressed because one or more lines are too long

View File

@@ -166,7 +166,11 @@
"select"
"reset"
"default"
"halt"))
"halt"
"precedes"
"follows"
"ignoring"
"case"))
(define hs-keyword? (fn (word) (some (fn (k) (= k word)) hs-keywords)))

File diff suppressed because one or more lines are too long

View File

@@ -994,6 +994,7 @@
"hs-falsy?",
"hs-matches?",
"hs-contains?",
"precedes?",
"hs-empty?",
"hs-first",
"hs-last",

View File

@@ -1,171 +1,109 @@
;; Pretext demo — DOM-free text layout
;;
;; Visual-first: shows typeset text, then explains how.
;; All layout computed server-side in pure SX.
;; All layout computed as data, then rendered.
;; Render a single line of positioned words
(defcomp
~pretext-demo/render-line
(&key line-words line-widths gap-w y)
(let
((positions (list)) (x 0))
(for-each
(fn
(i)
(let
((w (nth line-words i)) (ww (nth line-widths i)))
(append!
positions
(span
:style (str
"position:absolute;left:"
(+ x 16)
"px;top:"
(+ y 12)
"px;font-size:15px;line-height:24px;white-space:nowrap;")
w))
(set! x (+ x ww gap-w))))
(range (len line-words)))
positions))
;; 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)})))))))
;; Render a paragraph as positioned words using break-lines output
;; 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/typeset-block
(&key words widths space-width max-width line-height label)
~pretext-demo/render-paragraph
(&key lines max-width line-height n-words label)
(let
((ranges (break-lines widths space-width max-width))
(lh (or line-height 24)))
((lh (or line-height 24)) (n-lines (len lines)))
(div
(~tw
:tokens "relative rounded-lg border border-stone-200 bg-white overflow-hidden")
:class "relative rounded-lg border border-stone-200 bg-white overflow-hidden"
(when
label
(div
(~tw :tokens "px-4 pt-3 pb-1")
:class "px-4 pt-3 pb-1"
(span
(~tw
:tokens "text-xs font-medium uppercase tracking-wide text-stone-400")
:class "text-xs font-medium uppercase tracking-wide text-stone-400"
label)))
(div
:style (str
"position:relative;height:"
(* (len ranges) lh)
(* n-lines lh)
"px;padding:12px 16px;")
(map-indexed
(map
(fn
(line-idx range)
(line)
(let
((start (first range))
(end (nth range 1))
(y (* line-idx lh))
(line-words (slice words start end))
(line-widths (slice widths start end))
(total-word-w (reduce + 0 line-widths))
(gaps (max 1 (- (len line-words) 1)))
(slack (- max-width total-word-w))
(is-last (= line-idx (- (len ranges) 1)))
(gap-w (if is-last space-width (/ slack gaps))))
(~pretext-demo/render-line
:line-words line-words
:line-widths line-widths
:gap-w gap-w
:y y)))
ranges))
(div
(~tw
:tokens "px-4 py-2 border-t border-stone-100 bg-stone-50 flex justify-between")
(span
(~tw :tokens "text-xs text-stone-400")
(str (len ranges) " lines, " (len words) " words"))
(span
(~tw :tokens "text-xs text-stone-400")
(str "width: " max-width "px"))))))
;; Simple greedy word wrap for comparison
(defcomp
~pretext-demo/greedy-block
(&key words widths space-width max-width line-height label)
(let
((n (len widths))
(lines (list))
(current-start 0)
(current-width 0)
(lh (or line-height 24)))
(for-each
(fn
(i)
(let
((w (nth widths i))
(needed
(if (= i current-start) w (+ current-width space-width w))))
(if
(and (> needed max-width) (not (= i current-start)))
(do
(append! lines (list current-start i))
(set! current-start i)
(set! current-width w))
(set! current-width needed))))
(range n))
(append! lines (list current-start n))
(div
(~tw
:tokens "relative rounded-lg border border-stone-200 bg-white overflow-hidden")
(when
label
(div
(~tw :tokens "px-4 pt-3 pb-1")
(span
(~tw
:tokens "text-xs font-medium uppercase tracking-wide text-stone-400")
label)))
(div
:style (str
"position:relative;height:"
(* (len lines) lh)
"px;padding:12px 16px;")
(map-indexed
(fn
(line-idx range)
(let
((start (first range))
(end (nth range 1))
(y (* line-idx lh))
(line-words (slice words start end))
(line-widths (slice widths start end))
(total-word-w (reduce + 0 line-widths))
(gaps (max 1 (- (len line-words) 1)))
(slack (- max-width total-word-w))
(is-last (= line-idx (- (len lines) 1)))
(gap-w (if is-last space-width (/ slack gaps))))
(~pretext-demo/render-line
:line-words line-words
:line-widths line-widths
:gap-w gap-w
:y y)))
((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
(~tw
:tokens "px-4 py-2 border-t border-stone-100 bg-stone-50 flex justify-between")
:class "px-4 py-2 border-t border-stone-100 bg-stone-50 flex justify-between"
(span
(~tw :tokens "text-xs text-stone-400")
(str (len lines) " lines (greedy)"))
(span
(~tw :tokens "text-xs text-stone-400")
(str "width: " max-width "px"))))))
: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-text "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.")
(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."
" "))
((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
((sample-widths (map (fn (w) (* (len w) char-w)) sample-words)))
((sw (map (fn (w) (* (len w) char-w)) sample-words))
(n-words (len sample-words)))
(div
(~tw :tokens "space-y-10")
(div
@@ -177,14 +115,21 @@
(p
(~tw :tokens "mt-1 text-lg text-stone-500")
"DOM-free text layout. One IO boundary. Pure arithmetic."))
(div
(~tw :tokens "max-w-xl mx-auto mt-6")
(~pretext-demo/typeset-block
:words sample-words
:widths sample-widths
:space-width space-w
:max-width 520
:label "Knuth-Plass optimal line breaking — John 1:14")))
(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:14"))))
(div
(~tw :tokens "rounded-lg border border-violet-200 bg-violet-50 p-5")
(p
@@ -205,24 +150,35 @@
"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
((narrow-widths (map (fn (w) (* (len w) 7.8)) sample-words))
(narrow-sw 7.8)
(narrow-max 340))
((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/greedy-block
:words sample-words
:widths narrow-widths
:space-width narrow-sw
:max-width narrow-max
:line-height 22
(~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/typeset-block
:words sample-words
:widths narrow-widths
:space-width narrow-sw
:max-width narrow-max
:line-height 22
(~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")