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; var scrollY = (state && state.scrollY) ? state.scrollY : 0;
K.eval("(handle-popstate " + scrollY + ")"); 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 // Signal boot complete
document.documentElement.setAttribute("data-sx-ready", "true"); document.documentElement.setAttribute("data-sx-ready", "true");
console.log("[sx] boot done"); console.log("[sx] boot done");

View File

@@ -586,6 +586,10 @@
(list (quote strict-eq) left (parse-expr)))) (list (quote strict-eq) left (parse-expr))))
((and (= typ "keyword") (or (= val "contain") (= val "include") (= val "includes"))) ((and (= typ "keyword") (or (= val "contain") (= val "include") (= val "includes")))
(do (adv!) (list (quote contains?) left (parse-expr)))) (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))))) (true left)))))
(define (define
parse-collection parse-collection

View File

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

View File

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

View File

@@ -22,6 +22,7 @@
sum-widths sum-widths
find-breaks find-breaks
break-lines break-lines
break-lines-greedy
position-line position-line
position-lines position-lines
layout-paragraph layout-paragraph
@@ -309,4 +310,31 @@
(lh (or line-height 1.4))) (lh (or line-height 1.4)))
(layout-paragraph words f s w lh)))))) (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)) (import (sx text-layout))

View File

@@ -699,6 +699,19 @@
var scrollY = (state && state.scrollY) ? state.scrollY : 0; var scrollY = (state && state.scrollY) ? state.scrollY : 0;
K.eval("(handle-popstate " + scrollY + ")"); 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 // Signal boot complete
document.documentElement.setAttribute("data-sx-ready", "true"); document.documentElement.setAttribute("data-sx-ready", "true");
console.log("[sx] boot done"); console.log("[sx] boot done");

View File

