From ec4cd63c22e9e2237eabf63c39a351ec7ad2659f Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 7 Jun 2026 05:42:02 +0000 Subject: [PATCH] content: multi-doc index + tag filtering (index.sx) + 13 tests (710/710) Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/content/conformance.sh | 3 +- lib/content/index.sx | 51 +++++++++++++++++++++++++++++++ lib/content/scoreboard.json | 5 +-- lib/content/scoreboard.md | 3 +- lib/content/tests/index.sx | 61 +++++++++++++++++++++++++++++++++++++ plans/content-on-sx.md | 8 ++++- 6 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 lib/content/index.sx create mode 100644 lib/content/tests/index.sx diff --git a/lib/content/conformance.sh b/lib/content/conformance.sh index 60bb9bc2..4901d664 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 compose tree-edit move clone query toc anchor outline flatten transform normalize find-replace stats summary table callout media data wire 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 tree-edit move clone query toc anchor outline flatten transform normalize find-replace stats summary index table callout media data wire validate store snapshot crdt crdt-store sync md-import md-doc fed) OUT_JSON="lib/content/scoreboard.json" OUT_MD="lib/content/scoreboard.md" @@ -59,6 +59,7 @@ run_suite() { (load "lib/content/find-replace.sx") (load "lib/content/stats.sx") (load "lib/content/summary.sx") +(load "lib/content/index.sx") (load "lib/content/table.sx") (load "lib/content/callout.sx") (load "lib/content/media.sx") diff --git a/lib/content/index.sx b/lib/content/index.sx new file mode 100644 index 00000000..1820179d --- /dev/null +++ b/lib/content/index.sx @@ -0,0 +1,51 @@ +;; content-on-sx — multi-document index. +;; +;; Projects a list of documents into summary cards (the blog index page), with +;; tag filtering (category pages) and a tag cloud. Composes content/summary + +;; doc metadata. +;; +;; Requires (loaded by harness): summary.sx (content/summary), meta.sx (doc-tags). + +(define + idx-in? + (fn + (x xs) + (cond + ((= (len xs) 0) false) + ((= (first xs) x) true) + (else (idx-in? x (rest xs)))))) + +(define + idx-dedup + (fn + (xs seen) + (if + (= (len xs) 0) + (reverse seen) + (if + (idx-in? (first xs) seen) + (idx-dedup (rest xs) seen) + (idx-dedup (rest xs) (cons (first xs) seen)))))) + +(define content/index (fn (docs) (map content/summary docs))) + +(define content/has-tag? (fn (doc tag) (idx-in? tag (doc-tags doc)))) + +(define + content/index-by-tag + (fn + (docs tag) + (map content/summary (filter (fn (d) (content/has-tag? d tag)) docs)))) + +(define + content/all-tags + (fn (docs) (idx-dedup (ct-flatmap-tags docs) (list)))) + +(define + ct-flatmap-tags + (fn + (docs) + (if + (= (len docs) 0) + (list) + (append (doc-tags (first docs)) (ct-flatmap-tags (rest docs)))))) diff --git a/lib/content/scoreboard.json b/lib/content/scoreboard.json index 3cc0d400..e5f2d14d 100644 --- a/lib/content/scoreboard.json +++ b/lib/content/scoreboard.json @@ -24,6 +24,7 @@ "find-replace": {"pass": 10, "fail": 0}, "stats": {"pass": 17, "fail": 0}, "summary": {"pass": 14, "fail": 0}, + "index": {"pass": 13, "fail": 0}, "table": {"pass": 15, "fail": 0}, "callout": {"pass": 12, "fail": 0}, "media": {"pass": 15, "fail": 0}, @@ -39,7 +40,7 @@ "md-doc": {"pass": 12, "fail": 0}, "fed": {"pass": 20, "fail": 0} }, - "total_pass": 697, + "total_pass": 710, "total_fail": 0, - "total": 697 + "total": 710 } diff --git a/lib/content/scoreboard.md b/lib/content/scoreboard.md index b46bb402..5ae81e72 100644 --- a/lib/content/scoreboard.md +++ b/lib/content/scoreboard.md @@ -28,6 +28,7 @@ _Generated by `lib/content/conformance.sh`_ | find-replace | 10 | 0 | 10 | | stats | 17 | 0 | 17 | | summary | 14 | 0 | 14 | +| index | 13 | 0 | 13 | | table | 15 | 0 | 15 | | callout | 12 | 0 | 12 | | media | 15 | 0 | 15 | @@ -42,4 +43,4 @@ _Generated by `lib/content/conformance.sh`_ | md-import | 38 | 0 | 38 | | md-doc | 12 | 0 | 12 | | fed | 20 | 0 | 20 | -| **Total** | **697** | **0** | **697** | +| **Total** | **710** | **0** | **710** | diff --git a/lib/content/tests/index.sx b/lib/content/tests/index.sx new file mode 100644 index 00000000..e0c979ba --- /dev/null +++ b/lib/content/tests/index.sx @@ -0,0 +1,61 @@ +;; Extension — multi-document index + tag filtering. + +(st-bootstrap-classes!) +(content/bootstrap!) +(content-bootstrap-text!) + +(define + a + (doc-with-meta + (doc-append (doc-empty "a") (mk-text "p" "first post")) + {:title "A" :tags (list "sx" "news")})) +(define + b + (doc-with-meta + (doc-append (doc-empty "b") (mk-text "p" "second post")) + {:title "B" :tags (list "news")})) +(define + c + (doc-with-meta + (doc-append (doc-empty "c") (mk-text "p" "third")) + {:title "C" :tags (list "sx")})) +(define docs (list a b c)) + +;; ── index = list of summaries ── +(define idx (content/index docs)) +(content-test "index count" (len idx) 3) +(content-test + "index titles" + (map (fn (s) (get s :title)) idx) + (list "A" "B" "C")) +(content-test + "index ids" + (map (fn (s) (get s :id)) idx) + (list "a" "b" "c")) +(content-test "index excerpt" (get (first idx) :excerpt) "first post") + +;; ── has-tag? ── +(content-test "has-tag yes" (content/has-tag? a "news") true) +(content-test "has-tag no" (content/has-tag? c "news") false) + +;; ── index-by-tag (category page) ── +(content-test + "by-tag news" + (map (fn (s) (get s :id)) (content/index-by-tag docs "news")) + (list "a" "b")) +(content-test + "by-tag sx" + (map (fn (s) (get s :id)) (content/index-by-tag docs "sx")) + (list "a" "c")) +(content-test "by-tag none" (content/index-by-tag docs "missing") (list)) + +;; ── all-tags (tag cloud, deduped, document order) ── +(content-test "all-tags" (content/all-tags docs) (list "sx" "news")) +(content-test "all-tags empty" (content/all-tags (list)) (list)) +(content-test + "all-tags untagged" + (content/all-tags (list (doc-empty "x"))) + (list)) + +;; ── empty index ── +(content-test "empty index" (content/index (list)) (list)) diff --git a/plans/content-on-sx.md b/plans/content-on-sx.md index ad14f78b..c30c66af 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` → **697/697** (Phases 1–4 COMPLETE + ~32 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 + deep editing + flatten + relative reorder, doc stats + summary, table + callout + media blocks, HTML page wrapper + SEO page, doc composition + id-remap, portable data + wire serialization, block query + transforms + find/replace, TOC + anchored headings + outline, normalization) +`bash lib/content/conformance.sh` → **710/710** (Phases 1–4 COMPLETE + ~33 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 + deep editing + flatten + relative reorder, doc stats + summary + multi-doc index, table + callout + media blocks, HTML page wrapper + SEO page, doc composition + id-remap, portable data + wire serialization, block query + transforms + find/replace, TOC + anchored headings + outline, normalization) ## Ground rules @@ -92,6 +92,7 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─ - [x] callout block (`callout.sx`: CtCallout note/warning/tip, renders html/sx/text/md, validated) - [x] media block (`media.sx`: CtMedia video/audio, renders html/sx/text/md, validated) - [x] list-card summary (`summary.sx`: content/summary — title/excerpt/words/reading/cover) +- [x] multi-doc index (`index.sx`: content/index + index-by-tag + all-tags + has-tag?) - [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) @@ -111,6 +112,11 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─ ## Progress log +- 2026-06-07 — Extension: multi-document index (`index.sx`). `content/index` + projects a doc list into summary cards (blog index); `content/index-by-tag` + filters by tag (category pages); `content/all-tags` is a deduped tag cloud; + `content/has-tag?`. Composes content/summary + doc metadata. 13 tests; suite + 710/710. - 2026-06-07 — Extension: list-card summary (`summary.sx`). `content/summary` returns `{:id :title :excerpt :words :reading-minutes :cover}` for index/listing cards, composing metadata + text + stats + query (`content/cover` = first