From 4e26b3c0f794437c48a7fa85c5beb0db3dd4b720 Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 7 Jun 2026 03:25:46 +0000 Subject: [PATCH] content: deep tree editing (tree-edit.sx) + 17 tests (551/551) Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/content/conformance.sh | 3 +- lib/content/scoreboard.json | 5 +- lib/content/scoreboard.md | 3 +- lib/content/tests/tree-edit.sx | 91 ++++++++++++++++++++++++++++++++ lib/content/tree-edit.sx | 96 ++++++++++++++++++++++++++++++++++ plans/content-on-sx.md | 8 ++- 6 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 lib/content/tests/tree-edit.sx create mode 100644 lib/content/tree-edit.sx diff --git a/lib/content/conformance.sh b/lib/content/conformance.sh index 4c4ab5b5..11a0a257 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 stats table 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 stats table 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" @@ -46,6 +46,7 @@ run_suite() { (load "lib/content/text.sx") (load "lib/content/section.sx") (load "lib/content/compose.sx") +(load "lib/content/tree-edit.sx") (load "lib/content/stats.sx") (load "lib/content/table.sx") (load "lib/content/data.sx") diff --git a/lib/content/scoreboard.json b/lib/content/scoreboard.json index 6a90f654..1da17166 100644 --- a/lib/content/scoreboard.json +++ b/lib/content/scoreboard.json @@ -11,6 +11,7 @@ "text": {"pass": 20, "fail": 0}, "section": {"pass": 25, "fail": 0}, "compose": {"pass": 17, "fail": 0}, + "tree-edit": {"pass": 17, "fail": 0}, "stats": {"pass": 17, "fail": 0}, "table": {"pass": 15, "fail": 0}, "data": {"pass": 21, "fail": 0}, @@ -25,7 +26,7 @@ "md-doc": {"pass": 12, "fail": 0}, "fed": {"pass": 20, "fail": 0} }, - "total_pass": 534, + "total_pass": 551, "total_fail": 0, - "total": 534 + "total": 551 } diff --git a/lib/content/scoreboard.md b/lib/content/scoreboard.md index 9497efba..1eefa8f5 100644 --- a/lib/content/scoreboard.md +++ b/lib/content/scoreboard.md @@ -15,6 +15,7 @@ _Generated by `lib/content/conformance.sh`_ | text | 20 | 0 | 20 | | section | 25 | 0 | 25 | | compose | 17 | 0 | 17 | +| tree-edit | 17 | 0 | 17 | | stats | 17 | 0 | 17 | | table | 15 | 0 | 15 | | data | 21 | 0 | 21 | @@ -28,4 +29,4 @@ _Generated by `lib/content/conformance.sh`_ | md-import | 38 | 0 | 38 | | md-doc | 12 | 0 | 12 | | fed | 20 | 0 | 20 | -| **Total** | **534** | **0** | **534** | +| **Total** | **551** | **0** | **551** | diff --git a/lib/content/tests/tree-edit.sx b/lib/content/tests/tree-edit.sx new file mode 100644 index 00000000..43c41911 --- /dev/null +++ b/lib/content/tests/tree-edit.sx @@ -0,0 +1,91 @@ +;; Extension — deep tree editing (update/delete/insert into nested sections). + +(st-bootstrap-classes!) +(content/bootstrap!) +(content-bootstrap-section!) + +;; doc: top / sec[ a, inner[ b ] ] +(define + d + (doc-append + (doc-append (doc-empty "d") (mk-text "top" "T")) + (mk-section + "sec" + (list + (mk-text "a" "A") + (mk-section "inner" (list (mk-text "b" "B"))))))) + +;; ── deep-update a nested block ── +(define d1 (doc-deep-update d "b" "text" "Edited")) +(content-test + "deep-update nested" + (str (blk-send (doc-deep-find d1 "b") "text")) + "Edited") +(content-test + "deep-update immutable" + (str (blk-send (doc-deep-find d "b") "text")) + "B") +(content-test + "deep-update top-level" + (str + (blk-send + (doc-deep-find (doc-deep-update d "top" "text" "X") "top") + "text")) + "X") +(content-test + "deep-update mid-section" + (str + (blk-send (doc-deep-find (doc-deep-update d "a" "text" "AA") "a") "text")) + "AA") +(content-test + "deep-update preserves tree" + (doc-tree-ids d1) + (doc-tree-ids d)) + +;; ── deep-replace ── +(define d2 (doc-deep-replace d "b" (mk-heading "b" 3 "H"))) +(content-test + "deep-replace type" + (blk-type (doc-deep-find d2 "b")) + "heading") +(content-test + "deep-replace render" + (asHTML d2) + "

T

A

H

