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
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:
63
lib/content/api.sx
Normal file
63
lib/content/api.sx
Normal 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)
|
||||||
@@ -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)")
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
99
lib/content/tests/api.sx
Normal 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>")
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user