From 26a51ac5d8ea810c906e6aad7b98a9bd521a1e0e Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 7 Jun 2026 02:44:02 +0000 Subject: [PATCH] content: Markdown frontmatter -> metadata + 9 tests (469/469) Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/content/md-import.sx | 94 +++++++++++++++++++++++++++++++--- lib/content/scoreboard.json | 6 +-- lib/content/scoreboard.md | 4 +- lib/content/tests/md-import.sx | 39 +++++++++++++- plans/content-on-sx.md | 9 +++- 5 files changed, 136 insertions(+), 16 deletions(-) diff --git a/lib/content/md-import.sx b/lib/content/md-import.sx index b7cb9016..509dba84 100644 --- a/lib/content/md-import.sx +++ b/lib/content/md-import.sx @@ -1,14 +1,15 @@ ;; content-on-sx — Markdown import adapter (markdown text -> block document). ;; ;; 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 (--- / ***), pipe tables (header row + -;; --- separator row + body rows), and paragraphs (consecutive plain lines joined -;; with a space). Block ids are assigned sequentially b0,b1… +;; adapter boundary: the core knows nothing about Markdown. Handles a leading +;; --- frontmatter block (key: value -> doc metadata), ATX headings (#..######), +;; fenced code (```lang), blockquotes (> ), unordered (- / * ) and ordered (1. ) +;; lists, thematic breaks (--- / ***), pipe tables (header + --- separator + +;; body), and paragraphs (consecutive plain lines joined with a space). Block ids +;; are assigned sequentially b0,b1… ;; -;; Requires (loaded by harness): block.sx, doc.sx, table.sx (mk-table); and -;; markdown.sx for the adapter's export side. +;; Requires (loaded by harness): block.sx, doc.sx, table.sx (mk-table), +;; meta.sx (doc-with-meta); markdown.sx for the adapter's export side. (define md/-id (fn (i) (str "b" i))) (define md/-blank? (fn (s) (= s ""))) @@ -35,6 +36,18 @@ md/-drop (fn (s prefix) (substring s (string-length prefix) (string-length s)))) +(define + md/-drop-n + (fn + (xs n) + (if + (= n 0) + xs + (if + (= (len xs) 0) + xs + (md/-drop-n (rest xs) (- n 1)))))) + (define md/-join-with (fn @@ -365,7 +378,72 @@ md/parse (fn (text) (md/-walk (split text (str "\n")) 0 (list)))) +;; ── frontmatter (leading --- key: value --- block) ── +(define + md/-frontmatter? + (fn (lines) (and (> (len lines) 0) (= (first lines) "---")))) +(define + md/-fm-end + (fn + (lines i) + (cond + ((>= i (len lines)) -1) + ((= (nth lines i) "---") i) + (else (md/-fm-end lines (+ i 1)))))) +(define + md/-fm-add + (fn + (acc line) + (let + ((parts (split line ":"))) + (if + (< (len parts) 2) + acc + (let + ((key (trim (first parts))) + (val (trim (md/-join-with ":" (rest parts))))) + (cond + ((= key "title") (assoc acc :title val)) + ((= key "slug") (assoc acc :slug val)) + ((= key "tags") + (assoc acc :tags (map (fn (t) (trim t)) (split val ",")))) + (else acc))))))) +(define + md/-fm-pairs + (fn + (lines start end acc) + (if + (>= start end) + acc + (md/-fm-pairs + lines + (+ start 1) + end + (md/-fm-add acc (nth lines start)))))) + ;; ── adapter ── -(define md/import (fn (text doc-id) (doc-new doc-id (md/parse text)))) +(define + md/import + (fn + (text doc-id) + (let + ((lines (split text (str "\n")))) + (if + (md/-frontmatter? lines) + (let + ((end (md/-fm-end lines 1))) + (if + (= end -1) + (doc-new doc-id (md/-walk lines 0 (list))) + (doc-with-meta + (doc-new + doc-id + (md/-walk + (md/-drop-n lines (+ end 1)) + 0 + (list))) + (md/-fm-pairs lines 1 end {})))) + (doc-new doc-id (md/-walk lines 0 (list))))))) + (define content/from-markdown md/import) (define markdown-adapter {:export (fn (doc) (asMarkdown doc)) :import md/import}) diff --git a/lib/content/scoreboard.json b/lib/content/scoreboard.json index c8c26bc5..fc1339eb 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": 29, "fail": 0}, + "md-import": {"pass": 38, "fail": 0}, "fed": {"pass": 20, "fail": 0} }, - "total_pass": 460, + "total_pass": 469, "total_fail": 0, - "total": 460 + "total": 469 } diff --git a/lib/content/scoreboard.md b/lib/content/scoreboard.md index 16e68dfb..1cb542e6 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 | 29 | 0 | 29 | +| md-import | 38 | 0 | 38 | | fed | 20 | 0 | 20 | -| **Total** | **460** | **0** | **460** | +| **Total** | **469** | **0** | **469** | diff --git a/lib/content/tests/md-import.sx b/lib/content/tests/md-import.sx index 35f527c2..56c08da9 100644 --- a/lib/content/tests/md-import.sx +++ b/lib/content/tests/md-import.sx @@ -1,5 +1,5 @@ ;; Extension — Markdown import adapter (markdown text -> blocks), inverse of -;; asMarkdown. Round-trips canonical Markdown. +;; asMarkdown. Round-trips canonical Markdown; parses frontmatter + tables. (st-bootstrap-classes!) (content/bootstrap!) @@ -130,6 +130,43 @@ (doc-types dmix) (list "heading" "table" "text")) +;; ── frontmatter ── +(define + dfm + (md/import + (str + "---" + nl + "title: My Post" + nl + "slug: my-post" + nl + "tags: a, b, c" + nl + "---" + nl + "# Hi" + nl + nl + "body") + "d")) +(content-test "fm title" (doc-title dfm) "My Post") +(content-test "fm slug" (doc-slug dfm) "my-post") +(content-test "fm tags" (doc-tags dfm) (list "a" "b" "c")) +(content-test "fm body types" (doc-types dfm) (list "heading" "text")) +(content-test + "fm body content" + (str (blk-send (doc-find dfm "b0") "text")) + "Hi") +(content-test "no fm title nil" (doc-title (md/import "# Hi" "d")) nil) +(content-test + "hr not frontmatter" + (doc-types (md/import (str "text" nl nl "---") "d")) + (list "text" "divider")) +(define dfmo (md/import (str "---" nl "title: T" nl "---") "d")) +(content-test "fm only title" (doc-title dfmo) "T") +(content-test "fm only empty body" (doc-ids dfmo) (list)) + ;; ── round-trip: import . export == identity (canonical markdown) ── (define src diff --git a/plans/content-on-sx.md b/plans/content-on-sx.md index dfb8773e..92f5653a 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` → **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) +`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) ## 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; incl. pipe tables) +- [x] Markdown import adapter (`md-import.sx`: text → blocks, round-trips export; incl. pipe tables + frontmatter → metadata) - [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 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 `---` + elsewhere stays a divider. Ties the Markdown importer to the metadata layer the + way real blog posts work. +9 tests; suite 469/469. - 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