diff --git a/lib/content/clone.sx b/lib/content/clone.sx new file mode 100644 index 00000000..3da5a293 --- /dev/null +++ b/lib/content/clone.sx @@ -0,0 +1,34 @@ +;; content-on-sx — block id remapping / clone. +;; +;; Deep-rewrite every block id in the tree (descending into sections) by applying +;; a function. Enables collision-free composition: prefix one document's ids +;; before concatenating it with another. Immutable; content is unchanged, only +;; ids. +;; +;; Requires (loaded by harness): doc.sx, section.sx (section? / +;; section-children / section-with-children). + +(define + block-remap-id + (fn + (b f) + (let + ((nb (blk-set b "id" (f (blk-id b))))) + (if + (section? nb) + (section-with-children + nb + (map (fn (c) (block-remap-id c f)) (section-children nb))) + nb)))) + +(define + content/remap-ids + (fn + (doc f) + (doc-with-blocks + doc + (map (fn (b) (block-remap-id b f)) (doc-blocks doc))))) + +(define + content/prefix-ids + (fn (doc prefix) (content/remap-ids doc (fn (id) (str prefix id))))) diff --git a/lib/content/conformance.sh b/lib/content/conformance.sh index 11a0a257..2f233e49 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 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 clone 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" @@ -47,6 +47,7 @@ run_suite() { (load "lib/content/section.sx") (load "lib/content/compose.sx") (load "lib/content/tree-edit.sx") +(load "lib/content/clone.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 1da17166..b714d62e 100644 --- a/lib/content/scoreboard.json +++ b/lib/content/scoreboard.json @@ -12,6 +12,7 @@ "section": {"pass": 25, "fail": 0}, "compose": {"pass": 17, "fail": 0}, "tree-edit": {"pass": 17, "fail": 0}, + "clone": {"pass": 10, "fail": 0}, "stats": {"pass": 17, "fail": 0}, "table": {"pass": 15, "fail": 0}, "data": {"pass": 21, "fail": 0}, @@ -26,7 +27,7 @@ "md-doc": {"pass": 12, "fail": 0}, "fed": {"pass": 20, "fail": 0} }, - "total_pass": 551, + "total_pass": 561, "total_fail": 0, - "total": 551 + "total": 561 } diff --git a/lib/content/scoreboard.md b/lib/content/scoreboard.md index 1eefa8f5..4499a6ac 100644 --- a/lib/content/scoreboard.md +++ b/lib/content/scoreboard.md @@ -16,6 +16,7 @@ _Generated by `lib/content/conformance.sh`_ | section | 25 | 0 | 25 | | compose | 17 | 0 | 17 | | tree-edit | 17 | 0 | 17 | +| clone | 10 | 0 | 10 | | stats | 17 | 0 | 17 | | table | 15 | 0 | 15 | | data | 21 | 0 | 21 | @@ -29,4 +30,4 @@ _Generated by `lib/content/conformance.sh`_ | md-import | 38 | 0 | 38 | | md-doc | 12 | 0 | 12 | | fed | 20 | 0 | 20 | -| **Total** | **551** | **0** | **551** | +| **Total** | **561** | **0** | **561** | diff --git a/lib/content/tests/clone.sx b/lib/content/tests/clone.sx new file mode 100644 index 00000000..e5dcb292 --- /dev/null +++ b/lib/content/tests/clone.sx @@ -0,0 +1,55 @@ +;; Extension — block id remapping / clone. + +(st-bootstrap-classes!) +(content/bootstrap!) +(content-bootstrap-section!) + +(define + d + (doc-append + (doc-append (doc-empty "d") (mk-heading "h" 1 "Title")) + (mk-section "s" (list (mk-text "a" "A") (mk-text "b" "B"))))) + +;; ── prefix-ids rewrites every id in the tree ── +(define p (content/prefix-ids d "x-")) +(content-test "prefix top-level ids" (doc-ids p) (list "x-h" "x-s")) +(content-test + "prefix tree-ids" + (doc-tree-ids p) + (list "x-h" "x-s" "x-a" "x-b")) +(content-test "prefix immutable" (doc-tree-ids d) (list "h" "s" "a" "b")) +(content-test "prefix preserves content" (asHTML p) (asHTML d)) +(content-test + "prefix preserves nested content" + (str (blk-send (doc-deep-find p "x-a") "text")) + "A") + +;; ── custom remap fn ── +(define u (content/remap-ids d (fn (id) (str id "!")))) +(content-test "remap suffix" (doc-tree-ids u) (list "h!" "s!" "a!" "b!")) + +;; ── collision-free composition ── +(define + d2 + (doc-append (doc-empty "d2") (mk-heading "h" 2 "Other"))) +(define + combined + (content/concat + (content/prefix-ids d "left-") + (content/prefix-ids d2 "right-"))) +(content-test + "combined ids unique" + (doc-tree-ids combined) + (list "left-h" "left-s" "left-a" "left-b" "right-h")) +(content-test "combined validates" (content/valid? combined) true) +;; without prefixing, the shared id "h" collides +(content-test + "unprefixed collides" + (content/valid? (content/concat d d2)) + false) + +;; ── render of combined ── +(content-test + "combined render" + (asHTML combined) + "
A
B