diff --git a/scripts/roam-export-blog-test.el b/scripts/roam-export-blog-test.el new file mode 100644 index 0000000..f8007c6 --- /dev/null +++ b/scripts/roam-export-blog-test.el @@ -0,0 +1,188 @@ +;;; roam-export-blog-test.el --- ERT tests for roam-export-blog -*- lexical-binding: t; -*- + +;;; Commentary: + +;; Run with: +;; emacs --batch -Q -l org -l ox-md -l scripts/roam-export-blog.el \ +;; -l scripts/roam-export-blog-test.el -f ert-run-tests-batch-and-exit +;; +;; Tests use a fixture vault under scripts/test-fixtures/vault and write +;; output into throw-away firehose repo skeletons under /tmp. + +;;; Code: + +(require 'ert) +(require 'roam-export-blog) + +(defconst reb-test-dir + (directory-file-name (file-name-directory (or load-file-name default-directory)))) + +(defconst reb-test-repo-root (expand-file-name ".." reb-test-dir)) + +(defconst reb-test-vault (expand-file-name "test-fixtures/vault" reb-test-dir)) + +(defun reb-test--file (name) + "Path to fixture file NAME in the test vault." + (expand-file-name name reb-test-vault)) + +(defun reb-test--new-root () + "Create a throw-away firehose repo skeleton and return its root." + (let ((root (make-temp-file "/tmp/reb-root-" t))) + (make-directory (expand-file-name "app/priv/blog/engineering" root) t) + (make-directory (expand-file-name "app/priv/static/images/blog" root) t) + root)) + +(defun reb-test--setup-org-id () + "Point org-id at a writable temp locations file and index the fixture vault." + (setq org-id-locations-file (make-temp-file "/tmp/reb-orgid-")) + (setq org-id-locations nil) + (org-id-update-id-locations + (mapcar #'reb-test--file '("note.org" "jev.org" "laya.org" "notitle.org")) t)) + +(defmacro reb-test--with-export (root &rest body) + "Run BODY with a fresh repo ROOT and a pinned export date." + (declare (indent 1)) + `(progn + (reb-test--setup-org-id) + (let ((roam-export-blog-firehose-root ,root) + (roam-export-blog--current-date '("2026" "10" "08"))) + ,@body))) + +;;; Pure helpers + +(ert-deftest roam-export-blog/slug () + (should (equal "will-it-blend" (roam-export-blog--slug "Will It Blend"))) + (should (equal "improve-diagnostic-in-the-tdd-cycle" + (roam-export-blog--slug "Improve Diagnostic in the TDD cycle"))) + (should (equal "v0-2-0-rss-subscribe-links" + (roam-export-blog--slug "v0.2.0 — RSS Subscribe Links")))) + +(ert-deftest roam-export-blog/truncate-description-short () + (should (equal "Short paragraph." + (roam-export-blog--truncate-description "Short paragraph.")))) + +(ert-deftest roam-export-blog/truncate-description-at-word-boundary () + (let* ((words (make-list 60 "word")) + (text (string-join words " ")) + (result (roam-export-blog--truncate-description text))) + (should (<= (length result) (+ roam-export-blog-description-length 1))) + (should (string-suffix-p "…" result)) + (should-not (string-suffix-p " " result)) + (should (string-prefix-p "word word" result)))) + +;;; Roam ref resolution + +(ert-deftest roam-export-blog/roam-refs () + (reb-test--setup-org-id) + (should (equal "https://example.com/jev" + (roam-export-blog--roam-refs (reb-test--file "jev.org")))) + (should (null (roam-export-blog--roam-refs (reb-test--file "laya.org"))))) + +;;; Aborts + +(ert-deftest roam-export-blog/no-title-aborts () + (let ((root (reb-test--new-root))) + (reb-test--with-export root + (should-error (roam-export-blog--export-file (reb-test--file "notitle.org")) + :type 'user-error) + ;; Aborting must not have written any posts. + (should (null + (directory-files-recursively + (expand-file-name "app/priv/blog" root) "\\.md$")))))) + +(ert-deftest roam-export-blog/filename-collision-aborts () + (let ((root (reb-test--new-root))) + (reb-test--with-export root + ;; Pre-create the exact target file. + (make-directory + (expand-file-name "app/priv/blog/engineering/2026" root) t) + (write-region "" nil + (expand-file-name + "app/priv/blog/engineering/2026/10-08-will-it-blend.md" root)) + (should-error (roam-export-blog--export-file (reb-test--file "note.org")) + :type 'user-error) + ;; Aborting must not have copied any images (no side effects). + (should (null + (directory-files-recursively + (expand-file-name "app/priv/static/images/blog" root) + ".")))))) + +;;; Full export + +(ert-deftest roam-export-blog/end-to-end () + (let ((root (reb-test--new-root)) + result path body) + (reb-test--with-export root + (setq result (roam-export-blog--export-file (reb-test--file "note.org")))) + (setq path (car result)) + ;; Written at the agreed path. + (should (equal (expand-file-name + "app/priv/blog/engineering/2026/10-08-will-it-blend.md" root) + path)) + (should (file-exists-p path)) + ;; Image attachment copied into static images with unchanged content. + (let ((image (expand-file-name "app/priv/static/images/blog/2026/will-it-blend.svg" root))) + (should (file-exists-p image)) + (should (equal (with-temp-buffer (insert-file-contents image) (buffer-string)) + (with-temp-buffer + (insert-file-contents (reb-test--file ".attach/00/000000-0000-0000-0000-000000000001/will-it-blend.svg")) + (buffer-string))))) + (setq body (with-temp-buffer (insert-file-contents path) (buffer-string))) + ;; Frontmatter. + (should (string-match-p "title: \"Will It Blend\"" body)) + (should (string-match-p "author: \"Willem van den Ende\"" body)) + (should (string-match-p "tags: ~w()" body)) + (should (string-match-p + "description: \"Will it Blend was a series of videos promoting a blender brand.\"" + body)) + (should (string-match-p "image: \"/images/blog/2026/will-it-blend.svg\"" body)) + (should (string-match-p "published: false" body)) + ;; Markdown body. + (should (string-match-p "Will it Blend was a series of videos promoting a blender brand." body)) + (should (string-match-p + (regexp-quote "![will-it-blend](/images/blog/2026/will-it-blend.svg)") + body)) + (should (string-match-p (regexp-quote "[Jev](https://example.com/jev)") body)) + (should (string-match-p "Laya popped up" body)) + (should (string-match-p "a missing note vanished" body)) + (should (string-match-p "TODO source needed (LinkedIn)" body)) + (should (string-match-p "^## I learnt this from Julia$" body)) + (should (string-match-p "^> A quoted comment.$" body)) + (should (string-match-p "^```elixir$" body)) + (should (string-match-p (regexp-quote "**bold**") body)) + ;; Org syntax must not leak into the markdown. + (should-not (string-match-p "#\\+begin" body)) + (should-not (string-match-p "attachment:" body)) + (should-not (string-match-p "id:0000" body)) + (should-not (string-match-p "A private comment that must not be exported" body)) + ;; Warnings: dangling ref, ref without ROAM_REFS, ignored non-image + ;; attachment. + (should (= 3 (length (cdr result)))) + (should (cl-some (lambda (w) (string-match-p "00000000-0000-0000-0000-000000009999" w)) + (cdr result))) + (should (cl-some (lambda (w) (string-match-p "00000000-0000-0000-0000-000000000003" w)) + (cdr result))) + (should (cl-some (lambda (w) (string-match-p "attached-notes.txt" w)) + (cdr result))))) + +(ert-deftest roam-export-blog/description-truncated-in-frontmatter () + (let* ((root (reb-test--new-root)) + (vault (make-temp-file "/tmp/reb-vault-" t)) + (note (expand-file-name "longpar.org" vault)) + (long-text (string-join (make-list 50 "lorem" ) " "))) + (with-temp-file note + (insert ":PROPERTIES:\n:ID: 00000000-0000-0000-0000-000000000005\n:END:\n" + "#+title: Long Paragraph\n\n" long-text "\n")) + (reb-test--with-export root + (let ((result (roam-export-blog--export-file note))) + (let ((body (with-temp-buffer + (insert-file-contents (car result)) + (buffer-string)))) + (string-match "description: \"\\([^\"]*\\)\"" body) + (let ((desc (match-string 1 body))) + (should (<= (length desc) (+ roam-export-blog-description-length 1))) + (should (string-suffix-p "…" desc)) + (should (string-prefix-p "lorem lorem" desc)))))))) + +(provide 'roam-export-blog-test) +;;; roam-export-blog-test.el ends here diff --git a/scripts/roam-export-blog.el b/scripts/roam-export-blog.el new file mode 100644 index 0000000..0b4d27f --- /dev/null +++ b/scripts/roam-export-blog.el @@ -0,0 +1,453 @@ +;;; roam-export-blog.el --- Export an org-roam note as a firehose blog post -*- lexical-binding: t; -*- + +;;; Commentary: + +;; M-x roam-export-blog, run from an org-roam note, converts the note to +;; markdown and writes it directly into the firehose repo as a draft post: +;; +;; /app/priv/blog/engineering//-
-.md +;; +;; with `published: false' frontmatter, so the repo's git diff acts as the +;; review mechanism. +;; +;; What happens during export: +;; +;; - `[[id:][text]]' roam refs are resolved via org-id. When the +;; target note carries a :ROAM_REFS: property, the link becomes a +;; markdown link to the first URL; otherwise it degrades to plain text. +;; - `[[attachment:...]]' links are resolved via the org-attach ID scheme +;; (.attach///). Images are copied into +;; app/priv/static/images/blog// and referenced with the +;; /images/blog/... web path. Non-image attachments are ignored. +;; - Org syntax is converted to markdown with a derived ox-md backend +;; (fenced code blocks with language, H2 top-level headings, no TOC). +;; - Dropped refs, ignored attachments and unhandled constructs are +;; reported as warnings. + +;;; Code: + +(require 'org) +(require 'ox-md) +(require 'org-id) +(require 'cl-lib) + +(defgroup roam-export-blog nil + "Export org-roam notes as firehose blog posts." + :group 'org) + +(defcustom roam-export-blog-firehose-root "/Users/willem/dev/elixir/firehose" + "Path to the firehose repository root." + :type 'directory) + +(defcustom roam-export-blog-author "Willem van den Ende" + "Author name written into generated frontmatter." + :type 'string) + +(defcustom roam-export-blog-description-length 200 + "Maximum length of the generated description (before the ellipsis)." + :type 'integer) + +(defcustom roam-export-blog-image-extensions + '("svg" "png" "jpg" "jpeg" "gif" "webp" "avif") + "Attachment extensions that count as images." + :type '(repeat string)) + +(defvar roam-export-blog--current-date nil + "Override for the export date during tests, as a list (YEAR MONTH DAY).") + +;;; Derived markdown backend + +(defun roam-export-blog-md-src-block (src-block _contents info) + "Transcode SRC-BLOCK into a fenced markdown code block with language." + (let ((lang (org-element-property :language src-block)) + (value (org-export-format-code-default src-block info))) + (concat "```" (or lang "") "\n" value "```"))) + +(defun roam-export-blog-md-link (link desc info) + "Transcode LINK; file links to images become markdown images. + +Everything else is delegated to `org-md-link'." + (let* ((type (org-element-property :type link)) + (path (org-element-property :path link))) + (if (and (equal type "file") (roam-export-blog--image-p path)) + (format "![%s](%s)" + (or (and desc (string-trim (substring-no-properties desc))) + (file-name-base path)) + path) + (org-md-link link desc info)))) + +(org-export-define-derived-backend 'firehose-md 'md + :translate-alist '((src-block . roam-export-blog-md-src-block) + (link . roam-export-blog-md-link))) + +(defconst roam-export-blog--export-options + '(:with-toc nil :section-numbers nil :md-toplevel-hlevel 2)) + +;;; Small helpers + +(defun roam-export-blog--slug (title) + "Derive a filename slug from TITLE." + (let* ((lower (downcase title)) + (slugged (replace-regexp-in-string "[^a-z0-9]+" "-" lower))) + (replace-regexp-in-string "\\`-+\\|-+\\'" "" slugged))) + +(defun roam-export-blog--date-parts () + "Export date as a list of strings (YEAR MONTH DAY)." + (or roam-export-blog--current-date + (list (format-time-string "%Y") + (format-time-string "%m") + (format-time-string "%d")))) + +(defun roam-export-blog--escape-elixir-string (text) + "Escape TEXT for embedding in an Elixir double-quoted string." + (replace-regexp-in-string + "\"" "\\\\\"" + (replace-regexp-in-string "\\\\" "\\\\\\\\" text))) + +(defun roam-export-blog--truncate-description (text) + "Truncate TEXT to about `roam-export-blog-description-length' characters." + (let ((max roam-export-blog-description-length)) + (if (<= (length text) max) + text + (let ((cut (cl-position ?\s (substring text 0 max) :from-end t))) + (concat (if cut (substring text 0 cut) (substring text 0 max)) + "…"))))) + +(defun roam-export-blog--plain-text (element) + "Return a plain-text approximation of org ELEMENT." + (cond + ((stringp element) element) + ((memq (org-element-type element) '(code verbatim inline-src-block)) + (or (org-element-property :value element) "")) + ((eq (org-element-type element) 'entity) + (or (org-element-property :utf8 element) "")) + (t (mapconcat #'roam-export-blog--plain-text (org-element-contents element) "")))) + +;;; Reading the note + +(defun roam-export-blog--title (buffer) + "Return the #+TITLE: keyword value in BUFFER, or nil." + (with-current-buffer buffer + (cadr (assoc "TITLE" (org-collect-keywords '("TITLE")))))) + +(defun roam-export-blog--first-paragraph-text (buffer) + "Return the plain text of the first paragraph in BUFFER." + (with-current-buffer buffer + (let ((para (org-element-map (org-element-parse-buffer) 'paragraph + #'identity nil t))) + (when para + (replace-regexp-in-string + "[ \t\n]+" " " + (string-trim + (substring-no-properties (roam-export-blog--plain-text para)))))))) + +(defun roam-export-blog--roam-refs (file) + "Return the first :ROAM_REFS: URL of the note in FILE, or nil." + (with-current-buffer (find-file-noselect file) + (org-with-wide-buffer + (goto-char (point-min)) + (let ((value (org-entry-get (point) "ROAM_REFS"))) + (when value (car (split-string value "[ \t]+"))))))) + +(defun roam-export-blog--note-title (file) + "Return the #+TITLE: of the note in FILE, or nil." + (roam-export-blog--title (find-file-noselect file))) + +;;; Link processing + +(defun roam-export-blog--vault-attach-dir (file) + "Return the .attach directory dominating FILE, or nil." + (when-let* ((root (locate-dominating-file file ".attach"))) + (expand-file-name ".attach" root))) + +(defun roam-export-blog--image-p (file) + "Return non-nil when FILE looks like an image attachment." + (member (downcase (file-name-extension file)) + roam-export-blog-image-extensions)) + +(defun roam-export-blog--link-description (link) + "Return the description text of LINK, or nil." + (let ((begin (org-element-property :contents-begin link)) + (end (org-element-property :contents-end link))) + (when (and begin end) + (buffer-substring-no-properties begin end)))) + +(defun roam-export-blog--desc-or (desc fallback) + "Return trimmed DESC, or FALLBACK when DESC is nil or empty. + +An empty string is truthy in Elisp, so descriptions must be +treated as absent explicitly." + (let ((trimmed (when desc (string-trim desc)))) + (if (and trimmed (not (string-empty-p trimmed))) + trimmed + fallback))) + +(defun roam-export-blog--desc-verbatim-or (desc fallback) + "Return DESC unchanged, or FALLBACK when DESC is nil or blank. + +Used for plain-text replacements: a description like \"Jev \" +may carry the spacing that separated it from surrounding text in +the org source." + (if (and desc (not (string-empty-p (string-trim desc)))) + desc + fallback)) + +(defun roam-export-blog--attachment-path (link vault-attach-dir note-id) + "Return the absolute source path of attachment LINK, or nil. + +Handles three shapes: file links already normalized by org to an +absolute path under the vault's .attach directory, plain +attachment: links and fuzzy links carrying an attachment: prefix. +Resolution follows the org-attach ID scheme: +.attach///." + (let* ((type (org-element-property :type link)) + (path (org-element-property :path link)) + (bare (if (and (equal type "fuzzy") + (string-prefix-p "attachment:" path)) + (substring path (length "attachment:")) + path))) + (cond + ((and (equal type "file") (file-name-absolute-p path) + vault-attach-dir + (string-prefix-p vault-attach-dir (expand-file-name path))) + (expand-file-name path)) + ((and vault-attach-dir note-id bare + (>= (length note-id) 2)) + (let ((candidate (expand-file-name + (concat (substring note-id 0 2) + "/" (substring note-id 2) + "/" bare) + vault-attach-dir))) + (when (file-exists-p candidate) candidate))) + (t nil)))) + +(defun roam-export-blog--resolve-id (id) + "Return the file containing org ID, or nil." + (ignore-errors (org-id-find id))) + +(defun roam-export-blog--link-replacement (link vault-attach-dir note-id) + "Compute the replacement for LINK. + +Return (REPLACEMENT WARNINGS IMAGES), where REPLACEMENT is org +syntax to substitute for the link (nil leaves the link alone), +WARNINGS is a list of warning strings and IMAGES is a list of +(SOURCE . DEST) file copies to perform." + (let* ((type (org-element-property :type link)) + (path (org-element-property :path link)) + (desc (roam-export-blog--link-description link)) + (replacement nil) + (warnings nil) + (images nil)) + (cond + ;; Roam refs: [[id:uuid][text]] + ((equal type "id") + (let* ((target (roam-export-blog--resolve-id path)) + (refs (when target (roam-export-blog--roam-refs (car target))))) (cond + ((and target refs) + (setq replacement (format "[[%s][%s]]" + refs (roam-export-blog--desc-or desc refs)))) + (target + (setq replacement (roam-export-blog--desc-verbatim-or + desc (or (roam-export-blog--note-title (car target)) + ""))) + (push (format "dropped ref: id %s has no ROAM_REFS, kept plain text %S" + path replacement) + warnings)) + (t + (setq replacement (roam-export-blog--desc-verbatim-or desc "")) + (push (format "dropped ref: unresolved id %s, kept plain text %S" + path replacement) + warnings))))) + ;; Attachments (and file links that org normalized from attachments) + ((and vault-attach-dir + (or (member type '("file" "attachment")) + (and (equal type "fuzzy") + (string-prefix-p "attachment:" path)))) + (let ((source (roam-export-blog--attachment-path link vault-attach-dir note-id))) + (cond + ((and source (file-exists-p source) (roam-export-blog--image-p source)) + (let* ((name (file-name-nondirectory source)) + (year (car (roam-export-blog--date-parts))) + (web-path (format "/images/blog/%s/%s" year name)) + (dest (expand-file-name + (format "app/priv/static/images/blog/%s/%s" year name) + roam-export-blog-firehose-root))) + (setq images (list (cons source dest))) + (setq replacement + (format "[[file:%s][%s]]" web-path + (roam-export-blog--desc-or desc + (file-name-base name)))))) + ((and source (file-exists-p source)) + (setq replacement (roam-export-blog--desc-verbatim-or desc "")) + (push (format "ignored attachment (not an image): %s" + (file-name-nondirectory source)) + warnings)) + (t + (setq replacement (roam-export-blog--desc-verbatim-or desc "")) + (push (format "unhandled link dropped: %s:%s" type path) + warnings))))) + ;; Any other file: link points into the vault, not the blog. + ((equal type "file") + (setq replacement (roam-export-blog--desc-verbatim-or desc "")) + (push (format "unhandled link dropped: file:%s" path) warnings)) + ;; Everything else (https, mailto, ...) passes through untouched. + (t nil)) + (list replacement warnings images))) + +(defun roam-export-blog--process-links (buffer vault-attach-dir) + "Rewrite id/attachment/file links in BUFFER's text. + +BUFFER is not modified; a processed copy of its text is returned. +Return (TEXT WARNINGS IMAGES) where IMAGES is an alist of +(SOURCE . DEST) image copies to perform." + (with-current-buffer buffer + (let ((text (buffer-string)) + (note-id (org-entry-get (point-min) "ID")) + (warnings nil) + (all-images nil)) + (let* ((tree (org-element-parse-buffer)) + (replacements + (reverse + (org-element-map tree 'link + (lambda (link) + (let* ((result (roam-export-blog--link-replacement + link vault-attach-dir note-id)) + (replacement (nth 0 result)) + (warns (nth 1 result)) + (images (nth 2 result))) + (setq warnings (append warnings warns) + all-images (append all-images images)) + (when replacement + (list (org-element-property :begin link) + ;; :end includes :post-blank (trailing + ;; whitespace); exclude it from the + ;; replaced region. + (- (org-element-property :end link) + (or (org-element-property :post-blank link) 0)) + replacement)))) + nil nil nil)))) + (dolist (r replacements) + (let ((begin (nth 0 r)) + (end (nth 1 r)) + (replacement (nth 2 r))) + (setq text (concat (substring text 0 (1- begin)) + replacement + (substring text (1- end)))))) + (list text warnings all-images))))) + +;;; Markdown generation + +(defun roam-export-blog--to-markdown (text) + "Export org TEXT to markdown with the firehose-md backend." + (with-temp-buffer + (insert text) + (org-mode) + (let ((out (org-export-to-buffer + 'firehose-md "*roam-export-blog export*" + nil nil nil nil roam-export-blog--export-options))) + (with-current-buffer out (buffer-string))))) + +(defun roam-export-blog--cleanup (markdown) + "Collapse runs of blank lines in MARKDOWN." + (replace-regexp-in-string "\n\\(?:[ \t]*\n\\)+\\'" "\n" + (replace-regexp-in-string "\n\\{3,\\}" "\n\n" markdown))) + +;;; Frontmatter and writing + +(defun roam-export-blog--frontmatter (title description image) + "Build the Elixir-term frontmatter for the post." + (concat "%{\n" + (format " title: %S,\n" (roam-export-blog--escape-elixir-string title)) + (format " author: %S,\n" roam-export-blog-author) + " tags: ~w(),\n" + (format " description: %S,\n" + (roam-export-blog--escape-elixir-string description)) + (format " image: %S,\n" image) + " published: false\n" + "}\n---\n")) + +(defun roam-export-blog--post-path (slug) + "Return the target post path for SLUG." + (let ((year (car (roam-export-blog--date-parts))) + (month (nth 1 (roam-export-blog--date-parts))) + (day (nth 2 (roam-export-blog--date-parts)))) + (expand-file-name + (format "app/priv/blog/engineering/%s/%s-%s-%s.md" year month day slug) + roam-export-blog-firehose-root))) + +(defun roam-export-blog--copy-images (images) + "Copy IMAGES (alist of SOURCE . DEST), warning on basename clashes." + (let ((copied nil) + (warnings nil)) + (dolist (pair images) + (let ((source (car pair)) + (dest (cdr pair))) + (cond + ((member dest copied) + (push (format "skipped duplicate image copy: %s" + (file-name-nondirectory source)) + warnings)) + (t + (make-directory (file-name-directory dest) t) + (copy-file source dest t) + (push dest copied))))) + (nreverse warnings))) + +;;; The command + +(defun roam-export-blog--export-file (&optional file) + "Export org-roam note FILE to a firehose blog post. + +Return (OUTPUT-PATH . WARNINGS). Abort with `user-error' when the +note has no title or the target file already exists." + (setq file (or file (buffer-file-name))) + (unless file + (user-error "roam-export-blog: not visiting a file")) + (let* ((buffer (find-file-noselect file)) + (title (roam-export-blog--title buffer))) + (unless title + (user-error "roam-export-blog: %s has no #+title: keyword, aborting" file)) + (let* ((slug (roam-export-blog--slug title)) + (post-path (roam-export-blog--post-path slug))) + (when (file-exists-p post-path) + (user-error "roam-export-blog: %s already exists, aborting" post-path)) + (let* ((processed (roam-export-blog--process-links + buffer (roam-export-blog--vault-attach-dir file))) + (text (nth 0 processed)) + (link-warnings (nth 1 processed)) + (images (nth 2 processed)) + (copy-warnings (roam-export-blog--copy-images images)) + (warnings (append (nreverse link-warnings) copy-warnings)) + (image (when images + (let ((static (expand-file-name + "app/priv/static" + roam-export-blog-firehose-root))) + (string-remove-prefix static (cdr (car images)))))) + (description (roam-export-blog--truncate-description + (roam-export-blog--first-paragraph-text buffer))) + (markdown (roam-export-blog--cleanup (roam-export-blog--to-markdown text)))) + (make-directory (file-name-directory post-path) t) + (with-temp-file post-path + (insert (roam-export-blog--frontmatter title description image) + markdown)) + (cons post-path warnings))))) + +;;;###autoload +(defun roam-export-blog () + "Export the current org-roam note as a firehose blog post." + (interactive) + (let ((result (roam-export-blog--export-file (buffer-file-name)))) + (let ((path (car result)) + (warnings (cdr result))) + (message "roam-export-blog: wrote %s%s" path + (if warnings + (format " (%d warning%s)" (length warnings) + (if (= 1 (length warnings)) "" "s")) + "")) + (when warnings + (with-output-to-temp-buffer "*roam-export-blog warnings*" + (dolist (w warnings) + (princ (format "%s\n" w)))))))) + +(provide 'roam-export-blog) +;;; roam-export-blog.el ends here diff --git a/scripts/test-fixtures/vault/.attach/00/000000-0000-0000-0000-000000000001/attached-notes.txt b/scripts/test-fixtures/vault/.attach/00/000000-0000-0000-0000-000000000001/attached-notes.txt new file mode 100644 index 0000000..f070a7c --- /dev/null +++ b/scripts/test-fixtures/vault/.attach/00/000000-0000-0000-0000-000000000001/attached-notes.txt @@ -0,0 +1 @@ +some attached notes diff --git a/scripts/test-fixtures/vault/.attach/00/000000-0000-0000-0000-000000000001/will-it-blend.svg b/scripts/test-fixtures/vault/.attach/00/000000-0000-0000-0000-000000000001/will-it-blend.svg new file mode 100644 index 0000000..2b692e1 --- /dev/null +++ b/scripts/test-fixtures/vault/.attach/00/000000-0000-0000-0000-000000000001/will-it-blend.svg @@ -0,0 +1 @@ + diff --git a/scripts/test-fixtures/vault/jev.org b/scripts/test-fixtures/vault/jev.org new file mode 100644 index 0000000..ff7e835 --- /dev/null +++ b/scripts/test-fixtures/vault/jev.org @@ -0,0 +1,7 @@ +:PROPERTIES: +:ID: 00000000-0000-0000-0000-000000000002 +:ROAM_REFS: https://example.com/jev +:END: +#+title: Jev + +Body of Jev. diff --git a/scripts/test-fixtures/vault/laya.org b/scripts/test-fixtures/vault/laya.org new file mode 100644 index 0000000..8e2d126 --- /dev/null +++ b/scripts/test-fixtures/vault/laya.org @@ -0,0 +1,6 @@ +:PROPERTIES: +:ID: 00000000-0000-0000-0000-000000000003 +:END: +#+title: Laya + +Body of Laya. diff --git a/scripts/test-fixtures/vault/note.org b/scripts/test-fixtures/vault/note.org new file mode 100644 index 0000000..1f4cf06 --- /dev/null +++ b/scripts/test-fixtures/vault/note.org @@ -0,0 +1,26 @@ +:PROPERTIES: +:ID: 00000000-0000-0000-0000-000000000001 +:END: +#+title: Will It Blend + +Will it Blend was a series of videos promoting a blender brand. + +# A private comment that must not be exported. + +[[attachment:will-it-blend.svg]] + +For instance, a day after [[id:00000000-0000-0000-0000-000000000002][Jev]] came out, [[id:00000000-0000-0000-0000-000000000003][Laya]] popped up, and [[id:00000000-0000-0000-0000-000000009999][a missing note]] vanished. TODO source needed (LinkedIn) + +[[attachment:attached-notes.txt]] + +* I learnt this from Julia + +Body text with *bold*. + +#+begin_quote +A quoted comment. +#+end_quote + +#+begin_src elixir +IO.puts("hi") +#+end_src diff --git a/scripts/test-fixtures/vault/notitle.org b/scripts/test-fixtures/vault/notitle.org new file mode 100644 index 0000000..5262118 --- /dev/null +++ b/scripts/test-fixtures/vault/notitle.org @@ -0,0 +1,5 @@ +:PROPERTIES: +:ID: 00000000-0000-0000-0000-000000000004 +:END: + +No title here.