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:
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)))
|
||||
|
||||
|
||||
@@ -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))
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -994,6 +994,7 @@
|
||||
"hs-falsy?",
|
||||
"hs-matches?",
|
||||
"hs-contains?",
|
||||
"precedes?",
|
||||
"hs-empty?",
|
||||
"hs-first",
|
||||
"hs-last",
|
||||
|
||||
@@ -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:1–4")))
|
||||
(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
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user