# content-on-sx: Documents, blocks & collaborative editing on Smalltalk > **DRAFT outline.** The CMS vertical — blog, WYSIWYG editor, Ghost sync. Depends > on `persist-on-sx` (document history as an event log). Ghost/CMS sync stays a thin > external adapter (Python/FFI) until a native replacement exists. rose-ash's `blog` domain is content management: a block-based WYSIWYG editor, navigation, Ghost CMS sync. A document is a tree of live blocks; editing is a stream of operations; collaboration needs conflict-free merge. That is an object model — blocks are objects, edits are messages, and a document is the object graph responding to them. Smalltalk's "everything is an object responding to messages" maps directly to a block/WYSIWYG model, and a semilattice (CRDT) merge keeps concurrent edits conflict-free. End-state: a Smalltalk-on-SX document model (typed blocks, structural ops), operation log + CRDT merge for collaborative editing, versioning/history via the event store, and a render boundary to HTML/SX. External CMS (Ghost) sync is an injected adapter, not core. ## Status (rolling) `bash lib/content/conformance.sh` → **318/318** (Phases 1–4 COMPLETE + extensions: HTML/SX escaping, Markdown render+import, durable CRDT replication, validation) ## Ground rules - **Scope:** only `lib/content/**` and `plans/content-on-sx.md`. May **import** from `lib/smalltalk/`, and (once it exists) `lib/persist/`. Do not edit substrates. - **Architecture:** a document is an ordered tree of blocks (objects); an edit is a message (`insert`/`update`/`move`/`delete`); concurrent edits merge via a commutative (CRDT/semilattice) operation so order doesn't matter. History is the `persist` event stream; any version is a replay. - **Determinism:** merge must be commutative + idempotent (test: apply ops in any order / twice → same document). - **Commits:** one feature per commit. Progress log + tick boxes. ## Architecture sketch ``` Edit op Rendered document (insert block after id) ... HTML / SX tree │ ▲ ▼ │ lib/content/block.sx lib/content/render.sx — typed blocks as objects — block tree → HTML/SX — heading/text/image/embed — (reuses SX render boundary) │ ▲ ▼ │ lib/content/doc.sx lib/content/merge.sx — ordered block tree — CRDT/semilattice op merge — apply op, structural moves — concurrent-edit reconciliation │ ▲ ▼ │ lib/content/api.sx ── (content/edit) (content/render) (content/history) ──┐ │ │ ├── op log + versions → persist │ └── Ghost/CMS sync → injected external adapter (thin, non-core) ──┘ ``` ## Phase 1 — Block document model - [x] `block.sx` — typed block objects - [x] `doc.sx` — ordered tree, apply edit op, structural moves - [x] `render.sx` — block tree → HTML/SX - [x] `api.sx` + tests + scoreboard + conformance.sh ## Phase 2 — Op log + versioning - [x] edit ops as `persist` events; replay to any version - [x] `(content/history doc)`, diff between versions ## Phase 3 — Collaborative merge (CRDT) - [x] commutative/idempotent op merge - [x] concurrent-edit tests (any order, double-apply → identical) ## Phase 4 — External sync + federation - [x] Ghost/CMS sync via injected adapter (import/export) - [x] federated documents (peer-authored blocks) — trust-gated stub - [x] tests: round-trip import/export, conflict on concurrent external edit ## Extensions (post-roadmap) - [x] HTML escaping at the render boundary (`String>>htmlEscaped`: & < > ") - [x] asSx wire string-escaping (`String>>sxEscaped`: \ and " in SX literals) - [x] Markdown render mode (`asMarkdown:` / `content/render doc "md"`) - [x] durable CRDT replication (`crdt-store.sx`: ops on persist, replay + converge) - [x] document validation (`validate.sx`: ids, per-type fields, duplicate ids) - [x] Markdown import adapter (`md-import.sx`: text → blocks, round-trips export) ## Progress log - 2026-06-07 — Extension: Markdown import adapter (`md-import.sx`), inverse of asMarkdown. Line-based parser: ATX headings, fenced code (```lang), blockquotes, unordered/ordered lists (grouping consecutive items), thematic breaks, paragraphs (consecutive plain lines joined with a space). Sequential ids b0,b1…. `md/import` / `content/from-markdown` / `markdown-adapter` (import + asMarkdown export). Round-trips canonical Markdown (import∘export == identity); imported docs pass validation. 24 tests; suite 318/318. - 2026-06-07 — Extension: document validation (`validate.sx`). `content/validate` returns issue dicts `{:id :kind :detail}` (empty = valid); `content/valid?` and `content/issue-kinds` convenience. Checks block id (non-empty string), per-type required fields/types (heading level number, image src/alt strings, list ordered boolean + items list, etc.), unknown block types, and document-level duplicate ids. Guards imports/edits/federated input. 17 tests; suite 294/294. - 2026-06-07 — Extension: durable CRDT replication (`crdt-store.sx`), uniting Phase 2 (persist) + Phase 3 (CvRDT). Each replica appends its CRDT ops to its own stream (`crdt::`); `crdt/replay` folds one log into a state, `crdt/converge` merges every replica's replayed state, `crdt/document` / `crdt/order` materialise. Converged result is identical regardless of replica order or duplicate delivery (join + idempotent apply) → offline-capable, eventually-consistent editing. 14 tests; suite 277/277. - 2026-06-07 — Extension: Markdown render mode (`markdown.sx`). Third boundary format alongside asHTML/asSx via the same polymorphic dispatch; blocks answer `asMarkdown: nl` (boundary supplies the newline — this Smalltalk dialect has no Character newline ctor). `content/render doc "md"`/`"markdown"`/`:md`, `content/markdown`, `asMarkdown`. headings (`#`×level), fenced code, `> ` quote, `![alt](src)`, `- `/`1. ` lists, `---`; doc joins blocks with a blank line. No MD escaping yet. 20 tests; suite 263/263. - 2026-06-07 — Extension: asSx wire string-escaping. Added `String>>sxEscaped` (escapes `\`→`\\` then `"`→`\"`) and routed every `asSx` text/attr/list-item through it, so the SX wire format stays valid when content contains quotes or backslashes. +5 render tests (expected strings built from `q`/`bs` helpers to avoid escaping miscounts). Suite 243/243. - 2026-06-07 — Extension: HTML escaping at the render boundary. Added `String>>htmlEscaped` (recursive char walk escaping & < > ", order-safe so & isn't double-escaped) and routed every `asHTML` text/attr through it — heading, text, code body + language, quote, image src/alt, embed url, list items. Render stays fully polymorphic in Smalltalk; escaping lives at the boundary. +8 render tests (incl. `