From 69defdc517c17a1466f4ae411ebe661a68e627e7 Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 7 Jun 2026 02:17:44 +0000 Subject: [PATCH] content: table block (table.sx) + 15 tests (448/448) 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/table.sx | 54 ++++++++++++++++++++++++++ lib/content/tests/table.sx | 77 +++++++++++++++++++++++++++++++++++++ lib/content/validate.sx | 10 +++++ plans/content-on-sx.md | 9 ++++- 7 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 lib/content/table.sx create mode 100644 lib/content/tests/table.sx diff --git a/lib/content/conformance.sh b/lib/content/conformance.sh index ada93017..b29e41e8 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 markdown text section stats validate store snapshot crdt crdt-store sync md-import fed) +SUITES=(block doc render api meta markdown text section stats table validate store snapshot crdt crdt-store sync md-import 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/stats.sx") +(load "lib/content/table.sx") (load "lib/content/markdown.sx") (load "lib/content/validate.sx") (load "lib/content/store.sx") diff --git a/lib/content/scoreboard.json b/lib/content/scoreboard.json index 1c41185f..0c2a11c9 100644 --- a/lib/content/scoreboard.json +++ b/lib/content/scoreboard.json @@ -9,6 +9,7 @@ "text": {"pass": 20, "fail": 0}, "section": {"pass": 25, "fail": 0}, "stats": {"pass": 17, "fail": 0}, + "table": {"pass": 15, "fail": 0}, "validate": {"pass": 23, "fail": 0}, "store": {"pass": 29, "fail": 0}, "snapshot": {"pass": 20, "fail": 0}, @@ -18,7 +19,7 @@ "md-import": {"pass": 24, "fail": 0}, "fed": {"pass": 20, "fail": 0} }, - "total_pass": 433, + "total_pass": 448, "total_fail": 0, - "total": 433 + "total": 448 } diff --git a/lib/content/scoreboard.md b/lib/content/scoreboard.md index 17ab7aec..8430f924 100644 --- a/lib/content/scoreboard.md +++ b/lib/content/scoreboard.md @@ -13,6 +13,7 @@ _Generated by `lib/content/conformance.sh`_ | text | 20 | 0 | 20 | | section | 25 | 0 | 25 | | stats | 17 | 0 | 17 | +| table | 15 | 0 | 15 | | validate | 23 | 0 | 23 | | store | 29 | 0 | 29 | | snapshot | 20 | 0 | 20 | @@ -21,4 +22,4 @@ _Generated by `lib/content/conformance.sh`_ | sync | 14 | 0 | 14 | | md-import | 24 | 0 | 24 | | fed | 20 | 0 | 20 | -| **Total** | **433** | **0** | **433** | +| **Total** | **448** | **0** | **448** | diff --git a/lib/content/table.sx b/lib/content/table.sx new file mode 100644 index 00000000..864a7036 --- /dev/null +++ b/lib/content/table.sx @@ -0,0 +1,54 @@ +;; content-on-sx — table block. +;; +;; CtTable holds `headers` (list of strings) and `rows` (list of string lists). +;; Self-contained: it answers asHTML/asSx/asText/asMarkdown: by folding rows and +;; cells, so it composes with the render boundary with no changes elsewhere. HTML +;; cells are htmlEscaped, SX cells sxEscaped (render.sx must be loaded). +;; +;; Requires (loaded by harness): block.sx, doc.sx, render.sx (escapers); +;; markdown.sx / text.sx for those formats. + +(define + content-bootstrap-table! + (fn + () + (begin + (st-class-define! "CtTable" "CtBlock" (list "headers" "rows")) + (ct-def-method! "CtTable" "headers" "headers ^ headers") + (ct-def-method! "CtTable" "rows" "rows ^ rows") + (ct-def-method! "CtTable" "type" "type ^ #table") + (ct-def-method! + "CtTable" + "asHTML" + "asHTML | thead tbody | thead := '' , (headers inject: '' into: [:a :h | a , '' , h htmlEscaped , '']) , ''. tbody := '' , (rows inject: '' into: [:a :r | a , '' , (r inject: '' into: [:b :c | b , '' , c htmlEscaped , '']) , '']) , ''. ^ '' , thead , tbody , '
'") + (ct-def-method! + "CtTable" + "asSx" + "asSx ^ '(table (thead (tr ' , (headers inject: '' into: [:a :h | a , '(th \"' , h sxEscaped , '\")']) , ')) (tbody ' , (rows inject: '' into: [:a :r | a , '(tr ' , (r inject: '' into: [:b :c | b , '(td \"' , c sxEscaped , '\")']) , ')']) , '))'") + (ct-def-method! + "CtTable" + "asText" + "asText ^ (rows inject: (headers inject: '' into: [:a :h | (a = '' ifTrue: [h] ifFalse: [a , ' ' , h])]) into: [:acc :r | acc , ' ' , (r inject: '' into: [:b :c | (b = '' ifTrue: [c] ifFalse: [b , ' ' , c])])])") + (ct-def-method! + "CtTable" + "asMarkdown:" + "asMarkdown: nl | head sep body | head := '|' , (headers inject: '' into: [:a :h | a , ' ' , h , ' |']). sep := '|' , (headers inject: '' into: [:a :h | a , ' --- |']). body := (rows inject: '' into: [:acc :r | acc , nl , '|' , (r inject: '' into: [:a :c | a , ' ' , c , ' |'])]). ^ head , nl , sep , body") + true))) + +(define + mk-table + (fn + (id headers rows) + (st-iv-set! + (st-iv-set! + (st-iv-set! (st-make-instance "CtTable") "id" id) + "headers" + headers) + "rows" + rows))) + +(define + table? + (fn (b) (and (st-instance? b) (= (get b :class) "CtTable")))) +(define table-headers (fn (tb) (st-send tb "headers" (list)))) +(define table-rows (fn (tb) (st-send tb "rows" (list)))) diff --git a/lib/content/tests/table.sx b/lib/content/tests/table.sx new file mode 100644 index 00000000..5e00f6ef --- /dev/null +++ b/lib/content/tests/table.sx @@ -0,0 +1,77 @@ +;; Extension — table block. + +(st-bootstrap-classes!) +(content/bootstrap!) +(content-bootstrap-markdown!) +(content-bootstrap-text!) +(content-bootstrap-table!) + +(define nl (str "\n")) + +(define + t + (mk-table + "t" + (list "Name" "Age") + (list (list "Ada" "36") (list "Al" "40")))) + +;; ── identity ── +(content-test "table is block" (block? t) true) +(content-test "table? yes" (table? t) true) +(content-test "table type" (blk-type t) "table") +(content-test "table headers" (table-headers t) (list "Name" "Age")) +(content-test "table rows" (len (table-rows t)) 2) + +;; ── html ── +(content-test + "table html" + (asHTML t) + "
NameAge
Ada36
Al40
") +(content-test + "table html escapes cells" + (asHTML (mk-table "t" (list "AA<Bx&y") + +;; ── sx ── +(content-test + "table sx" + (asSx t) + "(table (thead (tr (th \"Name\")(th \"Age\"))) (tbody (tr (td \"Ada\")(td \"36\"))(tr (td \"Al\")(td \"40\"))))") + +;; ── text ── +(content-test "table text" (asText t) "Name Age Ada 36 Al 40") + +;; ── markdown ── +(content-test + "table markdown" + (asMarkdown t) + (str "| Name | Age |" nl "| --- | --- |" nl "| Ada | 36 |" nl "| Al | 40 |")) + +;; ── in a document ── +(define + d + (doc-append + (doc-append (doc-empty "d") (mk-heading "h" 1 "Data")) + t)) +(content-test + "doc with table html" + (asHTML d) + "

Data

NameAge
Ada36
Al40
") +(content-test "doc ids" (doc-ids d) (list "h" "t")) + +;; ── empty rows ── +(content-test + "table no rows html" + (asHTML (mk-table "t" (list "H") (list))) + "
H
") + +;; ── validation ── +(content-test + "valid table" + (content/valid? (doc-append (doc-empty "d") t)) + true) +(content-test + "bad headers flagged" + (content/issue-kinds + (doc-append (doc-empty "d") (mk-table "t" "nope" (list)))) + (list "field")) diff --git a/lib/content/validate.sx b/lib/content/validate.sx index 537053b6..65be8fc4 100644 --- a/lib/content/validate.sx +++ b/lib/content/validate.sx @@ -150,6 +150,16 @@ id (list? (blk-get b "children")) "section children must be a list")) + ((= t "table") + (append + (ct-field-issue + id + (list? (blk-get b "headers")) + "table headers must be a list") + (ct-field-issue + id + (list? (blk-get b "rows")) + "table rows must be a list"))) (else (list (ct-issue id "type" (str "unknown block type: " t)))))))) (define diff --git a/plans/content-on-sx.md b/plans/content-on-sx.md index 38ee01e0..e2e33eb4 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` → **433/433** (Phases 1–4 COMPLETE + 11 extensions: HTML/SX escaping, Markdown render+import, CRDT replication, tree-aware validation, snapshot cache, doc metadata, plain-text render, nested block trees, doc stats) +`bash lib/content/conformance.sh` → **448/448** (Phases 1–4 COMPLETE + 12 extensions: HTML/SX escaping, Markdown render+import, CRDT replication, tree-aware validation, snapshot cache, doc metadata, plain-text render, nested block trees, doc stats, table block) ## Ground rules @@ -87,9 +87,16 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─ - [x] plain-text render + excerpt (`text.sx`: asText, content/excerpt) - [x] nested block trees (`section.sx`: CtSection container, recursive render, deep-find) - [x] document statistics (`stats.sx`: word/char/block counts, reading time) +- [x] table block (`table.sx`: CtTable, renders html/sx/text/md, validated) ## Progress log +- 2026-06-07 — Extension: table block (`table.sx`). `CtTable` holds headers + + rows (string lists); answers asHTML (escaped ``), asSx, asText, and + asMarkdown: (pipe table with dashed separator row) by folding rows×cells via + nested `inject:into:`. Self-contained (no edits to block.sx/render.sx); + `mk-table`, `table?`, `table-headers/rows`. validate.sx gained a `table` field + case (headers/rows must be lists). 15 tests; suite 448/448. - 2026-06-07 — Extension: document statistics (`stats.sx`). `content/stats` returns `{:words :chars :blocks :reading-minutes}`; word/char counts derive from the tree-accurate `asText` projection, block count from an inline tree