content: table block (table.sx) + 15 tests (448/448)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 36s
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:
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
54
lib/content/table.sx
Normal 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))))
|
||||
77
lib/content/tests/table.sx
Normal file
77
lib/content/tests/table.sx
Normal 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<B</th></tr></thead><tbody><tr><td>x&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"))
|
||||
@@ -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
|
||||
|
||||
@@ -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 `<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
|
||||
|
||||
Reference in New Issue
Block a user