From a5ff21015e016ce9b9c20ca2d11afd978844f8be Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 7 Jun 2026 03:02:54 +0000 Subject: [PATCH] content: document composition (compose.sx) + 17 tests (502/502) Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/content/compose.sx | 42 ++++++++++++++++++++ lib/content/conformance.sh | 3 +- lib/content/scoreboard.json | 5 ++- lib/content/scoreboard.md | 3 +- lib/content/tests/compose.sx | 76 ++++++++++++++++++++++++++++++++++++ plans/content-on-sx.md | 8 +++- 6 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 lib/content/compose.sx create mode 100644 lib/content/tests/compose.sx diff --git a/lib/content/compose.sx b/lib/content/compose.sx new file mode 100644 index 00000000..e6f19201 --- /dev/null +++ b/lib/content/compose.sx @@ -0,0 +1,42 @@ +;; content-on-sx — document composition. +;; +;; Combine documents (header + body + footer, templates, partials) into a new +;; document. The result keeps the FIRST document's id and metadata; blocks are +;; concatenated. Immutable — inputs are untouched. Block-id collisions across +;; combined docs are the caller's concern (content/validate flags duplicates). +;; +;; Requires (loaded by harness): doc.sx. + +(define + content/concat + (fn (a b) (doc-with-blocks a (append (doc-blocks a) (doc-blocks b))))) + +(define + content/prepend + (fn (a b) (doc-with-blocks a (append (doc-blocks b) (doc-blocks a))))) + +(define + content/-concat-fold + (fn + (acc more) + (if + (= (len more) 0) + acc + (content/-concat-fold (content/concat acc (first more)) (rest more))))) + +(define + content/concat-all + (fn + (docs) + (if + (= (len docs) 0) + (doc-empty "merged") + (content/-concat-fold (first docs) (rest docs))))) + +;; wrap a document's blocks inside a single section (collapse to a subtree). +;; Requires section.sx (mk-section) when used. +(define + content/wrap-section + (fn + (doc section-id) + (doc-with-blocks doc (list (mk-section section-id (doc-blocks doc)))))) diff --git a/lib/content/conformance.sh b/lib/content/conformance.sh index 664dd8ea..1a7d54da 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 api meta page page-full markdown text section stats table validate store snapshot crdt crdt-store sync md-import md-doc fed) +SUITES=(block doc render api meta page page-full markdown text section compose stats table validate store snapshot crdt crdt-store sync md-import md-doc fed) OUT_JSON="lib/content/scoreboard.json" OUT_MD="lib/content/scoreboard.md" @@ -45,6 +45,7 @@ run_suite() { (load "lib/content/meta.sx") (load "lib/content/text.sx") (load "lib/content/section.sx") +(load "lib/content/compose.sx") (load "lib/content/stats.sx") (load "lib/content/table.sx") (load "lib/content/page.sx") diff --git a/lib/content/scoreboard.json b/lib/content/scoreboard.json index 580c50e6..45b6981f 100644 --- a/lib/content/scoreboard.json +++ b/lib/content/scoreboard.json @@ -10,6 +10,7 @@ "markdown": {"pass": 20, "fail": 0}, "text": {"pass": 20, "fail": 0}, "section": {"pass": 25, "fail": 0}, + "compose": {"pass": 17, "fail": 0}, "stats": {"pass": 17, "fail": 0}, "table": {"pass": 15, "fail": 0}, "validate": {"pass": 23, "fail": 0}, @@ -22,7 +23,7 @@ "md-doc": {"pass": 12, "fail": 0}, "fed": {"pass": 20, "fail": 0} }, - "total_pass": 485, + "total_pass": 502, "total_fail": 0, - "total": 485 + "total": 502 } diff --git a/lib/content/scoreboard.md b/lib/content/scoreboard.md index 78bd17db..1d2e416d 100644 --- a/lib/content/scoreboard.md +++ b/lib/content/scoreboard.md @@ -14,6 +14,7 @@ _Generated by `lib/content/conformance.sh`_ | markdown | 20 | 0 | 20 | | text | 20 | 0 | 20 | | section | 25 | 0 | 25 | +| compose | 17 | 0 | 17 | | stats | 17 | 0 | 17 | | table | 15 | 0 | 15 | | validate | 23 | 0 | 23 | @@ -25,4 +26,4 @@ _Generated by `lib/content/conformance.sh`_ | md-import | 38 | 0 | 38 | | md-doc | 12 | 0 | 12 | | fed | 20 | 0 | 20 | -| **Total** | **485** | **0** | **485** | +| **Total** | **502** | **0** | **502** | diff --git a/lib/content/tests/compose.sx b/lib/content/tests/compose.sx new file mode 100644 index 00000000..d10561bf --- /dev/null +++ b/lib/content/tests/compose.sx @@ -0,0 +1,76 @@ +;; Extension — document composition. + +(st-bootstrap-classes!) +(content/bootstrap!) +(content-bootstrap-section!) + +(define + a + (doc-with-title + (doc-append (doc-empty "a") (mk-heading "h" 1 "A")) + "Doc A")) +(define + b + (doc-append + (doc-append (doc-empty "b") (mk-text "p" "B1")) + (mk-text "q" "B2"))) + +;; ── concat ── +(define ab (content/concat a b)) +(content-test "concat ids" (doc-ids ab) (list "h" "p" "q")) +(content-test "concat keeps first id" (doc-id ab) "a") +(content-test "concat keeps first title" (doc-title ab) "Doc A") +(content-test "concat immutable a" (doc-ids a) (list "h")) +(content-test "concat immutable b" (doc-ids b) (list "p" "q")) + +;; ── prepend ── +(define ba (content/prepend a b)) +(content-test "prepend ids" (doc-ids ba) (list "p" "q" "h")) +(content-test "prepend keeps a id" (doc-id ba) "a") + +;; ── concat with empty ── +(content-test + "concat empty right" + (doc-ids (content/concat a (doc-empty "e"))) + (list "h")) +(content-test + "concat empty left" + (doc-ids (content/concat (doc-empty "e") b)) + (list "p" "q")) + +;; ── concat-all ── +(define c (doc-append (doc-empty "c") (mk-divider "d"))) +(content-test + "concat-all order" + (doc-ids (content/concat-all (list a b c))) + (list "h" "p" "q" "d")) +(content-test + "concat-all keeps first id" + (doc-id (content/concat-all (list a b c))) + "a") +(content-test + "concat-all single" + (doc-ids (content/concat-all (list a))) + (list "h")) +(content-test + "concat-all empty" + (doc-ids (content/concat-all (list))) + (list)) + +;; ── render of composed doc ── +(content-test + "composed renders" + (asHTML (content/concat a b)) + "