") + +;; ── deep-delete ── +(define d3 (doc-deep-delete d "b")) +(content-test "deep-delete removes nested" (doc-deep-find d3 "b") nil) +(content-test + "deep-delete tree-ids" + (doc-tree-ids d3) + (list "top" "sec" "a" "inner")) +(content-test "deep-delete immutable" (doc-tree-count d) 5) +(content-test + "deep-delete mid-section" + (doc-tree-ids (doc-deep-delete d "a")) + (list "top" "sec" "inner" "b")) +(content-test + "deep-delete top-level" + (doc-tree-ids (doc-deep-delete d "top")) + (list "sec" "a" "inner" "b")) + +;; ── deep-insert-into a nested section ── +(define d4 (doc-deep-insert-into d "inner" (mk-text "c" "C"))) +(content-test + "insert-into nested" + (doc-tree-ids d4) + (list "top" "sec" "a" "inner" "b" "c")) +(content-test + "insert-into found" + (str (blk-send (doc-deep-find d4 "c") "text")) + "C") +(content-test + "insert-into outer section" + (doc-tree-ids (doc-deep-insert-into d "sec" (mk-divider "dv"))) + (list "top" "sec" "a" "inner" "b" "dv")) +(content-test "insert-into immutable" (doc-tree-count d) 5) +(content-test + "insert-into render" + (asHTML d4) + "

T

A

B

C

") diff --git a/lib/content/tree-edit.sx b/lib/content/tree-edit.sx new file mode 100644 index 00000000..7f424862 --- /dev/null +++ b/lib/content/tree-edit.sx @@ -0,0 +1,96 @@ +;; content-on-sx — deep tree editing. +;; +;; Mutate blocks anywhere in the nested tree (descending into CtSection children), +;; complementing the top-level doc ops and the deep-find read path. All return +;; new documents (immutable). +;; +;; Requires (loaded by harness): doc.sx, section.sx (section? / section-children / +;; section-with-children / section-append). + +;; map f over every block in the tree, replacing the one whose id matches. +(define + block-tree-update + (fn + (blocks id f) + (map + (fn + (b) + (if + (= (blk-id b) id) + (f b) + (if + (section? b) + (section-with-children + b + (block-tree-update (section-children b) id f)) + b))) + blocks))) + +;; remove the block with id from anywhere in the tree. +(define + block-tree-delete + (fn + (blocks id) + (map + (fn + (b) + (if + (section? b) + (section-with-children + b + (block-tree-delete (section-children b) id)) + b)) + (filter (fn (b) (if (= (blk-id b) id) false true)) blocks)))) + +;; append a block into the children of the section with section-id. +(define + block-tree-insert-into + (fn + (blocks section-id block) + (map + (fn + (b) + (if + (section? b) + (if + (= (blk-id b) section-id) + (section-append b block) + (section-with-children + b + (block-tree-insert-into (section-children b) section-id block))) + b)) + blocks))) + +;; ── document-level deep ops ── +(define + doc-deep-update + (fn + (doc id field value) + (doc-with-blocks + doc + (block-tree-update + (doc-blocks doc) + id + (fn (b) (blk-set b field value)))))) + +(define + doc-deep-replace + (fn + (doc id newblock) + (doc-with-blocks + doc + (block-tree-update (doc-blocks doc) id (fn (b) newblock))))) + +(define + doc-deep-delete + (fn + (doc id) + (doc-with-blocks doc (block-tree-delete (doc-blocks doc) id)))) + +(define + doc-deep-insert-into + (fn + (doc section-id block) + (doc-with-blocks + doc + (block-tree-insert-into (doc-blocks doc) section-id block)))) diff --git a/plans/content-on-sx.md b/plans/content-on-sx.md index b11f284b..4dbca23f 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` → **534/534** (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, portable data + wire serialization) +`bash lib/content/conformance.sh` → **551/551** (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 + deep tree editing, doc stats, table block, HTML page wrapper + SEO page, doc composition, portable data + wire serialization) ## Ground rules @@ -92,11 +92,17 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─ - [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) +- [x] deep tree editing (`tree-edit.sx`: doc-deep-update/replace/delete/insert-into) - [x] portable data serialization (`data.sx`: content/to-data + from-data, round-trips tree) - [x] wire serialization (`wire.sx`: content/to-wire + from-wire, SX-text on the wire) ## Progress log +- 2026-06-07 — Extension: deep tree editing (`tree-edit.sx`). `doc-deep-update` + / `doc-deep-replace` / `doc-deep-delete` / `doc-deep-insert-into` mutate blocks + anywhere in the nested tree (descending into CtSection children), completing + tree mutation to match the deep-find read path; all immutable. 17 tests; suite + 551/551. - 2026-06-07 — Extension: on-the-wire serialization (`wire.sx`). `content/to-wire` serialises a document to a transmittable SX-text string (data form + SX serializer); `content/from-wire` parses it back into a live document.