content: Markdown frontmatter -> metadata + 9 tests (469/469)
Some checks are pending
Test, Build, and Deploy / test-build-deploy (push) Waiting to run
Some checks are pending
Test, Build, and Deploy / test-build-deploy (push) Waiting to run
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,15 @@
|
|||||||
;; content-on-sx — Markdown import adapter (markdown text -> block document).
|
;; content-on-sx — Markdown import adapter (markdown text -> block document).
|
||||||
;;
|
;;
|
||||||
;; A line-based parser, the inverse of markdown.sx's asMarkdown. Confined to the
|
;; 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
|
;; adapter boundary: the core knows nothing about Markdown. Handles a leading
|
||||||
;; (#..######), fenced code (```lang), blockquotes (> ), unordered (- / * ) and
|
;; --- frontmatter block (key: value -> doc metadata), ATX headings (#..######),
|
||||||
;; ordered (1. ) lists, thematic breaks (--- / ***), pipe tables (header row +
|
;; fenced code (```lang), blockquotes (> ), unordered (- / * ) and ordered (1. )
|
||||||
;; --- separator row + body rows), and paragraphs (consecutive plain lines joined
|
;; lists, thematic breaks (--- / ***), pipe tables (header + --- separator +
|
||||||
;; with a space). Block ids are assigned sequentially b0,b1…
|
;; 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
|
;; Requires (loaded by harness): block.sx, doc.sx, table.sx (mk-table),
|
||||||
;; markdown.sx for the adapter's export side.
|
;; meta.sx (doc-with-meta); markdown.sx for the adapter's export side.
|
||||||
|
|
||||||
(define md/-id (fn (i) (str "b" i)))
|
(define md/-id (fn (i) (str "b" i)))
|
||||||
(define md/-blank? (fn (s) (= s "")))
|
(define md/-blank? (fn (s) (= s "")))
|
||||||
@@ -35,6 +36,18 @@
|
|||||||
md/-drop
|
md/-drop
|
||||||
(fn (s prefix) (substring s (string-length prefix) (string-length s))))
|
(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
|
(define
|
||||||
md/-join-with
|
md/-join-with
|
||||||
(fn
|
(fn
|
||||||
@@ -365,7 +378,72 @@
|
|||||||
md/parse
|
md/parse
|
||||||
(fn (text) (md/-walk (split text (str "\n")) 0 (list))))
|
(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 ──
|
;; ── 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 content/from-markdown md/import)
|
||||||
(define markdown-adapter {:export (fn (doc) (asMarkdown doc)) :import md/import})
|
(define markdown-adapter {:export (fn (doc) (asMarkdown doc)) :import md/import})
|
||||||
|
|||||||
@@ -17,10 +17,10 @@
|
|||||||
"crdt": {"pass": 34, "fail": 0},
|
"crdt": {"pass": 34, "fail": 0},
|
||||||
"crdt-store": {"pass": 14, "fail": 0},
|
"crdt-store": {"pass": 14, "fail": 0},
|
||||||
"sync": {"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}
|
"fed": {"pass": 20, "fail": 0}
|
||||||
},
|
},
|
||||||
"total_pass": 460,
|
"total_pass": 469,
|
||||||
"total_fail": 0,
|
"total_fail": 0,
|
||||||
"total": 460
|
"total": 469
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,6 @@ _Generated by `lib/content/conformance.sh`_
|
|||||||
| crdt | 34 | 0 | 34 |
|
| crdt | 34 | 0 | 34 |
|
||||||
| crdt-store | 14 | 0 | 14 |
|
| crdt-store | 14 | 0 | 14 |
|
||||||
| sync | 14 | 0 | 14 |
|
| sync | 14 | 0 | 14 |
|
||||||
| md-import | 29 | 0 | 29 |
|
| md-import | 38 | 0 | 38 |
|
||||||
| fed | 20 | 0 | 20 |
|
| fed | 20 | 0 | 20 |
|
||||||
| **Total** | **460** | **0** | **460** |
|
| **Total** | **469** | **0** | **469** |
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
;; Extension — Markdown import adapter (markdown text -> blocks), inverse of
|
;; 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!)
|
(st-bootstrap-classes!)
|
||||||
(content/bootstrap!)
|
(content/bootstrap!)
|
||||||
@@ -130,6 +130,43 @@
|
|||||||
(doc-types dmix)
|
(doc-types dmix)
|
||||||
(list "heading" "table" "text"))
|
(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) ──
|
;; ── round-trip: import . export == identity (canonical markdown) ──
|
||||||
(define
|
(define
|
||||||
src
|
src
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ injected adapter, not core.
|
|||||||
|
|
||||||
## Status (rolling)
|
## 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
|
## 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] Markdown render mode (`asMarkdown:` / `content/render doc "md"`)
|
||||||
- [x] durable CRDT replication (`crdt-store.sx`: ops on persist, replay + converge)
|
- [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] 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] snapshot cache over replay (`snapshot.sx`: cache-not-primary, transparent)
|
||||||
- [x] document metadata (`meta.sx`: title/slug/tags + Ghost title plumbing)
|
- [x] document metadata (`meta.sx`: title/slug/tags + Ghost title plumbing)
|
||||||
- [x] plain-text render + excerpt (`text.sx`: asText, content/excerpt)
|
- [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
|
## 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
|
- 2026-06-07 — Extension: Markdown table import. `md-import.sx` now recognizes a
|
||||||
`| … |` header row followed by a `| --- |` separator and parses a `CtTable`
|
`| … |` header row followed by a `| --- |` separator and parses a `CtTable`
|
||||||
(cells trimmed, mixed with other blocks via blank-line separation), completing
|
(cells trimmed, mixed with other blocks via blank-line separation), completing
|
||||||
|
|||||||
Reference in New Issue
Block a user