@@ -586,6 +586,10 @@
(list (quote strict-eq) left (parse-expr)))) (list (quote strict-eq) left (parse-expr))))
((and (= typ "keyword") (or (= val "contain") (= val "include") (= val "includes"))) ((and (= typ "keyword") (or (= val "contain") (= val "include") (= val "includes")))
(do (adv!) (list (quote contains?) left (parse-expr)))) (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))))) (true left)))))
(define (define
parse-collection parse-collection

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -166,7 +166,11 @@
"select" "select"
"reset" "reset"
"default" "default"
"halt")) "halt"
"precedes"
"follows"
"ignoring"
"case"))
(define hs-keyword? (fn (word) (some (fn (k) (= k word)) hs-keywords))) (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-falsy?",
"hs-matches?", "hs-matches?",
"hs-contains?", "hs-contains?",
"precedes?",
"hs-empty?", "hs-empty?",
"hs-first", "hs-first",
"hs-last", "hs-last",

View File

@@ -1,171 +1,109 @@
;; Pretext demo — DOM-free text layout ;; Pretext demo — DOM-free text layout
;; ;;
;; Visual-first: shows typeset text, then explains how. ;; 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 ;; Compute positioned word data for one line.
(defcomp ;; Returns list of {:word :x :width} dicts.
~pretext-demo/render-line (define
(&key line-words line-widths gap-w y) pretext-position-line
(let (fn
((positions (list)) (x 0)) (words widths gap-w)
(for-each (let
(fn loop
(i) ((i 0) (x 0) (acc (list)))
(let (if
((w (nth line-words i)) (ww (nth line-widths i))) (>= i (len words))
(append! acc
positions (loop
(span (+ i 1)
:style (str (+ x (nth widths i) gap-w)
"position:absolute;left:" (append acc (list {:width (nth widths i) :x x :word (nth words i)})))))))
(+ 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))
;; 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 (defcomp
~pretext-demo/typeset-block ~pretext-demo/render-paragraph
(&key words widths space-width max-width line-height label) (&key lines max-width line-height n-words label)
(let (let
((ranges (break-lines widths space-width max-width)) ((lh (or line-height 24)) (n-lines (len lines)))
(lh (or line-height 24)))
(div (div
(~tw :class "relative rounded-lg border border-stone-200 bg-white overflow-hidden"
:tokens "relative rounded-lg border border-stone-200 bg-white overflow-hidden")
(when (when
label label
(div (div
(~tw :tokens "px-4 pt-3 pb-1") :class "px-4 pt-3 pb-1"
(span (span
(~tw :class "text-xs font-medium uppercase tracking-wide text-stone-400"
:tokens "text-xs font-medium uppercase tracking-wide text-stone-400")
label))) label)))
(div (div
:style (str :style (str
"position:relative;height:" "position:relative;height:"
(* (len ranges) lh) (* n-lines lh)
"px;padding:12px 16px;") "px;padding:12px 16px;")
(map-indexed (map
(fn (fn
(line-idx range) (line)
(let (let
((start (first range)) ((y (get line :y)))
(end (nth range 1)) (map
(y (* line-idx lh)) (fn
(line-words (slice words start end)) (pw)
(line-widths (slice widths start end)) (span
(total-word-w (reduce + 0 line-widths)) :style (str
(gaps (max 1 (- (len line-words) 1))) "position:absolute;left:"
(slack (- max-width total-word-w)) (+ (get pw :x) 16)
(is-last (= line-idx (- (len ranges) 1))) "px;top:"
(gap-w (if is-last space-width (/ slack gaps)))) (+ y 12)
(~pretext-demo/render-line "px;font-size:15px;line-height:"
:line-words line-words lh
:line-widths line-widths "px;white-space:nowrap;")
:gap-w gap-w (get pw :word)))
:y y))) (get line :words))))
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)))
lines)) lines))
(div (div
(~tw :class "px-4 py-2 border-t border-stone-100 bg-stone-50 flex justify-between"
:tokens "px-4 py-2 border-t border-stone-100 bg-stone-50 flex justify-between")
(span (span
(~tw :tokens "text-xs text-stone-400") :class "text-xs text-stone-400"
(str (len lines) " lines (greedy)")) (str n-lines " lines, " n-words " words"))
(span (span :class "text-xs text-stone-400" (str "width: " max-width "px"))))))
(~tw :tokens "text-xs text-stone-400")
(str "width: " max-width "px"))))))
(defcomp (defcomp
~pretext-demo/content ~pretext-demo/content
() ()
(let (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) (char-w 9.6)
(space-w 9.6)) (space-w 9.6))
(let (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 (div
(~tw :tokens "space-y-10") (~tw :tokens "space-y-10")
(div (div
@@ -177,14 +115,21 @@
(p (p
(~tw :tokens "mt-1 text-lg text-stone-500") (~tw :tokens "mt-1 text-lg text-stone-500")
"DOM-free text layout. One IO boundary. Pure arithmetic.")) "DOM-free text layout. One IO boundary. Pure arithmetic."))
(div (let
(~tw :tokens "max-w-xl mx-auto mt-6") ((hero-max 520) (hero-ranges (break-lines sw space-w 520)))
(~pretext-demo/typeset-block (div
:words sample-words (~tw :tokens "max-w-xl mx-auto mt-6")
:widths sample-widths (~pretext-demo/render-paragraph
:space-width space-w :lines (pretext-layout-lines
:max-width 520 sample-words
:label "Knuth-Plass optimal line breaking — John 1:14"))) 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 (div
(~tw :tokens "rounded-lg border border-violet-200 bg-violet-50 p-5") (~tw :tokens "rounded-lg border border-violet-200 bg-violet-50 p-5")
(p (p
@@ -205,24 +150,35 @@
"Most web text uses greedy word wrap — break when the next word doesn't fit. " "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.") "Knuth-Plass considers all possible breaks simultaneously, minimizing total raggedness.")
(let (let
((narrow-widths (map (fn (w) (* (len w) 7.8)) sample-words)) ((nw (map (fn (w) (* (len w) 7.8)) sample-words))
(narrow-sw 7.8) (ns 7.8)
(narrow-max 340)) (nm 340)
(nlh 22))
(div (div
(~tw :tokens "grid grid-cols-1 md:grid-cols-2 gap-4") (~tw :tokens "grid grid-cols-1 md:grid-cols-2 gap-4")
(~pretext-demo/greedy-block (~pretext-demo/render-paragraph
:words sample-words :lines (pretext-layout-lines
:widths narrow-widths sample-words
:space-width narrow-sw nw
:max-width narrow-max (break-lines-greedy nw ns nm)
:line-height 22 ns
nm
nlh)
:max-width nm
:line-height nlh
:n-words n-words
:label "Greedy (browser default)") :label "Greedy (browser default)")
(~pretext-demo/typeset-block (~pretext-demo/render-paragraph
:words sample-words :lines (pretext-layout-lines
:widths narrow-widths sample-words
:space-width narrow-sw nw
:max-width narrow-max (break-lines nw ns nm)
:line-height 22 ns
nm
nlh)
:max-width nm
:line-height nlh
:n-words n-words
:label "Knuth-Plass optimal")))) :label "Knuth-Plass optimal"))))
(div (div
(~tw :tokens "space-y-3") (~tw :tokens "space-y-3")