From 8dc918764599be1d0d90a388593f7c9a6acd8747 Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 7 Jun 2026 00:08:42 +0000 Subject: [PATCH] content: content/* API facade + 26 tests (Phase 1 complete, 133/133) Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/content/api.sx | 63 +++++++++++++++++++++++ lib/content/conformance.sh | 3 +- lib/content/scoreboard.json | 7 +-- lib/content/scoreboard.md | 3 +- lib/content/tests/api.sx | 99 +++++++++++++++++++++++++++++++++++++ plans/content-on-sx.md | 11 ++++- 6 files changed, 179 insertions(+), 7 deletions(-) create mode 100644 lib/content/api.sx create mode 100644 lib/content/tests/api.sx diff --git a/lib/content/api.sx b/lib/content/api.sx new file mode 100644 index 00000000..becac4ed --- /dev/null +++ b/lib/content/api.sx @@ -0,0 +1,63 @@ +;; content-on-sx — public API facade. +;; +;; The stable surface other code calls. Composes block + doc + render. Document +;; values are immutable; every edit returns a new document, so callers hold +;; explicit versions (the persist op log in Phase 2 becomes the source of truth). +;; +;; Requires (loaded by the harness): block.sx, doc.sx, render.sx and a base +;; Smalltalk class table (st-bootstrap-classes!). + +;; Register the content class hierarchy + render methods. Caller bootstraps the +;; base Smalltalk classes first; this only adds content classes (idempotent). +(define + content/bootstrap! + (fn + () + (begin + (content-bootstrap-blocks!) + (content-bootstrap-doc!) + (content-bootstrap-render!) + true))) + +;; ── documents ── +(define content/new doc-new) +(define content/empty doc-empty) +(define content/append doc-append) +(define content/blocks doc-blocks) +(define content/count doc-count) +(define content/find doc-find) +(define content/has? doc-has?) +(define content/ids doc-ids) +(define content/types doc-types) + +;; ── blocks ── +(define content/block mk-block) + +;; ── edit ops (data payload) ── +(define content/insert op-insert) +(define content/update op-update) +(define content/move op-move) +(define content/delete op-delete) + +(define content/op? (fn (x) (and (dict? x) (has-key? x :op)))) + +;; edit — apply one op or a stream of ops; returns a new document. +(define + content/edit + (fn + (doc ops) + (if (content/op? ops) (doc-apply doc ops) (doc-apply-all doc ops)))) + +;; ── render boundary ── +;; fmt is "html"/"sx" (or :html/:sx — keywords evaluate to their name). +(define + content/render + (fn + (doc fmt) + (cond + ((= fmt "html") (asHTML doc)) + ((= fmt "sx") (asSx doc)) + (else (error (str "unknown render format: " fmt)))))) + +(define content/html asHTML) +(define content/sx asSx) diff --git a/lib/content/conformance.sh b/lib/content/conformance.sh index 60931794..de8ba21a 100755 --- a/lib/content/conformance.sh +++ b/lib/content/conformance.sh @@ -15,7 +15,7 @@ if [ ! -x "$SX_SERVER" ]; then fi fi -SUITES=(block doc render) +SUITES=(block doc render api) OUT_JSON="lib/content/scoreboard.json" OUT_MD="lib/content/scoreboard.md" @@ -36,6 +36,7 @@ run_suite() { (load "lib/content/block.sx") (load "lib/content/doc.sx") (load "lib/content/render.sx") +(load "lib/content/api.sx") (epoch 2) (eval "(define content-test-pass 0)") (eval "(define content-test-fail 0)") diff --git a/lib/content/scoreboard.json b/lib/content/scoreboard.json index f50e783e..b9270ccf 100644 --- a/lib/content/scoreboard.json +++ b/lib/content/scoreboard.json @@ -2,9 +2,10 @@ "suites": { "block": {"pass": 38, "fail": 0}, "doc": {"pass": 40, "fail": 0}, - "render": {"pass": 29, "fail": 0} + "render": {"pass": 29, "fail": 0}, + "api": {"pass": 26, "fail": 0} }, - "total_pass": 107, + "total_pass": 133, "total_fail": 0, - "total": 107 + "total": 133 } diff --git a/lib/content/scoreboard.md b/lib/content/scoreboard.md index 40e6e23a..526ac685 100644 --- a/lib/content/scoreboard.md +++ b/lib/content/scoreboard.md @@ -7,4 +7,5 @@ _Generated by `lib/content/conformance.sh`_ | block | 38 | 0 | 38 | | doc | 40 | 0 | 40 | | render | 29 | 0 | 29 | -| **Total** | **107** | **0** | **107** | +| api | 26 | 0 | 26 | +| **Total** | **133** | **0** | **133** | diff --git a/lib/content/tests/api.sx b/lib/content/tests/api.sx new file mode 100644 index 00000000..4c3fa3ea --- /dev/null +++ b/lib/content/tests/api.sx @@ -0,0 +1,99 @@ +;; Phase 1 — public API facade. End-to-end through content/*. + +(st-bootstrap-classes!) +(content/bootstrap!) + +;; ── build a document via the facade ── +(define d0 (content/empty "post")) +(define + h + (content/block + "heading" + "h" + (list (list "level" 1) (list "text" "Hi")))) +(define p (content/block "text" "p" (list (list "text" "World")))) +(define d1 (content/append (content/append d0 h) p)) + +(content/op? (content/insert h nil)) +(content-test "count" (content/count d1) 2) +(content-test "ids" (content/ids d1) (list "h" "p")) +(content-test "types" (content/types d1) (list "heading" "text")) +(content-test "find" (blk-id (content/find d1 "p")) "p") +(content-test "has? yes" (content/has? d1 "h") true) +(content-test "has? no" (content/has? d1 "x") false) + +;; ── content/op? distinguishes a single op from a list / a block ── +(content-test "op? on insert" (content/op? (content/insert h nil)) true) +(content-test + "op? on update" + (content/op? (content/update "p" "text" "z")) + true) +(content-test "op? on list" (content/op? (list (content/delete "h"))) false) +(content-test "op? on block" (content/op? h) false) +(content-test "op? on doc" (content/op? d1) false) + +;; ── edit with a single op ── +(define + img + (content/block + "image" + "img" + (list (list "src" "/c.png") (list "alt" "cat")))) +(define d2 (content/edit d1 (content/insert img "h"))) +(content-test "edit single op order" (content/ids d2) (list "h" "img" "p")) +(content-test "edit single immutable" (content/ids d1) (list "h" "p")) +(content-test + "edit update" + (str + (blk-send + (content/find + (content/edit d1 (content/update "p" "text" "Edited")) + "p") + "text")) + "Edited") +(content-test + "edit delete" + (content/ids (content/edit d1 (content/delete "h"))) + (list "p")) +(content-test + "edit move" + (content/ids (content/edit d1 (content/move "p" 0))) + (list "p" "h")) + +;; ── edit with a stream of ops ── +(define ops (list (content/insert img "h") (content/delete "p"))) +(content-test + "edit op stream" + (content/ids (content/edit d1 ops)) + (list "h" "img")) +(content-test "edit op stream immutable" (content/ids d1) (list "h" "p")) + +;; ── render via facade ── +(content-test + "render html" + (content/render d1 "html") + "

Hi

World

") +(content-test + "render sx" + (content/render d1 "sx") + "(article (h1 \"Hi\")(p \"World\"))") +(content-test + "render html keyword" + (content/render d1 :html) + "

Hi

World

") +(content-test + "render sx keyword" + (content/render d1 :sx) + "(article (h1 \"Hi\")(p \"World\"))") +(content-test "content/html" (content/html d1) "

Hi

World

") +(content-test "content/sx" (content/sx d1) "(article (h1 \"Hi\")(p \"World\"))") + +;; ── render reflects each version ── +(content-test + "render edited version" + (content/render (content/edit d1 (content/update "h" "text" "Hey")) "html") + "

Hey

World

") +(content-test + "render original unchanged" + (content/render d1 "html") + "

Hi

World

") diff --git a/plans/content-on-sx.md b/plans/content-on-sx.md index 3024cc52..7917f442 100644 --- a/plans/content-on-sx.md +++ b/plans/content-on-sx.md @@ -19,7 +19,7 @@ injected adapter, not core. ## Status (rolling) -`bash lib/content/conformance.sh` → **107/107** (Phase 1: blocks + doc + render) +`bash lib/content/conformance.sh` → **133/133** (Phase 1 complete: blocks + doc + render + api) ## Ground rules @@ -60,7 +60,7 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─ - [x] `block.sx` — typed block objects - [x] `doc.sx` — ordered tree, apply edit op, structural moves - [x] `render.sx` — block tree → HTML/SX -- [ ] `api.sx` + tests + scoreboard + conformance.sh +- [x] `api.sx` + tests + scoreboard + conformance.sh ## Phase 2 — Op log + versioning - [ ] edit ops as `persist` events; replay to any version @@ -77,6 +77,13 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─ ## Progress log +- 2026-06-07 — Phase 1 `api.sx` (**Phase 1 complete**): `content/*` facade over + block + doc + render. `content/bootstrap!` registers the hierarchy; + `content/edit` applies one op or an op stream; `content/render` picks the + boundary format ("html"/"sx" or keyword). Re-exports `content/new`, + `content/append`, `content/insert|update|move|delete`, `content/find`, etc. + `content/op?` distinguishes a single op from a list/block. 26 tests; suite + 133/133. content/history deferred to Phase 2 (needs the persist op log). - 2026-06-07 — Phase 1 `render.sx`: render boundary as polymorphic message dispatch. Every block and `CtDoc` answers `asHTML` / `asSx`; the document folds children via Smalltalk `inject:into:` (works on raw SX lists), so