From 2f626173d9936d6f8730ca001e254efefda55b6c Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 7 Jun 2026 12:05:11 +0000 Subject: [PATCH] content: find-replace rewrites all text-bearing fields (756/756) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fr-rewrite dispatches per block type so image alt, list items, and table headers/cells are renamed alongside text/heading/code/quote/callout — matching exactly the set asText/stats/word-count fold into prose. Prior find-replace skipped them, so a rename stayed visible in counts/exports. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/content/find-replace.sx | 67 +++++++++++++++++++++++++------ lib/content/scoreboard.json | 6 +-- lib/content/scoreboard.md | 4 +- lib/content/tests/find-replace.sx | 48 +++++++++++++++++++--- plans/content-on-sx.md | 12 +++++- 5 files changed, 113 insertions(+), 24 deletions(-) diff --git a/lib/content/find-replace.sx b/lib/content/find-replace.sx index 2b9b34f5..e6ee76b7 100644 --- a/lib/content/find-replace.sx +++ b/lib/content/find-replace.sx @@ -1,11 +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 / heading / code / quote / callout blocks, tree-wide (via the transform -;; layer). For renaming a term throughout a document. Immutable; case-sensitive. -;; (Same text-bearing set asText/stats/summary treat as content.) +;; Replaces every occurrence of `from` with `to` in the text-bearing fields of +;; a document, tree-wide (via the transform layer): +;; - 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 fr-in? @@ -16,19 +22,54 @@ ((= (first xs) x) true) (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 fr-has-text? (fn (b) - (fr-in? (blk-type b) (list "text" "heading" "code" "quote" "callout")))) + (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 content/find-replace (fn (doc from to) - (content/map-blocks - doc - fr-has-text? - (fn - (b) - (blk-set b "text" (replace (str (blk-get b "text")) from to)))))) + (content/map-blocks doc fr-has-text? (fn (b) (fr-rewrite b from to))))) diff --git a/lib/content/scoreboard.json b/lib/content/scoreboard.json index 1dbed8dc..cb2a09cf 100644 --- a/lib/content/scoreboard.json +++ b/lib/content/scoreboard.json @@ -21,7 +21,7 @@ "flatten": {"pass": 10, "fail": 0}, "transform": {"pass": 12, "fail": 0}, "normalize": {"pass": 11, "fail": 0}, - "find-replace": {"pass": 12, "fail": 0}, + "find-replace": {"pass": 16, "fail": 0}, "stats": {"pass": 17, "fail": 0}, "summary": {"pass": 14, "fail": 0}, "index": {"pass": 13, "fail": 0}, @@ -42,7 +42,7 @@ "md-doc": {"pass": 12, "fail": 0}, "fed": {"pass": 20, "fail": 0} }, - "total_pass": 752, + "total_pass": 756, "total_fail": 0, - "total": 752 + "total": 756 } diff --git a/lib/content/scoreboard.md b/lib/content/scoreboard.md index 09e808d9..14c2c9b7 100644 --- a/lib/content/scoreboard.md +++ b/lib/content/scoreboard.md @@ -25,7 +25,7 @@ _Generated by `lib/content/conformance.sh`_ | flatten | 10 | 0 | 10 | | transform | 12 | 0 | 12 | | normalize | 11 | 0 | 11 | -| find-replace | 12 | 0 | 12 | +| find-replace | 16 | 0 | 16 | | stats | 17 | 0 | 17 | | summary | 14 | 0 | 14 | | index | 13 | 0 | 13 | @@ -45,4 +45,4 @@ _Generated by `lib/content/conformance.sh`_ | md-import | 38 | 0 | 38 | | md-doc | 12 | 0 | 12 | | fed | 20 | 0 | 20 | -| **Total** | **752** | **0** | **752** | +| **Total** | **756** | **0** | **756** | diff --git a/lib/content/tests/find-replace.sx b/lib/content/tests/find-replace.sx index 742dc3c0..47e802eb 100644 --- a/lib/content/tests/find-replace.sx +++ b/lib/content/tests/find-replace.sx @@ -1,9 +1,10 @@ -;; Extension — global find/replace across text-bearing blocks. +;; Extension — global find/replace across every text-bearing field. (st-bootstrap-classes!) (content/bootstrap!) (content-bootstrap-section!) (content-bootstrap-callout!) +(content-bootstrap-table!) (define d @@ -31,11 +32,12 @@ (str (blk-send (doc-deep-find r "n") "text")) "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 - "image alt untouched" + "image alt replaced" (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 "image src untouched" (str (blk-send (doc-deep-find r "img") "src")) @@ -77,7 +79,7 @@ (str (blk-send (doc-find r2 "q") "text")) "new saying") -;; ── callout text is now covered (consistency with asText/stats/summary) ── +;; ── callout text is covered (consistency with asText/stats/summary) ── (content-test "replace callout text" (str @@ -103,6 +105,42 @@ "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 ── (content-test "no match" diff --git a/plans/content-on-sx.md b/plans/content-on-sx.md index 7bf39f68..d0de3f9a 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` → **752/752** (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` → **756/756** (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 @@ -113,6 +113,16 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─ ## Progress log +- 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