;; 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 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), ;; 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 ""))) (define md/-hr? (fn (s) (if (= s "---") true (= s "***")))) (define ct-in? (fn (x xs) (cond ((= (len xs) 0) false) ((= (first xs) x) true) (else (ct-in? x (rest xs)))))) (define ct-starts-with? (fn (s prefix) (and (>= (string-length s) (string-length prefix)) (= (substring s 0 (string-length prefix)) prefix)))) (define 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 (sep parts) (cond ((= (len parts) 0) "") ((= (len parts) 1) (first parts)) (else (str (first parts) sep (md/-join-with sep (rest parts))))))) (define md/-join-sp (fn (parts) (md/-join-with " " parts))) (define md/-join-nl (fn (parts) (md/-join-with (str "\n") parts))) ;; ── heading detection (leading #s then a space) ── (define md/-hashes (fn (s n) (if (and (< n (string-length s)) (= (substring s n (+ n 1)) "#")) (md/-hashes s (+ n 1)) n))) (define md/-heading? (fn (line) (let ((n (md/-hashes line 0))) (and (> n 0) (<= n 6) (> (string-length line) n) (= (substring line n (+ n 1)) " "))))) (define md/-heading-block (fn (line i) (let ((n (md/-hashes line 0))) (mk-heading (md/-id i) n (substring line (+ n 1) (string-length line)))))) ;; ── list detection ── (define ct-digit? (fn (ch) (ct-in? ch (list "0" "1" "2" "3" "4" "5" "6" "7" "8" "9")))) (define md/-digits (fn (s n) (if (and (< n (string-length s)) (ct-digit? (substring s n (+ n 1)))) (md/-digits s (+ n 1)) n))) (define md/-ol? (fn (line) (let ((n (md/-digits line 0))) (and (> n 0) (>= (string-length line) (+ n 2)) (= (substring line n (+ n 2)) ". "))))) (define md/-drop-ol (fn (line) (let ((n (md/-digits line 0))) (substring line (+ n 2) (string-length line))))) (define md/-ul? (fn (line) (if (ct-starts-with? line "- ") true (ct-starts-with? line "* ")))) (define 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 (line) (if (md/-blank? line) false (if (ct-starts-with? line "```") false (if (md/-heading? line) false (if (ct-starts-with? line "> ") false (if (md/-hr? line) false (if (md/-ul? line) false (if (md/-ol? line) false true))))))))) ;; ── multi-line collectors ── (define md/-code (fn (lines i acc) (md/-code-collect (rest lines) (md/-drop (first lines) "```") (list) i acc))) (define md/-code-collect (fn (lines lang body i acc) (cond ((= (len lines) 0) (md/-walk lines (+ i 1) (cons (mk-code (md/-id i) lang (md/-join-nl (reverse body))) acc))) ((= (first lines) "```") (md/-walk (rest lines) (+ i 1) (cons (mk-code (md/-id i) lang (md/-join-nl (reverse body))) acc))) (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 (lines items i acc ordered) (if (= (len lines) 0) (md/-walk lines (+ i 1) (cons (mk-list (md/-id i) ordered (reverse items)) acc)) (let ((line (first lines))) (cond (ordered (if (md/-ol? line) (md/-list-collect (rest lines) (cons (md/-drop-ol line) items) i acc ordered) (md/-walk lines (+ i 1) (cons (mk-list (md/-id i) ordered (reverse items)) acc)))) (else (if (md/-ul? line) (md/-list-collect (rest lines) (cons (md/-drop-ul line) items) i acc ordered) (md/-walk lines (+ i 1) (cons (mk-list (md/-id i) ordered (reverse items)) acc))))))))) (define md/-para-collect (fn (lines parts i acc) (if (= (len lines) 0) (md/-walk lines (+ i 1) (cons (mk-text (md/-id i) (md/-join-sp (reverse parts))) acc)) (let ((line (first lines))) (if (md/-plain? line) (md/-para-collect (rest lines) (cons line parts) i acc) (md/-walk lines (+ i 1) (cons (mk-text (md/-id i) (md/-join-sp (reverse parts))) acc))))))) ;; ── main walk ── (define md/-walk (fn (lines i acc) (if (= (len lines) 0) (reverse acc) (let ((line (first lines))) (cond ((md/-blank? line) (md/-walk (rest lines) i acc)) ((ct-starts-with? line "```") (md/-code lines i acc)) ((md/-heading? line) (md/-walk (rest lines) (+ i 1) (cons (md/-heading-block line i) acc))) ((ct-starts-with? line "> ") (md/-walk (rest lines) (+ i 1) (cons (mk-quote (md/-id i) "" (md/-drop line "> ")) acc))) ((md/-hr? line) (md/-walk (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))))))) (define 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) (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})