A

B1

B2

") + +;; ── wrap-section collapses blocks into a subtree ── +(define w (content/wrap-section ab "sec")) +(content-test "wrap top-level is one section" (doc-ids w) (list "sec")) +(content-test + "wrap children preserved" + (doc-tree-ids w) + (list "sec" "h" "p" "q")) +(content-test + "wrap renders nested" + (asHTML w) + "

A

B1

B2

") diff --git a/plans/content-on-sx.md b/plans/content-on-sx.md index 222846ae..2e512e88 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` → **485/485** (Phases 1–4 COMPLETE + extensions: HTML/SX escaping, Markdown render + import/export incl. tables & frontmatter (full round-trip), CRDT replication, tree-aware validation, snapshot cache, doc metadata, plain-text render, nested block trees, doc stats, table block, HTML page wrapper + SEO page) +`bash lib/content/conformance.sh` → **502/502** (Phases 1–4 COMPLETE + extensions: HTML/SX escaping, Markdown render + import/export incl. tables & frontmatter (full round-trip), CRDT replication, tree-aware validation, snapshot cache, doc metadata, plain-text render, nested block trees, doc stats, table block, HTML page wrapper + SEO page, doc composition) ## Ground rules @@ -91,9 +91,15 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─ - [x] table block (`table.sx`: CtTable, renders html/sx/text/md, validated) - [x] HTML page wrapper (`page.sx`: content/page, escaped title from metadata) - [x] SEO page (`page-full.sx`: content/page-full, lang + meta description from excerpt) +- [x] document composition (`compose.sx`: concat/prepend/concat-all/wrap-section) ## Progress log +- 2026-06-07 — Extension: document composition (`compose.sx`). `content/concat` + / `content/prepend` / `content/concat-all` combine documents (keeping the + first's id + metadata, concatenating blocks, immutable); `content/wrap-section` + collapses a doc's blocks into a single nested section. For assembling pages + from header/body/footer parts and templates. 17 tests; suite 502/502. - 2026-06-07 — Extension: SEO-complete page (`page-full.sx`). `content/page-full` extends content/page with `` and a `` drawn from the document excerpt (plain text, escaped, 160 chars), composing the