Merge loops/content into architecture: content-on-sx hardening — tree-wide content/find+has?, tree-wide revision diff, find-replace across all text-bearing fields, in-document prose search (6 commits, 778/778)
This commit is contained in:
@@ -25,8 +25,13 @@
|
|||||||
(define content/append doc-append)
|
(define content/append doc-append)
|
||||||
(define content/blocks doc-blocks)
|
(define content/blocks doc-blocks)
|
||||||
(define content/count doc-count)
|
(define content/count doc-count)
|
||||||
(define content/find doc-find)
|
;; find / has? are TREE-WIDE by id (descend into sections) — so the facade reads
|
||||||
(define content/has? doc-has?)
|
;; back any block content/edit can update or delete. content/find-top / has-top?
|
||||||
|
;; keep the top-level-only lookup for callers that mean the ordered sequence.
|
||||||
|
(define content/find doc-find-deep)
|
||||||
|
(define content/has? doc-has-deep?)
|
||||||
|
(define content/find-top doc-find)
|
||||||
|
(define content/has-top? doc-has?)
|
||||||
(define content/ids doc-ids)
|
(define content/ids doc-ids)
|
||||||
(define content/types doc-types)
|
(define content/types doc-types)
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,19 @@
|
|||||||
;; and returns a NEW document — the input is never mutated, so any version is the
|
;; and returns a NEW document — the input is never mutated, so any version is the
|
||||||
;; head of an op stream (replay-friendly for persist + CRDT merge).
|
;; head of an op stream (replay-friendly for persist + CRDT merge).
|
||||||
;;
|
;;
|
||||||
;; CtDoc also carries optional metadata (title/slug/tags) — see meta.sx for the
|
;; By-id ops (update/delete) and by-id lookup (doc-find-deep/doc-has-deep?) are
|
||||||
;; ergonomic API; they default nil and do not affect block operations.
|
;; TREE-WIDE: they descend into any block carrying a `children` list (i.e.
|
||||||
|
;; sections), since ids are unique across the tree. This keeps the persist
|
||||||
|
;; op-log, content/edit and content/find correct for nested documents.
|
||||||
|
;; insert/move are positional and act at the top level.
|
||||||
|
;;
|
||||||
|
;; CtDoc also carries optional metadata (title/slug/tags) — see meta.sx.
|
||||||
;;
|
;;
|
||||||
;; Op shapes (data, not objects — they are the persist event payload):
|
;; Op shapes (data, not objects — they are the persist event payload):
|
||||||
;; {:op "insert" :block <blk> :after <id|nil>} ; after nil = prepend
|
;; {:op "insert" :block <blk> :after <id|nil>} ; after nil = prepend (top level)
|
||||||
;; {:op "update" :id <id> :field <name> :value <v>}
|
;; {:op "update" :id <id> :field <name> :value <v>} ; tree-wide by id
|
||||||
;; {:op "move" :id <id> :index <n>}
|
;; {:op "move" :id <id> :index <n>} ; top level
|
||||||
;; {:op "delete" :id <id>}
|
;; {:op "delete" :id <id>} ; tree-wide by id
|
||||||
|
|
||||||
(define
|
(define
|
||||||
content-bootstrap-doc!
|
content-bootstrap-doc!
|
||||||
@@ -76,17 +81,58 @@
|
|||||||
(first blocks)
|
(first blocks)
|
||||||
(ct-insert-at (rest blocks) (- i 1) x))))))
|
(ct-insert-at (rest blocks) (- i 1) x))))))
|
||||||
|
|
||||||
|
;; tree-wide remove by id: drop matches at this level, recurse into children
|
||||||
|
;; (blocks carrying a `children` list, i.e. sections).
|
||||||
(define
|
(define
|
||||||
ct-remove-id
|
ct-remove-id
|
||||||
(fn
|
(fn
|
||||||
(blocks id)
|
(blocks id)
|
||||||
(filter (fn (b) (if (= (blk-id b) id) false true)) blocks)))
|
(map
|
||||||
|
(fn
|
||||||
|
(b)
|
||||||
|
(let
|
||||||
|
((ch (st-iv-get b "children")))
|
||||||
|
(if (list? ch) (st-iv-set! b "children" (ct-remove-id ch id)) b)))
|
||||||
|
(filter (fn (b) (if (= (blk-id b) id) false true)) blocks))))
|
||||||
|
|
||||||
|
;; tree-wide replace by id: apply f to the match wherever it sits in the tree.
|
||||||
(define
|
(define
|
||||||
ct-replace-id
|
ct-replace-id
|
||||||
(fn
|
(fn
|
||||||
(blocks id f)
|
(blocks id f)
|
||||||
(map (fn (b) (if (= (blk-id b) id) (f b) b)) blocks)))
|
(map
|
||||||
|
(fn
|
||||||
|
(b)
|
||||||
|
(if
|
||||||
|
(= (blk-id b) id)
|
||||||
|
(f b)
|
||||||
|
(let
|
||||||
|
((ch (st-iv-get b "children")))
|
||||||
|
(if
|
||||||
|
(list? ch)
|
||||||
|
(st-iv-set! b "children" (ct-replace-id ch id f))
|
||||||
|
b))))
|
||||||
|
blocks)))
|
||||||
|
|
||||||
|
;; tree-wide find by id: first block matching id anywhere in the tree, or nil.
|
||||||
|
;; Descends into any `children` list, mirroring ct-replace-id/ct-remove-id.
|
||||||
|
(define
|
||||||
|
ct-find-id
|
||||||
|
(fn
|
||||||
|
(blocks id)
|
||||||
|
(if
|
||||||
|
(= (len blocks) 0)
|
||||||
|
nil
|
||||||
|
(let
|
||||||
|
((b (first blocks)))
|
||||||
|
(if
|
||||||
|
(= (blk-id b) id)
|
||||||
|
b
|
||||||
|
(let
|
||||||
|
((ch (st-iv-get b "children")))
|
||||||
|
(let
|
||||||
|
((nested (if (list? ch) (ct-find-id ch id) nil)))
|
||||||
|
(if (= nested nil) (ct-find-id (rest blocks) id) nested))))))))
|
||||||
|
|
||||||
;; ── query ──
|
;; ── query ──
|
||||||
(define doc-index-of (fn (doc id) (ct-index-of (doc-blocks doc) id)))
|
(define doc-index-of (fn (doc id) (ct-index-of (doc-blocks doc) id)))
|
||||||
@@ -103,6 +149,14 @@
|
|||||||
doc-has?
|
doc-has?
|
||||||
(fn (doc id) (if (= (doc-index-of doc id) -1) false true)))
|
(fn (doc id) (if (= (doc-index-of doc id) -1) false true)))
|
||||||
|
|
||||||
|
;; tree-wide lookup by id — reads a nested block by the same id content/edit can
|
||||||
|
;; update/delete (no section.sx dependency; uses the generic children descent).
|
||||||
|
(define doc-find-deep (fn (doc id) (ct-find-id (doc-blocks doc) id)))
|
||||||
|
|
||||||
|
(define
|
||||||
|
doc-has-deep?
|
||||||
|
(fn (doc id) (if (= (doc-find-deep doc id) nil) false true)))
|
||||||
|
|
||||||
;; ── structural edits (each returns a new document) ──
|
;; ── structural edits (each returns a new document) ──
|
||||||
(define doc-with-blocks (fn (doc blocks) (st-iv-set! doc "blocks" blocks)))
|
(define doc-with-blocks (fn (doc blocks) (st-iv-set! doc "blocks" blocks)))
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
;; content-on-sx — global find/replace across text-bearing blocks.
|
;; content-on-sx — global find/replace across every text-bearing field.
|
||||||
;;
|
;;
|
||||||
;; Replaces every occurrence of `from` with `to` in the text field of text /
|
;; Replaces every occurrence of `from` with `to` in the text-bearing fields of
|
||||||
;; heading / code / quote blocks, tree-wide (via the transform layer). For
|
;; a document, tree-wide (via the transform layer):
|
||||||
;; renaming a term throughout a document. Immutable; case-sensitive.
|
;; - the `text` of text / heading / code / quote / callout blocks
|
||||||
|
;; - the `alt` of image blocks
|
||||||
|
;; - each item of list blocks
|
||||||
|
;; - every header and cell of table blocks
|
||||||
|
;; This is exactly the set asText / stats / summary draw prose from, so a rename
|
||||||
|
;; via content/find-replace and a word count over asText stay consistent.
|
||||||
|
;; Immutable; case-sensitive.
|
||||||
;;
|
;;
|
||||||
;; Requires (loaded by harness): block.sx, transform.sx (content/map-blocks).
|
;; Requires (loaded by harness): block.sx, transform.sx (content/map-blocks),
|
||||||
|
;; table.sx (CtTable ivars).
|
||||||
|
|
||||||
(define
|
(define
|
||||||
fr-in?
|
fr-in?
|
||||||
@@ -15,17 +22,54 @@
|
|||||||
((= (first xs) x) true)
|
((= (first xs) x) true)
|
||||||
(else (fr-in? x (rest xs))))))
|
(else (fr-in? x (rest xs))))))
|
||||||
|
|
||||||
|
(define fr-rep (fn (s from to) (replace (str s) from to)))
|
||||||
|
|
||||||
|
;; Blocks whose prose content find/replace rewrites (matches asText's set).
|
||||||
(define
|
(define
|
||||||
fr-has-text?
|
fr-has-text?
|
||||||
(fn (b) (fr-in? (blk-type b) (list "text" "heading" "code" "quote"))))
|
(fn
|
||||||
|
(b)
|
||||||
|
(fr-in?
|
||||||
|
(blk-type b)
|
||||||
|
(list "text" "heading" "code" "quote" "callout" "image" "list" "table"))))
|
||||||
|
|
||||||
|
;; Per-type field rewrite. Each branch returns a new (copy-on-write) block.
|
||||||
|
(define
|
||||||
|
fr-rewrite
|
||||||
|
(fn
|
||||||
|
(b from to)
|
||||||
|
(let
|
||||||
|
((t (blk-type b)))
|
||||||
|
(cond
|
||||||
|
((= t "image")
|
||||||
|
(blk-set b "alt" (fr-rep (blk-get b "alt") from to)))
|
||||||
|
((= t "list")
|
||||||
|
(let
|
||||||
|
((items (blk-get b "items")))
|
||||||
|
(if
|
||||||
|
(list? items)
|
||||||
|
(blk-set b "items" (map (fn (it) (fr-rep it from to)) items))
|
||||||
|
b)))
|
||||||
|
((= t "table")
|
||||||
|
(let
|
||||||
|
((hs (blk-get b "headers")) (rs (blk-get b "rows")))
|
||||||
|
(let
|
||||||
|
((b1 (if (list? hs) (blk-set b "headers" (map (fn (h) (fr-rep h from to)) hs)) b)))
|
||||||
|
(if
|
||||||
|
(list? rs)
|
||||||
|
(blk-set
|
||||||
|
b1
|
||||||
|
"rows"
|
||||||
|
(map
|
||||||
|
(fn
|
||||||
|
(r)
|
||||||
|
(if (list? r) (map (fn (c) (fr-rep c from to)) r) r))
|
||||||
|
rs))
|
||||||
|
b1))))
|
||||||
|
(else (blk-set b "text" (fr-rep (blk-get b "text") from to)))))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
content/find-replace
|
content/find-replace
|
||||||
(fn
|
(fn
|
||||||
(doc from to)
|
(doc from to)
|
||||||
(content/map-blocks
|
(content/map-blocks doc fr-has-text? (fn (b) (fr-rewrite b from to)))))
|
||||||
doc
|
|
||||||
fr-has-text?
|
|
||||||
(fn
|
|
||||||
(b)
|
|
||||||
(blk-set b "text" (replace (str (blk-get b "text")) from to))))))
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
;; content-on-sx — block query + table of contents.
|
;; content-on-sx — block query + table of contents.
|
||||||
;;
|
;;
|
||||||
;; Collect blocks across the whole tree (descending into sections) by predicate
|
;; Collect blocks across the whole tree (descending into sections) by predicate
|
||||||
;; or type, and derive a table of contents from headings. Tree detection is
|
;; or type, search them by prose, and derive a table of contents from headings.
|
||||||
;; inline (class + st-iv-get) so this needs no section.sx.
|
;; Tree detection is inline (class + st-iv-get) so this needs no section.sx.
|
||||||
;;
|
;;
|
||||||
;; Requires (loaded by harness): block.sx, doc.sx.
|
;; Requires (loaded by harness): block.sx, doc.sx, text.sx (asText for search).
|
||||||
|
|
||||||
(define
|
(define
|
||||||
qry-section?
|
qry-section?
|
||||||
@@ -45,6 +45,30 @@
|
|||||||
content/select-ids
|
content/select-ids
|
||||||
(fn (doc pred) (map (fn (b) (blk-id b)) (content/select doc pred))))
|
(fn (doc pred) (map (fn (b) (blk-id b)) (content/select doc pred))))
|
||||||
|
|
||||||
|
;; Blocks (tree-wide, excluding section containers) whose own prose contains
|
||||||
|
;; `term`. "Prose" is (asText b), so search covers exactly what every block
|
||||||
|
;; exposes as text — text/heading/code/quote/callout text, image alt, list
|
||||||
|
;; items, table headers+cells — with no separate field list to drift from
|
||||||
|
;; asText / find-replace / stats. Case-sensitive substring match.
|
||||||
|
(define
|
||||||
|
content/search-text
|
||||||
|
(fn
|
||||||
|
(doc term)
|
||||||
|
(content/select
|
||||||
|
doc
|
||||||
|
(fn
|
||||||
|
(b)
|
||||||
|
(and
|
||||||
|
(not (qry-section? b))
|
||||||
|
(>= (index-of (asText b) term) 0))))))
|
||||||
|
|
||||||
|
;; Same search, returning matching block ids in document order.
|
||||||
|
(define
|
||||||
|
content/search-text-ids
|
||||||
|
(fn
|
||||||
|
(doc term)
|
||||||
|
(map (fn (b) (blk-id b)) (content/search-text doc term))))
|
||||||
|
|
||||||
;; table of contents: {:id :level :text} for every heading, in document order.
|
;; table of contents: {:id :level :text} for every heading, in document order.
|
||||||
(define
|
(define
|
||||||
content/headings
|
content/headings
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"block": {"pass": 38, "fail": 0},
|
"block": {"pass": 38, "fail": 0},
|
||||||
"doc": {"pass": 40, "fail": 0},
|
"doc": {"pass": 40, "fail": 0},
|
||||||
"render": {"pass": 42, "fail": 0},
|
"render": {"pass": 42, "fail": 0},
|
||||||
"api": {"pass": 26, "fail": 0},
|
"api": {"pass": 32, "fail": 0},
|
||||||
"meta": {"pass": 27, "fail": 0},
|
"meta": {"pass": 27, "fail": 0},
|
||||||
"page": {"pass": 7, "fail": 0},
|
"page": {"pass": 7, "fail": 0},
|
||||||
"page-full": {"pass": 4, "fail": 0},
|
"page-full": {"pass": 4, "fail": 0},
|
||||||
@@ -14,14 +14,14 @@
|
|||||||
"tree-edit": {"pass": 17, "fail": 0},
|
"tree-edit": {"pass": 17, "fail": 0},
|
||||||
"move": {"pass": 11, "fail": 0},
|
"move": {"pass": 11, "fail": 0},
|
||||||
"clone": {"pass": 10, "fail": 0},
|
"clone": {"pass": 10, "fail": 0},
|
||||||
"query": {"pass": 13, "fail": 0},
|
"query": {"pass": 20, "fail": 0},
|
||||||
"toc": {"pass": 8, "fail": 0},
|
"toc": {"pass": 8, "fail": 0},
|
||||||
"anchor": {"pass": 6, "fail": 0},
|
"anchor": {"pass": 6, "fail": 0},
|
||||||
"outline": {"pass": 14, "fail": 0},
|
"outline": {"pass": 14, "fail": 0},
|
||||||
"flatten": {"pass": 10, "fail": 0},
|
"flatten": {"pass": 10, "fail": 0},
|
||||||
"transform": {"pass": 12, "fail": 0},
|
"transform": {"pass": 12, "fail": 0},
|
||||||
"normalize": {"pass": 11, "fail": 0},
|
"normalize": {"pass": 11, "fail": 0},
|
||||||
"find-replace": {"pass": 10, "fail": 0},
|
"find-replace": {"pass": 16, "fail": 0},
|
||||||
"stats": {"pass": 17, "fail": 0},
|
"stats": {"pass": 17, "fail": 0},
|
||||||
"summary": {"pass": 14, "fail": 0},
|
"summary": {"pass": 14, "fail": 0},
|
||||||
"index": {"pass": 13, "fail": 0},
|
"index": {"pass": 13, "fail": 0},
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"data": {"pass": 25, "fail": 0},
|
"data": {"pass": 25, "fail": 0},
|
||||||
"wire": {"pass": 11, "fail": 0},
|
"wire": {"pass": 11, "fail": 0},
|
||||||
"validate": {"pass": 23, "fail": 0},
|
"validate": {"pass": 23, "fail": 0},
|
||||||
"store": {"pass": 33, "fail": 0},
|
"store": {"pass": 46, "fail": 0},
|
||||||
"snapshot": {"pass": 20, "fail": 0},
|
"snapshot": {"pass": 20, "fail": 0},
|
||||||
"crdt": {"pass": 34, "fail": 0},
|
"crdt": {"pass": 34, "fail": 0},
|
||||||
"crdt-tree": {"pass": 21, "fail": 0},
|
"crdt-tree": {"pass": 21, "fail": 0},
|
||||||
@@ -42,7 +42,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": 746,
|
"total_pass": 778,
|
||||||
"total_fail": 0,
|
"total_fail": 0,
|
||||||
"total": 746
|
"total": 778
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ _Generated by `lib/content/conformance.sh`_
|
|||||||
| block | 38 | 0 | 38 |
|
| block | 38 | 0 | 38 |
|
||||||
| doc | 40 | 0 | 40 |
|
| doc | 40 | 0 | 40 |
|
||||||
| render | 42 | 0 | 42 |
|
| render | 42 | 0 | 42 |
|
||||||
| api | 26 | 0 | 26 |
|
| api | 32 | 0 | 32 |
|
||||||
| meta | 27 | 0 | 27 |
|
| meta | 27 | 0 | 27 |
|
||||||
| page | 7 | 0 | 7 |
|
| page | 7 | 0 | 7 |
|
||||||
| page-full | 4 | 0 | 4 |
|
| page-full | 4 | 0 | 4 |
|
||||||
@@ -18,14 +18,14 @@ _Generated by `lib/content/conformance.sh`_
|
|||||||
| tree-edit | 17 | 0 | 17 |
|
| tree-edit | 17 | 0 | 17 |
|
||||||
| move | 11 | 0 | 11 |
|
| move | 11 | 0 | 11 |
|
||||||
| clone | 10 | 0 | 10 |
|
| clone | 10 | 0 | 10 |
|
||||||
| query | 13 | 0 | 13 |
|
| query | 20 | 0 | 20 |
|
||||||
| toc | 8 | 0 | 8 |
|
| toc | 8 | 0 | 8 |
|
||||||
| anchor | 6 | 0 | 6 |
|
| anchor | 6 | 0 | 6 |
|
||||||
| outline | 14 | 0 | 14 |
|
| outline | 14 | 0 | 14 |
|
||||||
| flatten | 10 | 0 | 10 |
|
| flatten | 10 | 0 | 10 |
|
||||||
| transform | 12 | 0 | 12 |
|
| transform | 12 | 0 | 12 |
|
||||||
| normalize | 11 | 0 | 11 |
|
| normalize | 11 | 0 | 11 |
|
||||||
| find-replace | 10 | 0 | 10 |
|
| find-replace | 16 | 0 | 16 |
|
||||||
| stats | 17 | 0 | 17 |
|
| stats | 17 | 0 | 17 |
|
||||||
| summary | 14 | 0 | 14 |
|
| summary | 14 | 0 | 14 |
|
||||||
| index | 13 | 0 | 13 |
|
| index | 13 | 0 | 13 |
|
||||||
@@ -35,7 +35,7 @@ _Generated by `lib/content/conformance.sh`_
|
|||||||
| data | 25 | 0 | 25 |
|
| data | 25 | 0 | 25 |
|
||||||
| wire | 11 | 0 | 11 |
|
| wire | 11 | 0 | 11 |
|
||||||
| validate | 23 | 0 | 23 |
|
| validate | 23 | 0 | 23 |
|
||||||
| store | 33 | 0 | 33 |
|
| store | 46 | 0 | 46 |
|
||||||
| snapshot | 20 | 0 | 20 |
|
| snapshot | 20 | 0 | 20 |
|
||||||
| crdt | 34 | 0 | 34 |
|
| crdt | 34 | 0 | 34 |
|
||||||
| crdt-tree | 21 | 0 | 21 |
|
| crdt-tree | 21 | 0 | 21 |
|
||||||
@@ -45,4 +45,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** | **746** | **0** | **746** |
|
| **Total** | **778** | **0** | **778** |
|
||||||
|
|||||||
@@ -5,9 +5,10 @@
|
|||||||
;; replay of its op stream up to a sequence number; the materialised doc is a
|
;; replay of its op stream up to a sequence number; the materialised doc is a
|
||||||
;; cache, never primary state.
|
;; cache, never primary state.
|
||||||
;;
|
;;
|
||||||
;; Requires (loaded by the harness): block.sx, doc.sx, and persist
|
;; Requires (loaded by the harness): block.sx, doc.sx, section.sx (doc-deep-find
|
||||||
;; (event/backend/log/kv/api). The persist backend `b` is opened by the caller
|
;; + doc-tree-ids, for the tree-wide diff), plus persist (event/backend/log/kv/
|
||||||
;; via (persist/open) and injected — content knows nothing about which backend.
|
;; api). The persist backend `b` is opened by the caller via (persist/open) and
|
||||||
|
;; injected — content knows nothing about which backend.
|
||||||
|
|
||||||
(define content/-stream (fn (doc-id) (str "content:" doc-id)))
|
(define content/-stream (fn (doc-id) (str "content:" doc-id)))
|
||||||
|
|
||||||
@@ -69,11 +70,18 @@
|
|||||||
(fn (b doc-id) (map (fn (ev) {:type (persist/event-type ev) :at (persist/event-at ev) :seq (persist/event-seq ev)}) (content/log b doc-id))))
|
(fn (b doc-id) (map (fn (ev) {:type (persist/event-type ev) :at (persist/event-at ev) :seq (persist/event-seq ev)}) (content/log b doc-id))))
|
||||||
|
|
||||||
;; ── diff between two materialised document versions ──
|
;; ── diff between two materialised document versions ──
|
||||||
;; Returns {:added (ids) :removed (ids) :changed (ids)} where changed = ids
|
;; Tree-wide: ids are enumerated across the whole block tree (descending into
|
||||||
;; present in both whose block content differs.
|
;; sections), so nested-block adds/removes/changes are detected, not just
|
||||||
(define
|
;; top-level ones. Returns {:added :removed :changed} (lists of ids):
|
||||||
content/-missing?
|
;; :added — ids present (anywhere) in `new` but not in `old`
|
||||||
(fn (doc id) (= (ct-index-of (doc-blocks doc) id) -1)))
|
;; :removed — ids present (anywhere) in `old` but not in `new`
|
||||||
|
;; :changed — content blocks present in both whose block value differs
|
||||||
|
;; Section containers never appear in :changed (they hold no own content — a
|
||||||
|
;; child change surfaces as that child's own entry); a whole section appearing
|
||||||
|
;; or disappearing shows up in :added / :removed by its id.
|
||||||
|
(define content/-all-ids (fn (doc) (doc-tree-ids doc)))
|
||||||
|
|
||||||
|
(define content/-missing? (fn (doc id) (= (doc-deep-find doc id) nil)))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
content/-changed
|
content/-changed
|
||||||
@@ -83,15 +91,16 @@
|
|||||||
(fn
|
(fn
|
||||||
(id)
|
(id)
|
||||||
(let
|
(let
|
||||||
((bo (doc-find old id)) (bn (doc-find new id)))
|
((bo (doc-deep-find old id)) (bn (doc-deep-find new id)))
|
||||||
(cond
|
(cond
|
||||||
((= bo nil) false)
|
((= bo nil) false)
|
||||||
((= bn nil) false)
|
((= bn nil) false)
|
||||||
|
((= (blk-type bo) "section") false)
|
||||||
((= bo bn) false)
|
((= bo bn) false)
|
||||||
(else true))))
|
(else true))))
|
||||||
(doc-ids old))))
|
(content/-all-ids old))))
|
||||||
|
|
||||||
(define content/diff (fn (old new) {:changed (content/-changed old new) :removed (filter (fn (id) (content/-missing? new id)) (doc-ids old)) :added (filter (fn (id) (content/-missing? old id)) (doc-ids new))}))
|
(define content/diff (fn (old new) {:changed (content/-changed old new) :removed (filter (fn (id) (content/-missing? new id)) (content/-all-ids old)) :added (filter (fn (id) (content/-missing? old id)) (content/-all-ids new))}))
|
||||||
|
|
||||||
;; convenience: diff two persisted versions by seq.
|
;; convenience: diff two persisted versions by seq.
|
||||||
(define
|
(define
|
||||||
|
|||||||
@@ -97,3 +97,37 @@
|
|||||||
"render original unchanged"
|
"render original unchanged"
|
||||||
(content/render d1 "html")
|
(content/render d1 "html")
|
||||||
"<h1>Hi</h1><p>World</p>")
|
"<h1>Hi</h1><p>World</p>")
|
||||||
|
|
||||||
|
;; ── facade find/has? are TREE-WIDE (reach into sections); find-top/has-top?
|
||||||
|
;; keep the top-level-only lookup. This makes the read-by-id surface consistent
|
||||||
|
;; with content/edit, whose update/delete are already tree-wide. ──
|
||||||
|
(content-bootstrap-section!)
|
||||||
|
(define
|
||||||
|
nd
|
||||||
|
(content/append
|
||||||
|
(content/empty "nested")
|
||||||
|
(mk-section
|
||||||
|
"sec"
|
||||||
|
(list (content/block "text" "inner" (list (list "text" "deep")))))))
|
||||||
|
(content-test
|
||||||
|
"find nested (deep)"
|
||||||
|
(blk-id (content/find nd "inner"))
|
||||||
|
"inner")
|
||||||
|
(content-test "has? nested (deep)" (content/has? nd "inner") true)
|
||||||
|
(content-test "find-top misses nested" (content/find-top nd "inner") nil)
|
||||||
|
(content-test "has-top? misses nested" (content/has-top? nd "inner") false)
|
||||||
|
(content-test
|
||||||
|
"find-top sees top-level"
|
||||||
|
(blk-id (content/find-top nd "sec"))
|
||||||
|
"sec")
|
||||||
|
;; a nested block updated by id via content/edit is now readable by id via
|
||||||
|
;; content/find (was impossible when find was top-level-only).
|
||||||
|
(content-test
|
||||||
|
"edit-then-find nested round-trip"
|
||||||
|
(str
|
||||||
|
(blk-send
|
||||||
|
(content/find
|
||||||
|
(content/edit nd (content/update "inner" "text" "edited"))
|
||||||
|
"inner")
|
||||||
|
"text"))
|
||||||
|
"edited")
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
;; Extension — global find/replace across text-bearing blocks.
|
;; Extension — global find/replace across every text-bearing field.
|
||||||
|
|
||||||
(st-bootstrap-classes!)
|
(st-bootstrap-classes!)
|
||||||
(content/bootstrap!)
|
(content/bootstrap!)
|
||||||
(content-bootstrap-section!)
|
(content-bootstrap-section!)
|
||||||
|
(content-bootstrap-callout!)
|
||||||
|
(content-bootstrap-table!)
|
||||||
|
|
||||||
(define
|
(define
|
||||||
d
|
d
|
||||||
@@ -30,11 +32,12 @@
|
|||||||
(str (blk-send (doc-deep-find r "n") "text"))
|
(str (blk-send (doc-deep-find r "n") "text"))
|
||||||
"nested Bar")
|
"nested Bar")
|
||||||
|
|
||||||
;; ── does NOT touch image alt/src (not a text field) ──
|
;; ── image alt IS a text field (asText ^ alt), so it is rewritten ──
|
||||||
(content-test
|
(content-test
|
||||||
"image alt untouched"
|
"image alt replaced"
|
||||||
(str (blk-send (doc-deep-find r "img") "alt"))
|
(str (blk-send (doc-deep-find r "img") "alt"))
|
||||||
"Foo alt")
|
"Bar alt")
|
||||||
|
;; ── but src is a URL, not prose, so it stays put ──
|
||||||
(content-test
|
(content-test
|
||||||
"image src untouched"
|
"image src untouched"
|
||||||
(str (blk-send (doc-deep-find r "img") "src"))
|
(str (blk-send (doc-deep-find r "img") "src"))
|
||||||
@@ -76,6 +79,68 @@
|
|||||||
(str (blk-send (doc-find r2 "q") "text"))
|
(str (blk-send (doc-find r2 "q") "text"))
|
||||||
"new saying")
|
"new saying")
|
||||||
|
|
||||||
|
;; ── callout text is covered (consistency with asText/stats/summary) ──
|
||||||
|
(content-test
|
||||||
|
"replace callout text"
|
||||||
|
(str
|
||||||
|
(blk-send
|
||||||
|
(doc-find
|
||||||
|
(content/find-replace
|
||||||
|
(doc-append (doc-empty "d") (mk-callout "co" "note" "Foo here"))
|
||||||
|
"Foo"
|
||||||
|
"Bar")
|
||||||
|
"co")
|
||||||
|
"text"))
|
||||||
|
"Bar here")
|
||||||
|
(content-test
|
||||||
|
"callout kind untouched by text replace"
|
||||||
|
(str
|
||||||
|
(blk-send
|
||||||
|
(doc-find
|
||||||
|
(content/find-replace
|
||||||
|
(doc-append (doc-empty "d") (mk-callout "co" "note" "x"))
|
||||||
|
"note"
|
||||||
|
"X")
|
||||||
|
"co")
|
||||||
|
"kind"))
|
||||||
|
"note")
|
||||||
|
|
||||||
|
;; ── list items are rewritten (asText folds items) ──
|
||||||
|
(define
|
||||||
|
rl
|
||||||
|
(content/find-replace
|
||||||
|
(doc-append
|
||||||
|
(doc-empty "d")
|
||||||
|
(mk-list "l" false (list "Foo one" "two Foo")))
|
||||||
|
"Foo"
|
||||||
|
"Bar"))
|
||||||
|
(content-test
|
||||||
|
"replace first list item"
|
||||||
|
(str (first (blk-send (doc-find rl "l") "items")))
|
||||||
|
"Bar one")
|
||||||
|
(content-test
|
||||||
|
"replace second list item"
|
||||||
|
(str (first (rest (blk-send (doc-find rl "l") "items"))))
|
||||||
|
"two Bar")
|
||||||
|
|
||||||
|
;; ── table headers + cells are rewritten (asText folds rows) ──
|
||||||
|
(define
|
||||||
|
rt
|
||||||
|
(content/find-replace
|
||||||
|
(doc-append
|
||||||
|
(doc-empty "d")
|
||||||
|
(mk-table "t" (list "Foo head") (list (list "a Foo" "b"))))
|
||||||
|
"Foo"
|
||||||
|
"Bar"))
|
||||||
|
(content-test
|
||||||
|
"replace table header"
|
||||||
|
(str (first (table-headers (doc-find rt "t"))))
|
||||||
|
"Bar head")
|
||||||
|
(content-test
|
||||||
|
"replace table cell"
|
||||||
|
(str (first (first (table-rows (doc-find rt "t")))))
|
||||||
|
"a Bar")
|
||||||
|
|
||||||
;; ── no match → unchanged render ──
|
;; ── no match → unchanged render ──
|
||||||
(content-test
|
(content-test
|
||||||
"no match"
|
"no match"
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
;; Extension — block query + table of contents.
|
;; Extension — block query + table of contents + prose search.
|
||||||
|
|
||||||
(st-bootstrap-classes!)
|
(st-bootstrap-classes!)
|
||||||
(content/bootstrap!)
|
(content/bootstrap!)
|
||||||
|
(content-bootstrap-text!)
|
||||||
(content-bootstrap-section!)
|
(content-bootstrap-section!)
|
||||||
|
(content-bootstrap-table!)
|
||||||
|
(content-bootstrap-callout!)
|
||||||
|
|
||||||
(define
|
(define
|
||||||
d
|
d
|
||||||
@@ -87,3 +90,49 @@
|
|||||||
"deep toc level"
|
"deep toc level"
|
||||||
(get (first (content/headings deep)) :level)
|
(get (first (content/headings deep)) :level)
|
||||||
3)
|
3)
|
||||||
|
|
||||||
|
;; ── prose search (content/search-text) ──
|
||||||
|
;; "cat" appears in text, image alt, a list item, a table cell, and a callout
|
||||||
|
;; — every text-bearing field — so search must find all five via asText.
|
||||||
|
(define
|
||||||
|
sd
|
||||||
|
(doc-append
|
||||||
|
(doc-append
|
||||||
|
(doc-append
|
||||||
|
(doc-append
|
||||||
|
(doc-append
|
||||||
|
(doc-empty "sd")
|
||||||
|
(mk-heading "sh" 1 "Welcome aboard"))
|
||||||
|
(mk-text "st" "the cat sat"))
|
||||||
|
(mk-image "si" "/x.png" "a cat photo"))
|
||||||
|
(mk-list "sl" false (list "first cat" "second dog")))
|
||||||
|
(mk-section
|
||||||
|
"sec"
|
||||||
|
(list
|
||||||
|
(mk-table "stb" (list "Animal") (list (list "cat") (list "fish")))
|
||||||
|
(mk-callout "sc" "note" "beware of cat")))))
|
||||||
|
|
||||||
|
(content-test
|
||||||
|
"search across every text-bearing field"
|
||||||
|
(content/search-text-ids sd "cat")
|
||||||
|
(list "st" "si" "sl" "stb" "sc"))
|
||||||
|
(content-test "search count" (len (content/search-text sd "cat")) 5)
|
||||||
|
(content-test
|
||||||
|
"search heading text"
|
||||||
|
(content/search-text-ids sd "Welcome")
|
||||||
|
(list "sh"))
|
||||||
|
(content-test
|
||||||
|
"search list item only"
|
||||||
|
(content/search-text-ids sd "dog")
|
||||||
|
(list "sl"))
|
||||||
|
(content-test "search no match" (content/search-text-ids sd "zzz") (list))
|
||||||
|
;; section containers are excluded — a term living only inside a section's
|
||||||
|
;; children returns the child, never the section wrapper.
|
||||||
|
(content-test
|
||||||
|
"search excludes section wrapper"
|
||||||
|
(content/search-text-ids sd "fish")
|
||||||
|
(list "stb"))
|
||||||
|
(content-test
|
||||||
|
"search returns block objects"
|
||||||
|
(blk-id (first (content/search-text sd "Welcome")))
|
||||||
|
"sh")
|
||||||
|
|||||||
@@ -151,3 +151,58 @@
|
|||||||
"op-log media type"
|
"op-log media type"
|
||||||
(blk-type (doc-find (content/head B3 "rich") "v"))
|
(blk-type (doc-find (content/head B3 "rich") "v"))
|
||||||
"media")
|
"media")
|
||||||
|
|
||||||
|
;; ── op-log update/delete reach NESTED blocks (tree-wide by id) ──
|
||||||
|
(content-bootstrap-section!)
|
||||||
|
(define B4 (persist/open))
|
||||||
|
(content/commit!
|
||||||
|
B4
|
||||||
|
"nest"
|
||||||
|
(op-insert (mk-section "sec" (list (mk-text "n" "orig"))) nil)
|
||||||
|
1)
|
||||||
|
(content/commit! B4 "nest" (op-update "n" "text" "edited") 2)
|
||||||
|
(content-test
|
||||||
|
"op-log nested update"
|
||||||
|
(str (blk-send (doc-deep-find (content/head B4 "nest") "n") "text"))
|
||||||
|
"edited")
|
||||||
|
(content-test
|
||||||
|
"op-log nested update tree intact"
|
||||||
|
(doc-tree-ids (content/head B4 "nest"))
|
||||||
|
(list "sec" "n"))
|
||||||
|
(content/commit! B4 "nest" (op-delete "n") 3)
|
||||||
|
(content-test
|
||||||
|
"op-log nested delete"
|
||||||
|
(doc-tree-ids (content/head B4 "nest"))
|
||||||
|
(list "sec"))
|
||||||
|
(content-test
|
||||||
|
"op-log nested delete via content/at seq2"
|
||||||
|
(doc-tree-ids (content/at B4 "nest" 2))
|
||||||
|
(list "sec" "n"))
|
||||||
|
|
||||||
|
;; ── diff is TREE-WIDE: nested-block add/change/remove are detected, and
|
||||||
|
;; section containers never appear in :changed (a top-level-only diff would miss
|
||||||
|
;; "n" entirely and instead flag the section). ──
|
||||||
|
(define dn01 (content/diff-versions B4 "nest" 0 1))
|
||||||
|
(content-test
|
||||||
|
"diff nested added (section + child)"
|
||||||
|
(get dn01 :added)
|
||||||
|
(list "sec" "n"))
|
||||||
|
(content-test "diff nested added removed empty" (get dn01 :removed) (list))
|
||||||
|
(content-test "diff nested added changed empty" (get dn01 :changed) (list))
|
||||||
|
|
||||||
|
(define dn12 (content/diff-versions B4 "nest" 1 2))
|
||||||
|
(content-test
|
||||||
|
"diff nested changed child only"
|
||||||
|
(get dn12 :changed)
|
||||||
|
(list "n"))
|
||||||
|
(content-test "diff nested changed no add" (get dn12 :added) (list))
|
||||||
|
(content-test "diff nested changed no remove" (get dn12 :removed) (list))
|
||||||
|
|
||||||
|
(define dn23 (content/diff-versions B4 "nest" 2 3))
|
||||||
|
(content-test "diff nested removed child" (get dn23 :removed) (list "n"))
|
||||||
|
(content-test "diff nested removed no change" (get dn23 :changed) (list))
|
||||||
|
|
||||||
|
(content-test
|
||||||
|
"diff nested no-op"
|
||||||
|
(get (content/diff-versions B4 "nest" 1 1) :changed)
|
||||||
|
(list))
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ injected adapter, not core.
|
|||||||
|
|
||||||
## Status (rolling)
|
## Status (rolling)
|
||||||
|
|
||||||
`bash lib/content/conformance.sh` → **746/746** (Phases 1–4 COMPLETE + ~34 extensions, hardened: HTML/SX escaping, Markdown render + import/export incl. tables & frontmatter (full round-trip), CvRDT flat + nested-tree + durable 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)
|
`bash lib/content/conformance.sh` → **778/778** (Phases 1–4 COMPLETE + ~34 extensions, hardened: HTML/SX escaping, Markdown render + import/export incl. tables & frontmatter (full round-trip), CvRDT flat + nested-tree + durable 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
|
## Ground rules
|
||||||
|
|
||||||
@@ -113,6 +113,66 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─
|
|||||||
|
|
||||||
## Progress log
|
## Progress log
|
||||||
|
|
||||||
|
- 2026-06-07 — Hardening (tree-wide audit): the public facade `content/find` /
|
||||||
|
`content/has?` were top-level-only (`doc-find`/`doc-has?`), so you could
|
||||||
|
`content/edit` an update/delete to a nested block by id (those ops are
|
||||||
|
tree-wide) but couldn't read that same block back by id through the facade — a
|
||||||
|
concrete read/write asymmetry. Added a generic `ct-find-id` to doc.sx (descends
|
||||||
|
into any `children` list, mirroring ct-replace-id/ct-remove-id, no section.sx
|
||||||
|
dependency) plus `doc-find-deep`/`doc-has-deep?`; `content/find`/`content/has?`
|
||||||
|
now point at them. Kept `content/find-top`/`content/has-top?` for the
|
||||||
|
top-level-only lookup. Audited all `doc-find`/`doc-ids`/`ct-index-of` callers:
|
||||||
|
the remaining ones are insert/move (positional, top-level by design) — no other
|
||||||
|
seams. +6 api tests (nested deep find/has, top variants miss nested,
|
||||||
|
edit-then-find round-trip). 778/778.
|
||||||
|
|
||||||
|
- 2026-06-07 — Hardening: `content/diff` (and `content/diff-versions`) are now
|
||||||
|
TREE-WIDE. They enumerated ids via `doc-ids`/`doc-find` (top-level only), so a
|
||||||
|
diff between two versions of a document containing sections silently missed
|
||||||
|
every nested-block add/remove/change — the same class of seam as the by-id
|
||||||
|
op-log bug. Now ids come from `doc-tree-ids` and lookups from `doc-deep-find`,
|
||||||
|
so nested changes surface precisely. Section containers are excluded from
|
||||||
|
`:changed` (they hold no own content; a child change reports as that child),
|
||||||
|
while whole-section add/remove still shows in `:added`/`:removed`. Flat-doc
|
||||||
|
diffs are unchanged (deep == top-level with no sections). +9 store tests
|
||||||
|
(nested add = section+child, nested change = child only, nested remove,
|
||||||
|
no-op). 772/772.
|
||||||
|
|
||||||
|
- 2026-06-07 — Feature: in-document prose search. `content/search-text` (and
|
||||||
|
`content/search-text-ids`) return every content block, tree-wide, whose
|
||||||
|
`(asText b)` contains a term — so search spans text/heading/code/quote/callout
|
||||||
|
text, image alt, list items and table cells **by construction**: it reuses the
|
||||||
|
one canonical "prose of a block" projection (asText) rather than re-listing
|
||||||
|
fields, so it can't drift from stats/find-replace. Section containers are
|
||||||
|
excluded (a term living only in a section's children returns the child, not the
|
||||||
|
wrapper). +7 query tests (cross-field match, count, single-field, no-match,
|
||||||
|
section exclusion, object return). 763/763.
|
||||||
|
|
||||||
|
- 2026-06-07 — Consistency: `find-replace` now rewrites **every** text-bearing
|
||||||
|
field, not just `text`. New `fr-rewrite` dispatches per block type — `alt` of
|
||||||
|
image blocks, each item of list blocks, and every header/cell of table blocks
|
||||||
|
now get rewritten alongside text/heading/code/quote/callout. This closes a real
|
||||||
|
seam: `asText`/stats/word-count already fold image alt, list items, and table
|
||||||
|
cells into a document's prose, so a `content/find-replace` rename that skipped
|
||||||
|
them was inconsistent (a renamed term would still show up in word counts and
|
||||||
|
exports). Flipped the two `image alt untouched` tests to `image alt replaced`;
|
||||||
|
+4 tests (list items ×2, table header + cell). find-replace 16/16, 756/756.
|
||||||
|
|
||||||
|
- 2026-06-07 — Consistency: `find-replace` now covers `callout` text. `fr-has-text?`
|
||||||
|
(find-replace.sx) added `callout` to its text-bearing block kinds, matching
|
||||||
|
`asText`/stats/summary which already treat callout bodies as prose. Previously a
|
||||||
|
`content/find-replace` over a doc containing callouts silently skipped them. +2
|
||||||
|
find-replace tests (replace callout text; callout kind untouched by text replace).
|
||||||
|
752/752 (41 suites).
|
||||||
|
|
||||||
|
- 2026-06-07 — Hardening: fixed a real layer seam (surfaced in the architecture
|
||||||
|
review) — by-id ops (update/delete) now act TREE-WIDE. `ct-replace-id` /
|
||||||
|
`ct-remove-id` (doc.sx) descend into any block carrying a `children` list, so
|
||||||
|
the persist op-log and `content/edit` correctly reach blocks nested in
|
||||||
|
sections (previously a silent no-op). `doc-move` stays top-level (guarded by
|
||||||
|
doc-find); insert/move remain positional. Inline section detection (no
|
||||||
|
section.sx dep). +4 store regression tests (nested update/delete via op-log +
|
||||||
|
replay-to-seq). Full gate over foundational doc.sx: 750/750.
|
||||||
- 2026-06-07 — Hardening: audit confirmed the persist op-log (store.sx) carries
|
- 2026-06-07 — Hardening: audit confirmed the persist op-log (store.sx) carries
|
||||||
every block type through commit → replay (op-insert carries the block
|
every block type through commit → replay (op-insert carries the block
|
||||||
instance; updates apply by id). Locked with +4 store tests (callout/media
|
instance; updates apply by id). Locked with +4 store tests (callout/media
|
||||||
|
|||||||
Reference in New Issue
Block a user