Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 40s
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>
228 lines
6.2 KiB
Plaintext
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 & b <c></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 & <b></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))
|