content: document composition (compose.sx) + 17 tests (502/502)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 59s

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 03:02:54 +00:00
parent 20867a62c3
commit a5ff21015e
6 changed files with 132 additions and 5 deletions

42
lib/content/compose.sx Normal file
View File

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

View File

@@ -15,7 +15,7 @@ if [ ! -x "$SX_SERVER" ]; then
fi fi
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_JSON="lib/content/scoreboard.json"
OUT_MD="lib/content/scoreboard.md" OUT_MD="lib/content/scoreboard.md"
@@ -45,6 +45,7 @@ run_suite() {
(load "lib/content/meta.sx") (load "lib/content/meta.sx")
(load "lib/content/text.sx") (load "lib/content/text.sx")
(load "lib/content/section.sx") (load "lib/content/section.sx")
(load "lib/content/compose.sx")
(load "lib/content/stats.sx") (load "lib/content/stats.sx")
(load "lib/content/table.sx") (load "lib/content/table.sx")
(load "lib/content/page.sx") (load "lib/content/page.sx")

View File

@@ -10,6 +10,7 @@
"markdown": {"pass": 20, "fail": 0}, "markdown": {"pass": 20, "fail": 0},
"text": {"pass": 20, "fail": 0}, "text": {"pass": 20, "fail": 0},
"section": {"pass": 25, "fail": 0}, "section": {"pass": 25, "fail": 0},
"compose": {"pass": 17, "fail": 0},
"stats": {"pass": 17, "fail": 0}, "stats": {"pass": 17, "fail": 0},
"table": {"pass": 15, "fail": 0}, "table": {"pass": 15, "fail": 0},
"validate": {"pass": 23, "fail": 0}, "validate": {"pass": 23, "fail": 0},
@@ -22,7 +23,7 @@
"md-doc": {"pass": 12, "fail": 0}, "md-doc": {"pass": 12, "fail": 0},
"fed": {"pass": 20, "fail": 0} "fed": {"pass": 20, "fail": 0}
}, },
"total_pass": 485, "total_pass": 502,
"total_fail": 0, "total_fail": 0,
"total": 485 "total": 502
} }

View File

@@ -14,6 +14,7 @@ _Generated by `lib/content/conformance.sh`_
| markdown | 20 | 0 | 20 | | markdown | 20 | 0 | 20 |
| text | 20 | 0 | 20 | | text | 20 | 0 | 20 |
| section | 25 | 0 | 25 | | section | 25 | 0 | 25 |
| compose | 17 | 0 | 17 |
| stats | 17 | 0 | 17 | | stats | 17 | 0 | 17 |
| table | 15 | 0 | 15 | | table | 15 | 0 | 15 |
| validate | 23 | 0 | 23 | | validate | 23 | 0 | 23 |
@@ -25,4 +26,4 @@ _Generated by `lib/content/conformance.sh`_
| md-import | 38 | 0 | 38 | | md-import | 38 | 0 | 38 |
| md-doc | 12 | 0 | 12 | | md-doc | 12 | 0 | 12 |
| fed | 20 | 0 | 20 | | fed | 20 | 0 | 20 |
| **Total** | **485** | **0** | **485** | | **Total** | **502** | **0** | **502** |

View File

@@ -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))
"<h1>A</h1><p>B1</p><p>B2</p>")
;; ── 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)
"<section><h1>A</h1><p>B1</p><p>B2</p></section>")

View File

@@ -19,7 +19,7 @@ injected adapter, not core.
## Status (rolling) ## Status (rolling)
`bash lib/content/conformance.sh`**485/485** (Phases 14 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 14 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 ## 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] table block (`table.sx`: CtTable, renders html/sx/text/md, validated)
- [x] HTML page wrapper (`page.sx`: content/page, escaped title from metadata) - [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] 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 ## 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` - 2026-06-07 — Extension: SEO-complete page (`page-full.sx`). `content/page-full`
extends content/page with `<html lang="en">` and a `<meta name="description">` extends content/page with `<html lang="en">` and a `<meta name="description">`
drawn from the document excerpt (plain text, escaped, 160 chars), composing the drawn from the document excerpt (plain text, escaped, 160 chars), composing the