;; 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
x
x
") (content-test "mark sup html" (asHTML (one (list :superscript) "")) "x
") (content-test "mark link html" (asHTML (one (list :link) "/u")) "") ;; 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 & ba & 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) "")))) "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")) "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))