;;; seam-export.el --- Seam HTML exporter -*- lexical-binding: t -*- ;; Copyright (C) 2025 Spencer Williams ;; Author: Spencer Williams <spnw@plexwave.org> ;; SPDX-License-Identifier: GPL-3.0-or-later ;; This file is not part of GNU Emacs. ;; This program is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <http://www.gnu.org/licenses/>. ;;; Commentary: ;; Frontend for Seam's HTML exporter. ;;; Code: (require 'cl-lib) (require 'seam-html) (defvar seam-export--types nil) (defvar seam-export--template nil) (defvar seam-export--root-path nil) (defvar seam-export--no-extension nil) (defvar seam-export--internal-link-class nil) (defvar seam-export--options nil) (defgroup seam-export nil "Options for Seam exporter." :tag "Seam Export" :group 'seam) (defcustom seam-export-alist nil "Association list used by Seam to determine how to export notes. The car of each element is an HTML directory to which Seam will export a subset of notes. The cdr is a plist containing any number of these properties: `:types' List of note types to export to this directory. Required. `:template-file' The HTML template file to be used by the exporter. If this is missing, falls back to :template-string, `seam-export-template-file', or `seam-export-template-string' in that order. `:template-string' The HTML template string to be used by the exporter. If this is missing, falls back to :template-file, `seam-export-template-file', or `seam-export-template-string' in that order. `:root-path' The root path used for rendering internal links. Defaults to \"\", which means all paths are relative. `:no-extension' Whether to drop the \".html\" file extension in links. Defaults to nil. `:internal-link-class' CSS class name for internal links. Defaults to the value of `seam-export-internal-link-class'. `:backend-options' A plist of extra options passed to the Org HTML backend. This can be used to override any of the defaults set in `seam-export-backend-options'." :group 'seam-export :type '(alist :key-type string :value-type plist)) (defcustom seam-export-template-file nil "The HTML template file to be used by the exporter. The template format is documented at `seam-export-default-template-string'. See `seam-export-alist' for more information about specifying templates." :group 'seam-export :type '(choice file (const nil))) (defvar seam-export-default-template-string "<!doctype html> <html lang=\"en\"> <head> <meta charset=\"utf-8\" /> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" /> <title>{{title}}</title> </head> <body> <main> <header> <h1>{{title}}</h1> <p class=\"modified\">Last modified: <time datetime=\"{{modified-dt}}\">{{modified}}</time></p> </header> {{contents}} <section class=\"backlinks\"> <h1>Backlinks</h1> {{backlinks}} </section> </main> </body> </html>" "The default HTML template string if no other template is specified. It should be plain HTML5. Several variables are defined which can be interpolated using the {{variable}} syntax: `contents' The full HTML contents of the note, sans the title header. `title' The note's title (HTML-escaped). `backlinks' A list (<ul>) of notes that link to the given note. `modified' The human-readable date that the note was last modified. See `seam-export-time-format'. `modified-dt' The machine-readable date that the note was last modified. See `seam-export-time-format-datetime'.") (defcustom seam-export-template-string seam-export-default-template-string "The HTML template string to be used by the exporter. The template format is documented at `seam-export-default-template-string'. See `seam-export-alist' for more information about specifying templates." :group 'seam-export :type '(choice string (const nil))) (defcustom seam-export-time-format "%e %B %Y" "Human-readable format for template time strings. Passed to `format-time-string'." :group 'seam-export :type 'string) (defcustom seam-export-time-format-datetime "%Y-%m-%d" "Machine-readable format for template time strings. Meant to be used in the datetime attribute of <time>. Passed to `format-time-string'." :group 'seam-export :type 'string) (defcustom seam-export-time-zone t "Time zone used for template time strings. Passed to `format-time-string'." :group 'seam-export :type 'sexp) (defcustom seam-export-internal-link-class nil "CSS class name to use for internal links (i.e., links to other Seam notes)." :group 'seam-export :type 'string) (defvar seam-export-backend-options (list :html-container "article" :html-doctype "html5" :html-html5-fancy t :html-text-markup-alist '((bold . "<strong>%s</strong>") (code . "<code>%s</code>") (italic . "<em>%s</em>") (strike-through . "<s>%s</s>") (underline . "<span class=\"underline\">%s</span>") (verbatim . "<code>%s</code>")) :html-toplevel-hlevel 1 :html-use-infojs nil :section-numbers nil :time-stamp-file nil :with-smart-quotes t :with-toc nil)) (defmacro seam-export--to-string (&rest body) (declare (indent 0)) (let ((buf (gensym))) `(let ((,buf (generate-new-buffer " *seam-export*"))) (unwind-protect (progn (with-temp-buffer ,@body ;; This let prevents Org from popping up a window. (let ((org-export-show-temporary-export-buffer nil)) (org-export-to-buffer 'seam ,buf nil nil nil t seam-export--options nil))) (with-current-buffer ,buf (buffer-string))) (kill-buffer ,buf))))) ;;; Some HACK-ery to get fully escaped and smartquote-ized string. (defun seam-export--escape-string (s) (string-remove-prefix "<p>\n" (string-remove-suffix "</p>\n" (seam-export--to-string (insert s))))) (defun seam-export--replace-variable (var replacement) (goto-char 1) (while (re-search-forward (format "{{%s}}" var) nil t) (replace-match replacement))) (defun seam-export--generate-backlinks (file) (seam-export--to-string (let ((files (sort (let ((seam--subset seam-export--types)) (cl-loop for x in (seam-get-links-to-file file) collect (cons (seam-get-title-from-file x) x))) :key #'car :lessp #'string<))) (when files (cl-loop for (title . file) in files do (insert (format "- [[seam:%s][%s]]\n" (file-name-base file) title))))))) (defun seam-export--note-to-html (note-file html-directory) (seam-ensure-directory-exists html-directory) (let ((html-file (file-name-concat html-directory (concat (file-name-base note-file) ".html"))) (modified (file-attribute-modification-time (file-attributes note-file)))) (with-temp-buffer (insert seam-export--template) (seam-export--replace-variable "title" (seam-export--escape-string (seam-get-title-from-file note-file))) (seam-export--replace-variable "modified" (format-time-string seam-export-time-format modified seam-export-time-zone)) (seam-export--replace-variable "modified-dt" (format-time-string seam-export-time-format-datetime modified seam-export-time-zone)) (seam-export--replace-variable "contents" (seam-export--to-string (insert-file-contents note-file) (re-search-forward (org-headline-re 1)) (org-mode) ;Needed for `org-set-property'. (org-set-property "seam-title-p" "t"))) (seam-export--replace-variable "backlinks" (seam-export--generate-backlinks note-file)) (write-file html-file)))) (defun seam-export--file-string (file) (with-temp-buffer (insert-file-contents file) (buffer-string))) (defun seam-export-note (file) (let ((type (seam-get-note-type file))) (cl-loop for (dir . plist) in seam-export-alist do (let ((types (plist-get plist :types)) (template-file (plist-get plist :template-file)) (template-string (plist-get plist :template-string))) (unless types (error "You must specify :types for export")) (let ((template (cond (template-file (seam-export--file-string template-file)) (template-string template-string) (seam-export-template-file (seam-export--file-string seam-export-template-file)) (seam-export-template-string seam-export-template-string) (t (error "You must specify a template for export (see `seam-export-alist')"))))) (when (member type types) (let ((seam-export--types types) (seam-export--root-path (or (plist-get plist :root-path) "")) (seam-export--no-extension (plist-get plist :no-extension)) (seam-export--template template) (seam-export--internal-link-class (or (plist-get plist :internal-link-class) seam-export-internal-link-class)) (seam-export--options (org-combine-plists seam-export-backend-options (plist-get plist :backend-options)))) (seam-export--note-to-html file dir)))))))) (defun seam-export-all-notes () "Export all note files as HTML." (interactive) (unless seam-export-alist (error "Nothing to export. Please configure `seam-export-alist'.")) (dolist (dir (seam-note-subdirectories)) (dolist (file (directory-files dir t seam-note-file-regexp)) (seam-export-note file)))) (provide 'seam-export) ;;; seam-export.el ends here