Files
rose-ash/lib/content/tests/runs.sx
giles f68591456e
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 40s
content: Phase 5 — rich inline text via structured runs (861/861)
CtText.text may be a list of runs (text marks href); CtHeading/CtQuote rich,
CtCode verbatim. New runs.sx overrides render/markdown/text methods (byte-
identical for plain strings, opt-in). 4 modes: HTML tags / markdown / nested
SX / plain asText (drift-proof). find-replace per-run marks-preserving;
search across run boundaries; CRDT block-granularity LWW; data+wire round-trip.
Runs are a Smalltalk-renderable list (not a dict — substrate can't read dict
fields under nested render dispatch). +36 tests (44 suites). Phase 6 (char-
level inline CRDT) recorded as future.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:10:56 +00:00

228 lines
6.2 KiB
Plaintext

;; Phase 5 — rich inline text (structured runs). Acceptance suite.
(st-bootstrap-classes!)
(content/bootstrap!)
(content-bootstrap-markdown!)
(content-bootstrap-text!)
(content-bootstrap-section!)
(content-bootstrap-runs!)
;; one-run helper: a CtText with a single marked run
(define
one
(fn (marks href) (mk-rich-text "p" (list (mk-run "x" marks href)))))
;; ── (1) four render modes ──
;; a paragraph mixing plain + bold + a link
(define
rd
(doc-append
(doc-empty "d")
(mk-rich-text
"p"
(list
(mk-run "the " (list) "")
(mk-run "cat" (list :bold) "")
(mk-run " and " (list) "")
(mk-run "dog" (list :italic :link) "/d")
(mk-run " sat" (list) "")))))
(define p (doc-find rd "p"))
(content-test
"asHTML rich"
(asHTML rd)
"<p>the <strong>cat</strong> and <a href=\"/d\"><em>dog</em></a> sat</p>")
(content-test
"asMarkdown rich"
(asMarkdown rd)
"the **cat** and [_dog_](/d) sat")
(content-test
"asSx rich"
(asSx rd)
"(article (p \"the \" (strong \"cat\") \" and \" (a :href \"/d\" (em \"dog\")) \" sat\"))")
(content-test
"asText rich is plain (no markup)"
(asText rd)
"the cat and dog sat")
;; every mark renders in HTML
(content-test
"mark bold html"
(asHTML (one (list :bold) ""))
"<p><strong>x</strong></p>")
(content-test
"mark italic html"
(asHTML (one (list :italic) ""))
"<p><em>x</em></p>")
(content-test
"mark underline html"
(asHTML (one (list :underline) ""))
"<p><u>x</u></p>")
(content-test
"mark strike html"
(asHTML (one (list :strikethrough) ""))
"<p><s>x</s></p>")
(content-test
"mark code html"
(asHTML (one (list :code) ""))
"<p><code>x</code></p>")
(content-test
"mark sub html"
(asHTML (one (list :subscript) ""))
"<p><sub>x</sub></p>")
(content-test
"mark sup html"
(asHTML (one (list :superscript) ""))
"<p><sup>x</sup></p>")
(content-test
"mark link html"
(asHTML (one (list :link) "/u"))
"<p><a href=\"/u\">x</a></p>")
;; markdown marks
(content-test "mark bold md" (asMarkdown (one (list :bold) "")) "**x**")
(content-test "mark italic md" (asMarkdown (one (list :italic) "")) "_x_")
(content-test
"mark strike md"
(asMarkdown (one (list :strikethrough) ""))
"~~x~~")
(content-test "mark code md" (asMarkdown (one (list :code) "")) "`x`")
(content-test "mark link md" (asMarkdown (one (list :link) "/u")) "[x](/u)")
(content-test
"mark underline md fallback"
(asMarkdown (one (list :underline) ""))
"<u>x</u>")
;; nested marks (bold+italic) — deterministic nesting order
(content-test
"nested marks html"
(asHTML (one (list :bold :italic) ""))
"<p><em><strong>x</strong></em></p>")
;; escaping still happens inside runs
(content-test
"run text escaped html"
(asHTML (mk-rich-text "p" (list (mk-run "a & b <c>" (list :bold) ""))))
"<p><strong>a &amp; b &lt;c&gt;</strong></p>")
;; rich heading + quote + code
(content-test
"rich heading html"
(asHTML
(st-iv-set!
(mk-heading "h" 2 "")
"text"
(list (mk-run "Big " (list) "") (mk-run "bold" (list :bold) ""))))
"<h2>Big <strong>bold</strong></h2>")
(content-test
"rich quote html"
(asHTML
(st-iv-set!
(mk-quote "q" "" "")
"text"
(list (mk-run "wise" (list :italic) ""))))
"<blockquote><em>wise</em></blockquote>")
;; code is verbatim — runs concatenate as plain text, marks ignored
(content-test
"code runs plain html"
(asHTML
(st-iv-set!
(mk-code "c" "py" "")
"text"
(list (mk-run "a=" (list :bold) "") (mk-run "1" (list) ""))))
"<pre><code class=\"language-py\">a=1</code></pre>")
;; ── (2) backward compat: plain-string CtText unchanged ──
(content-test
"plain html"
(asHTML (mk-text "q" "hi & <b>"))
"<p>hi &amp; &lt;b&gt;</p>")
(content-test "plain sx" (asSx (mk-text "q" "hi")) "(p \"hi\")")
(content-test "plain md" (asMarkdown (mk-text "q" "hi")) "hi")
(content-test "plain text" (asText (mk-text "q" "hi")) "hi")
(content-test
"plain heading html"
(asHTML (mk-heading "h" 3 "T"))
"<h3>T</h3>")
;; ── (3) find-replace across runs (per-run, marks preserved) ──
(define
frd
(doc-append
(doc-empty "d")
(mk-rich-text
"p"
(list
(mk-run "the Foo" (list :bold) "")
(mk-run " and Foo here" (list) "")))))
(define frr (content/find-replace frd "Foo" "Bar"))
(content-test
"find-replace rich plain text"
(asText frr)
"the Bar and Bar here")
(content-test
"find-replace rich preserves marks"
(asHTML frr)
"<p><strong>the Bar</strong> and Bar here</p>")
(content-test
"find-replace rich run0 still bold"
(nth (nth (blk-get (doc-find frr "p") "text") 0) 1)
(list "bold"))
;; ── (4) search-text via asText, across run boundary ──
;; "cat sat" spans run1 ("the cat") and run2 (" sat")
(define
sd
(doc-append
(doc-empty "d")
(mk-rich-text
"p"
(list (mk-run "the cat" (list :bold) "") (mk-run " sat" (list) "")))))
(content-test
"search finds substring across runs"
(content/search-text-ids sd "cat sat")
(list "p"))
(content-test "search miss" (content/search-text-ids sd "zzz") (list))
;; ── (5) CRDT invariant — runs are an opaque block-level value ──
(define ra (list (mk-run "x" (list :bold) "")))
(define rb (list (mk-run "y" (list :italic) "")))
(define
s1
(crdt-insert
(crdt-empty)
"p"
"text"
(crdt-pos 5 "a")
{:text ra}
1
"a"))
(define s2 (crdt-update s1 "p" "text" rb 2 "b"))
(content-test
"crdt merge commutes with runs"
(get (crdt-merge s1 s2) :elements)
(get (crdt-merge s2 s1) :elements))
(content-test
"crdt merge idempotent with runs"
(get (crdt-merge s2 s2) :elements)
(get s2 :elements))
;; LWW: later ts (rb, ts 2) wins; runs survive as the field value
(content-test
"crdt LWW keeps latest runs"
(asHTML
(crdt-element->block (get (get (crdt-merge s1 s2) :elements) "p")))
"<p><em>y</em></p>")
;; ── (6) data + wire round-trip runs losslessly ──
(content-test
"data round-trip rich html"
(asHTML (content/from-data (content/to-data rd)))
(asHTML rd))
(content-test
"data round-trip rich text"
(asText (content/from-data (content/to-data rd)))
"the cat and dog sat")
(content-test
"wire round-trip rich html"
(asHTML (content/from-wire (content/to-wire rd)))
(asHTML rd))