From 7610da1d6d1b326cf4a98682e8931e69991f6cfb Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 7 Jun 2026 02:37:02 +0000 Subject: [PATCH] content: Markdown table import + 5 tests (round-trip, 460/460) Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/content/md-import.sx | 109 +++++++++++++++++++++++++++++++-- lib/content/scoreboard.json | 6 +- lib/content/scoreboard.md | 4 +- lib/content/tests/md-import.sx | 49 +++++++++++++++ plans/content-on-sx.md | 9 ++- 5 files changed, 166 insertions(+), 11 deletions(-) diff --git a/lib/content/md-import.sx b/lib/content/md-import.sx index 5abd885c..b7cb9016 100644 --- a/lib/content/md-import.sx +++ b/lib/content/md-import.sx @@ -3,11 +3,12 @@ ;; A line-based parser, the inverse of markdown.sx's asMarkdown. Confined to the ;; adapter boundary: the core knows nothing about Markdown. Handles ATX headings ;; (#..######), fenced code (```lang), blockquotes (> ), unordered (- / * ) and -;; ordered (1. ) lists, thematic breaks (--- / ***), and paragraphs (consecutive -;; plain lines joined with a space). Block ids are assigned sequentially b0,b1… +;; ordered (1. ) lists, thematic breaks (--- / ***), pipe tables (header row + +;; --- separator row + body rows), and paragraphs (consecutive plain lines joined +;; with a space). Block ids are assigned sequentially b0,b1… ;; -;; Requires (loaded by harness): block.sx, doc.sx (and markdown.sx for the -;; adapter's export side). +;; Requires (loaded by harness): block.sx, doc.sx, table.sx (mk-table); and +;; markdown.sx for the adapter's export side. (define md/-id (fn (i) (str "b" i))) (define md/-blank? (fn (s) (= s ""))) @@ -118,6 +119,70 @@ md/-drop-ul (fn (line) (substring line 2 (string-length line)))) +;; ── table detection ── +(define md/-pipe-row? (fn (line) (ct-starts-with? (trim line) "|"))) +(define md/-sep-char? (fn (ch) (ct-in? ch (list "-" ":" "|" " ")))) +(define + md/-all-sep? + (fn + (s i) + (if + (>= i (string-length s)) + true + (if + (md/-sep-char? (substring s i (+ i 1))) + (md/-all-sep? s (+ i 1)) + false)))) +(define + md/-has-dash? + (fn + (s i) + (if + (>= i (string-length s)) + false + (if + (= (substring s i (+ i 1)) "-") + true + (md/-has-dash? s (+ i 1)))))) +(define + md/-sep-row? + (fn + (line) + (and + (md/-pipe-row? line) + (md/-all-sep? (trim line) 0) + (md/-has-dash? line 0)))) +(define + md/-table-start? + (fn + (lines) + (and + (md/-pipe-row? (first lines)) + (> (len lines) 1) + (md/-sep-row? (nth lines 1))))) +(define + md/-strip-pipes + (fn + (s0) + (let + ((s (trim s0))) + (let + ((a (if (ct-starts-with? s "|") (substring s 1 (string-length s)) s))) + (if + (and + (> (string-length a) 0) + (= + (substring + a + (- (string-length a) 1) + (string-length a)) + "|")) + (substring a 0 (- (string-length a) 1)) + a))))) +(define + md/-cells + (fn (line) (map (fn (c) (trim c)) (split (md/-strip-pipes line) "|")))) + (define md/-plain? (fn @@ -168,6 +233,41 @@ (else (md/-code-collect (rest lines) lang (cons (first lines) body) i acc))))) +(define + md/-table-body + (fn + (lines headers rows i acc) + (if + (= (len lines) 0) + (md/-walk + lines + (+ i 1) + (cons (mk-table (md/-id i) headers (reverse rows)) acc)) + (let + ((line (first lines))) + (if + (md/-pipe-row? line) + (md/-table-body + (rest lines) + headers + (cons (md/-cells line) rows) + i + acc) + (md/-walk + lines + (+ i 1) + (cons (mk-table (md/-id i) headers (reverse rows)) acc))))))) +(define + md/-table + (fn + (lines i acc) + (md/-table-body + (rest (rest lines)) + (md/-cells (first lines)) + (list) + i + acc))) + (define md/-list-collect (fn @@ -256,6 +356,7 @@ (rest lines) (+ i 1) (cons (mk-divider (md/-id i)) acc))) + ((md/-table-start? lines) (md/-table lines i acc)) ((md/-ul? line) (md/-list-collect lines (list) i acc false)) ((md/-ol? line) (md/-list-collect lines (list) i acc true)) (else (md/-para-collect lines (list) i acc))))))) diff --git a/lib/content/scoreboard.json b/lib/content/scoreboard.json index 9419d345..c8c26bc5 100644 --- a/lib/content/scoreboard.json +++ b/lib/content/scoreboard.json @@ -17,10 +17,10 @@ "crdt": {"pass": 34, "fail": 0}, "crdt-store": {"pass": 14, "fail": 0}, "sync": {"pass": 14, "fail": 0}, - "md-import": {"pass": 24, "fail": 0}, + "md-import": {"pass": 29, "fail": 0}, "fed": {"pass": 20, "fail": 0} }, - "total_pass": 455, + "total_pass": 460, "total_fail": 0, - "total": 455 + "total": 460 } diff --git a/lib/content/scoreboard.md b/lib/content/scoreboard.md index ce7265bb..16e68dfb 100644 --- a/lib/content/scoreboard.md +++ b/lib/content/scoreboard.md @@ -21,6 +21,6 @@ _Generated by `lib/content/conformance.sh`_ | crdt | 34 | 0 | 34 | | crdt-store | 14 | 0 | 14 | | sync | 14 | 0 | 14 | -| md-import | 24 | 0 | 24 | +| md-import | 29 | 0 | 29 | | fed | 20 | 0 | 20 | -| **Total** | **455** | **0** | **455** | +| **Total** | **460** | **0** | **460** | diff --git a/lib/content/tests/md-import.sx b/lib/content/tests/md-import.sx index 24572d65..35f527c2 100644 --- a/lib/content/tests/md-import.sx +++ b/lib/content/tests/md-import.sx @@ -4,6 +4,7 @@ (st-bootstrap-classes!) (content/bootstrap!) (content-bootstrap-markdown!) +(content-bootstrap-table!) (define nl (str "\n")) @@ -81,6 +82,54 @@ (doc-ids (md/import (str nl nl) "d")) (list)) +;; ── pipe tables ── +(define + dt + (md/import + (str + "| Name | Age |" + nl + "| --- | --- |" + nl + "| Ada | 36 |" + nl + "| Al | 40 |") + "d")) +(content-test "table import type" (doc-types dt) (list "table")) +(content-test + "table headers" + (table-headers (doc-find dt "b0")) + (list "Name" "Age")) +(content-test + "table rows" + (table-rows (doc-find dt "b0")) + (list (list "Ada" "36") (list "Al" "40"))) +(content-test + "table round-trip" + (asMarkdown + (md/import (str "| A | B |" nl "| --- | --- |" nl "| 1 | 2 |") "d")) + (str "| A | B |" nl "| --- | --- |" nl "| 1 | 2 |")) +(define + dmix + (md/import + (str + "# Title" + nl + nl + "| H1 | H2 |" + nl + "| --- | --- |" + nl + "| a | b |" + nl + nl + "para") + "d")) +(content-test + "table mixed types" + (doc-types dmix) + (list "heading" "table" "text")) + ;; ── round-trip: import . export == identity (canonical markdown) ── (define src diff --git a/plans/content-on-sx.md b/plans/content-on-sx.md index f5da924d..dfb8773e 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` → **455/455** (Phases 1–4 COMPLETE + 13 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, HTML page wrapper) +`bash lib/content/conformance.sh` → **460/460** (Phases 1–4 COMPLETE + 13 extensions: HTML/SX escaping, Markdown render + import incl. tables, CRDT replication, tree-aware validation, snapshot cache, doc metadata, plain-text render, nested block trees, doc stats, table block, HTML page wrapper) ## Ground rules @@ -81,7 +81,7 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─ - [x] Markdown render mode (`asMarkdown:` / `content/render doc "md"`) - [x] durable CRDT replication (`crdt-store.sx`: ops on persist, replay + converge) - [x] document validation (`validate.sx`: ids, per-type fields, duplicate ids; tree-aware — descends into sections, tree-wide dup ids, section field check) -- [x] Markdown import adapter (`md-import.sx`: text → blocks, round-trips export) +- [x] Markdown import adapter (`md-import.sx`: text → blocks, round-trips export; incl. pipe tables) - [x] snapshot cache over replay (`snapshot.sx`: cache-not-primary, transparent) - [x] document metadata (`meta.sx`: title/slug/tags + Ghost title plumbing) - [x] plain-text render + excerpt (`text.sx`: asText, content/excerpt) @@ -92,6 +92,11 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─ ## Progress log +- 2026-06-07 — Extension: Markdown table import. `md-import.sx` now recognizes a + `| … |` header row followed by a `| --- |` separator and parses a `CtTable` + (cells trimmed, mixed with other blocks via blank-line separation), completing + the Markdown table round-trip (import∘export == identity). +5 tests; suite + 460/460. - 2026-06-07 — Extension: HTML page wrapper (`page.sx`). `content/page` composes metadata + render into a minimal valid HTML5 document — escaped `` from doc metadata (falling back to id) and the rendered blocks as the body.