content: content/* API facade + 26 tests (Phase 1 complete, 133/133)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 52s

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 00:08:42 +00:00
parent 0d93a9820f
commit 8dc9187645
6 changed files with 179 additions and 7 deletions

63
lib/content/api.sx Normal file
View File

@@ -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)

View File

@@ -15,7 +15,7 @@ if [ ! -x "$SX_SERVER" ]; then
fi fi
fi fi
SUITES=(block doc render) SUITES=(block doc render api)
OUT_JSON="lib/content/scoreboard.json" OUT_JSON="lib/content/scoreboard.json"
OUT_MD="lib/content/scoreboard.md" OUT_MD="lib/content/scoreboard.md"
@@ -36,6 +36,7 @@ run_suite() {
(load "lib/content/block.sx") (load "lib/content/block.sx")
(load "lib/content/doc.sx") (load "lib/content/doc.sx")
(load "lib/content/render.sx") (load "lib/content/render.sx")
(load "lib/content/api.sx")
(epoch 2) (epoch 2)
(eval "(define content-test-pass 0)") (eval "(define content-test-pass 0)")
(eval "(define content-test-fail 0)") (eval "(define content-test-fail 0)")

View File

@@ -2,9 +2,10 @@
"suites": { "suites": {
"block": {"pass": 38, "fail": 0}, "block": {"pass": 38, "fail": 0},
"doc": {"pass": 40, "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_fail": 0,
"total": 107 "total": 133
} }

View File

@@ -7,4 +7,5 @@ _Generated by `lib/content/conformance.sh`_
| block | 38 | 0 | 38 | | block | 38 | 0 | 38 |
| doc | 40 | 0 | 40 | | doc | 40 | 0 | 40 |
| render | 29 | 0 | 29 | | render | 29 | 0 | 29 |
| **Total** | **107** | **0** | **107** | | api | 26 | 0 | 26 |
| **Total** | **133** | **0** | **133** |

99
lib/content/tests/api.sx Normal file
View File

@@ -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")
"<h1>Hi</h1><p>World</p>")
(content-test
"render sx"
(content/render d1 "sx")
"(article (h1 \"Hi\")(p \"World\"))")
(content-test
"render html keyword"
(content/render d1 :html)
"<h1>Hi</h1><p>World</p>")
(content-test
"render sx keyword"
(content/render d1 :sx)
"(article (h1 \"Hi\")(p \"World\"))")
(content-test "content/html" (content/html d1) "<h1>Hi</h1><p>World</p>")
(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")
"<h1>Hey</h1><p>World</p>")
(content-test
"render original unchanged"
(content/render d1 "html")
"<h1>Hi</h1><p>World</p>")

View File

@@ -19,7 +19,7 @@ injected adapter, not core.
## Status (rolling) ## 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 ## Ground rules
@@ -60,7 +60,7 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─
- [x] `block.sx` — typed block objects - [x] `block.sx` — typed block objects
- [x] `doc.sx` — ordered tree, apply edit op, structural moves - [x] `doc.sx` — ordered tree, apply edit op, structural moves
- [x] `render.sx` — block tree → HTML/SX - [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 ## Phase 2 — Op log + versioning
- [ ] edit ops as `persist` events; replay to any version - [ ] 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 ## 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 - 2026-06-07 — Phase 1 `render.sx`: render boundary as polymorphic message
dispatch. Every block and `CtDoc` answers `asHTML` / `asSx`; the document dispatch. Every block and `CtDoc` answers `asHTML` / `asSx`; the document
folds children via Smalltalk `inject:into:` (works on raw SX lists), so folds children via Smalltalk `inject:into:` (works on raw SX lists), so