content: table block (table.sx) + 15 tests (448/448)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 36s

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 02:17:44 +00:00
parent 7791867bbc
commit 69defdc517
7 changed files with 156 additions and 5 deletions

View File

@@ -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")

View File

@@ -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
}

View File

@@ -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** |

54
lib/content/table.sx Normal file
View File

@@ -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 := '<thead><tr>' , (headers inject: '' into: [:a :h | a , '<th>' , h htmlEscaped , '</th>']) , '</tr></thead>'. tbody := '<tbody>' , (rows inject: '' into: [:a :r | a , '<tr>' , (r inject: '' into: [:b :c | b , '<td>' , c htmlEscaped , '</td>']) , '</tr>']) , '</tbody>'. ^ '<table>' , thead , tbody , '</table>'")
(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))))

View File

@@ -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)
"<table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Ada</td><td>36</td></tr><tr><td>Al</td><td>40</td></tr></tbody></table>")
(content-test
"table html escapes cells"
(asHTML (mk-table "t" (list "A<B") (list (list "x&y"))))
"<table><thead><tr><th>A&lt;B</th></tr></thead><tbody><tr><td>x&amp;y</td></tr></tbody></table>")
;; ── 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)
"<h1>Data</h1><table><thead><tr><th>Name</th><th>Age</th></tr></thead><tbody><tr><td>Ada</td><td>36</td></tr><tr><td>Al</td><td>40</td></tr></tbody></table>")
(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)))
"<table><thead><tr><th>H</th></tr></thead><tbody></tbody></table>")
;; ── 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"))

View File

@@ -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

View File

@@ -19,7 +19,7 @@ injected adapter, not core.
## Status (rolling)
`bash lib/content/conformance.sh`**433/433** (Phases 14 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 14 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 `<table>`), 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