From d99457959820f8b39e7927acbacc3c129bdc726c Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 7 Jun 2026 02:49:52 +0000 Subject: [PATCH] content: Markdown doc export w/ frontmatter (md-doc.sx) + 12 tests (481/481) Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/content/conformance.sh | 3 +- lib/content/md-doc.sx | 63 ++++++++++++++++++++++++++++++++ lib/content/scoreboard.json | 5 +-- lib/content/scoreboard.md | 3 +- lib/content/tests/md-doc.sx | 71 +++++++++++++++++++++++++++++++++++++ plans/content-on-sx.md | 9 ++++- 6 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 lib/content/md-doc.sx create mode 100644 lib/content/tests/md-doc.sx diff --git a/lib/content/conformance.sh b/lib/content/conformance.sh index 7fc2e99e..f4382aa4 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 page markdown text section stats table validate store snapshot crdt crdt-store sync md-import fed) +SUITES=(block doc render api meta page markdown text section stats table validate store snapshot crdt crdt-store sync md-import md-doc fed) OUT_JSON="lib/content/scoreboard.json" OUT_MD="lib/content/scoreboard.md" @@ -56,6 +56,7 @@ run_suite() { (load "lib/content/crdt-store.sx") (load "lib/content/sync.sx") (load "lib/content/md-import.sx") +(load "lib/content/md-doc.sx") (load "lib/content/fed.sx") (epoch 2) (eval "(define content-test-pass 0)") diff --git a/lib/content/md-doc.sx b/lib/content/md-doc.sx new file mode 100644 index 00000000..d7856111 --- /dev/null +++ b/lib/content/md-doc.sx @@ -0,0 +1,63 @@ +;; content-on-sx — Markdown document export (frontmatter + body). +;; +;; content/markdown-doc emits a YAML-ish --- frontmatter block from the document +;; metadata (title/slug/tags) followed by the Markdown body, completing the +;; metadata round-trip with md/import (md/import ∘ content/markdown-doc keeps +;; title/slug/tags). With no metadata it is just asMarkdown. +;; +;; Requires (loaded by harness): doc.sx, meta.sx (doc-title/slug/tags), +;; markdown.sx (asMarkdown). + +(define mdd-nl (str "\n")) + +(define + mdd-join + (fn + (sep parts) + (cond + ((= (len parts) 0) "") + ((= (len parts) 1) (first parts)) + (else (str (first parts) sep (mdd-join sep (rest parts))))))) + +(define + content/-fm-parts + (fn + (doc) + (append + (append + (if + (= (doc-title doc) nil) + (list) + (list (str "title: " (doc-title doc)))) + (if + (= (doc-slug doc) nil) + (list) + (list (str "slug: " (doc-slug doc))))) + (let + ((tags (doc-tags doc))) + (if + (= (len tags) 0) + (list) + (list (str "tags: " (mdd-join ", " tags)))))))) + +(define + content/-frontmatter + (fn + (doc) + (let + ((parts (content/-fm-parts doc))) + (if + (= (len parts) 0) + "" + (str "---" mdd-nl (mdd-join mdd-nl parts) mdd-nl "---"))))) + +(define + content/markdown-doc + (fn + (doc) + (let + ((fm (content/-frontmatter doc))) + (if + (= fm "") + (asMarkdown doc) + (str fm mdd-nl mdd-nl (asMarkdown doc)))))) diff --git a/lib/content/scoreboard.json b/lib/content/scoreboard.json index fc1339eb..289c7a8f 100644 --- a/lib/content/scoreboard.json +++ b/lib/content/scoreboard.json @@ -18,9 +18,10 @@ "crdt-store": {"pass": 14, "fail": 0}, "sync": {"pass": 14, "fail": 0}, "md-import": {"pass": 38, "fail": 0}, + "md-doc": {"pass": 12, "fail": 0}, "fed": {"pass": 20, "fail": 0} }, - "total_pass": 469, + "total_pass": 481, "total_fail": 0, - "total": 469 + "total": 481 } diff --git a/lib/content/scoreboard.md b/lib/content/scoreboard.md index 1cb542e6..d5824dd6 100644 --- a/lib/content/scoreboard.md +++ b/lib/content/scoreboard.md @@ -22,5 +22,6 @@ _Generated by `lib/content/conformance.sh`_ | crdt-store | 14 | 0 | 14 | | sync | 14 | 0 | 14 | | md-import | 38 | 0 | 38 | +| md-doc | 12 | 0 | 12 | | fed | 20 | 0 | 20 | -| **Total** | **469** | **0** | **469** | +| **Total** | **481** | **0** | **481** | diff --git a/lib/content/tests/md-doc.sx b/lib/content/tests/md-doc.sx new file mode 100644 index 00000000..30894df8 --- /dev/null +++ b/lib/content/tests/md-doc.sx @@ -0,0 +1,71 @@ +;; Extension — Markdown document export (frontmatter + body), round-trips with +;; md/import including metadata. + +(st-bootstrap-classes!) +(content/bootstrap!) +(content-bootstrap-markdown!) +(content-bootstrap-table!) + +(define nl (str "\n")) + +;; ── no metadata → plain markdown (no frontmatter) ── +(define plain (doc-append (doc-empty "d") (mk-heading "h" 1 "Hi"))) +(content-test + "no-meta == asMarkdown" + (content/markdown-doc plain) + (asMarkdown plain)) +(content-test "no-meta no frontmatter" (content/markdown-doc plain) "# Hi") + +;; ── full metadata frontmatter ── +(define + d + (doc-with-meta + (doc-append (doc-empty "post") (mk-heading "h" 1 "Hi")) + {:slug "my-post" :title "My Post" :tags (list "a" "b")})) +(content-test + "frontmatter export" + (content/markdown-doc d) + (str + "---" + nl + "title: My Post" + nl + "slug: my-post" + nl + "tags: a, b" + nl + "---" + nl + nl + "# Hi")) + +;; ── title only ── +(content-test + "title-only frontmatter" + (content/markdown-doc + (doc-with-title (doc-append (doc-empty "p") (mk-text "x" "body")) "T")) + (str "---" nl "title: T" nl "---" nl nl "body")) + +;; ── round-trip: import . export keeps metadata + blocks ── +(define rt (md/import (content/markdown-doc d) "post")) +(content-test "round-trip title" (doc-title rt) "My Post") +(content-test "round-trip slug" (doc-slug rt) "my-post") +(content-test "round-trip tags" (doc-tags rt) (list "a" "b")) +(content-test "round-trip body" (doc-types rt) (list "heading")) +(content-test + "round-trip body text" + (str (blk-send (doc-find rt "b0") "text")) + "Hi") + +;; ── round-trip a richer doc ── +(define + d2 + (doc-with-meta + (doc-append + (doc-append (doc-empty "p") (mk-heading "h" 2 "Title")) + (mk-text "p" "para text")) + {:title "Big" :tags (list "x")})) +(define rt2 (md/import (content/markdown-doc d2) "p")) +(content-test "rt2 title" (doc-title rt2) "Big") +(content-test "rt2 tags" (doc-tags rt2) (list "x")) +(content-test "rt2 types" (doc-types rt2) (list "heading" "text")) diff --git a/plans/content-on-sx.md b/plans/content-on-sx.md index 92f5653a..7800b3f9 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` → **469/469** (Phases 1–4 COMPLETE + 13 extensions: HTML/SX escaping, Markdown render + import incl. tables & frontmatter, 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` → **481/481** (Phases 1–4 COMPLETE + extensions: HTML/SX escaping, Markdown render + import/export incl. tables & frontmatter (full round-trip), CRDT replication, tree-aware validation, snapshot cache, doc metadata, plain-text render, nested block trees, doc stats, table block, HTML page wrapper) ## Ground rules @@ -82,6 +82,7 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─ - [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; incl. pipe tables + frontmatter → metadata) +- [x] Markdown doc export (`md-doc.sx`: content/markdown-doc, frontmatter from metadata, full round-trip) - [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 +93,12 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─ ## Progress log +- 2026-06-07 — Extension: Markdown document export (`md-doc.sx`). + `content/markdown-doc` emits a `---` frontmatter block from metadata + (title/slug/tags, only present fields) ahead of the Markdown body, or plain + asMarkdown when there's no metadata. Completes the metadata round-trip: + `md/import ∘ content/markdown-doc` preserves title/slug/tags + blocks. 12 + tests; suite 481/481. - 2026-06-07 — Extension: Markdown frontmatter. `md/import` parses a leading `---` / `key: value` / `---` block into document metadata (title, slug, comma-separated tags via `doc-with-meta`) before parsing the body; a `---`