;; 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) "

the cat and dog sat

") (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) "")) "

x

") (content-test "mark italic html" (asHTML (one (list :italic) "")) "

x

") (content-test "mark underline html" (asHTML (one (list :underline) "")) "

x

") (content-test "mark strike html" (asHTML (one (list :strikethrough) "")) "

x

") (content-test "mark code html" (asHTML (one (list :code) "")) "

x

") (content-test "mark sub html" (asHTML (one (list :subscript) "")) "

x

") (content-test "mark sup html" (asHTML (one (list :superscript) "")) "

x

") (content-test "mark link html" (asHTML (one (list :link) "/u")) "

x

") ;; 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) "")) "x") ;; nested marks (bold+italic) — deterministic nesting order (content-test "nested marks html" (asHTML (one (list :bold :italic) "")) "

x

") ;; escaping still happens inside runs (content-test "run text escaped html" (asHTML (mk-rich-text "p" (list (mk-run "a & b " (list :bold) "")))) "

a & b <c>

") ;; 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) "")))) "

Big bold

") (content-test "rich quote html" (asHTML (st-iv-set! (mk-quote "q" "" "") "text" (list (mk-run "wise" (list :italic) "")))) "
wise
") ;; 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) "")))) "
a=1
") ;; ── (2) backward compat: plain-string CtText unchanged ── (content-test "plain html" (asHTML (mk-text "q" "hi & ")) "

hi & <b>

") (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")) "

T

") ;; ── (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) "

the Bar and Bar here

") (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"))) "

y

") ;; ── (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))