;;; seam-html.el --- Seam HTML exporter -*- lexical-binding: t -*- ;; Copyright (C) 2025 Spencer Williams ;; Author: Spencer Williams ;; 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 . ;;; Commentary: ;; Seam's HTML exporter. ;; ;; This was blithely hacked together using large chunks of code lifted ;; straight from ox-html.el, and could do with much improvement. ;; ;; Original ox-html code is licensed under GPL v3+. Copyright (c) ;; 2011-2025 Free Software Foundation, Inc. Original authors: Carsten ;; Dominik and Jambunathan K . ;;; Code: (require 'ox-html) ;;; NOTE: This function does not respect `:headline-levels' or ;;; `:html-self-link-headlines'. (defun seam-html-headline (headline contents info) "Transcode a HEADLINE element from Org to HTML. CONTENTS holds the contents of the headline. INFO is a plist holding contextual information." (cond ((org-element-property :SEAM-TITLE-P headline) contents) ((org-element-property :footnote-section-p headline) nil) (t (let* ((level (+ (org-export-get-relative-level headline info) (1- (plist-get info :html-toplevel-hlevel)))) (text (org-export-data (org-element-property :title headline) info)) (contents (or contents "")) (id (org-html--reference headline info)) (todo (and (plist-get info :with-todo-keywords) (let ((todo (org-element-property :todo-keyword headline))) (and todo (org-export-data todo info))))) (todo-type (and todo (org-element-property :todo-type headline))) (priority (and (plist-get info :with-priority) (org-element-property :priority headline))) (tags (and (plist-get info :with-tags) (org-export-get-tags headline info))) (full-text (funcall (plist-get info :html-format-headline-function) todo todo-type priority text tags info))) (let ((headline-class (org-element-property :HTML_HEADLINE_CLASS headline))) (format "%s%s\n" (format "%s\n" level id (if (not headline-class) "" (format " class=\"%s\"" headline-class)) full-text level) contents)))))) (defun seam-html-section (_section contents _info) "Transcode a SECTION element from Org to HTML. CONTENTS holds the contents of the section." contents) (defvar seam-html-standalone-image-predicate) (defun seam-html-standalone-image-p (element info) "Non-nil if ELEMENT is a standalone image. INFO is a plist holding contextual information. An element or object is a standalone image when - its type is `paragraph' and its sole content, save for white spaces, is a link that qualifies as an inline image; - its type is `link' and its containing paragraph has no other content save white spaces. Bind `seam-html-standalone-image-predicate' to constrain paragraph further. For example, to check for only captioned standalone images, set it to: (lambda (paragraph) (org-element-property :caption paragraph))" (let ((paragraph (pcase (org-element-type element) (`paragraph element) (`link (org-element-parent element))))) (and (org-element-type-p paragraph 'paragraph) (or (not (and (boundp 'seam-html-standalone-image-predicate) (fboundp seam-html-standalone-image-predicate))) (funcall seam-html-standalone-image-predicate paragraph)) (catch 'exit (let ((link-count 0)) (org-element-map (org-element-contents paragraph) (cons 'plain-text org-element-all-objects) (lambda (obj) (when (pcase (org-element-type obj) (`plain-text (org-string-nw-p obj)) (`link (or (> (cl-incf link-count) 1) (not (org-html-inline-image-p obj info)))) (_ t)) (throw 'exit nil))) info nil 'link) (= link-count 1)))))) (defun seam-html-link (link desc info) "Transcode a LINK object from Org to HTML. DESC is the description part of the link, or the empty string. INFO is a plist holding contextual information. See `org-export-data'." (let* ((html-ext (plist-get info :html-extension)) (dot (when (> (length html-ext) 0) ".")) (link-org-files-as-html-maybe (lambda (raw-path info) ;; Treat links to `file.org' as links to `file.html', if ;; needed. See `org-html-link-org-files-as-html'. (save-match-data (cond ((and (plist-get info :html-link-org-files-as-html) (let ((case-fold-search t)) (string-match "\\(.+\\)\\.org\\(?:\\.gpg\\)?$" raw-path))) (concat (match-string 1 raw-path) dot html-ext)) (t raw-path))))) (link-type (org-element-property :type link)) (raw-path (org-element-property :path link)) ;; Ensure DESC really exists, or set it to nil. (desc (org-string-nw-p desc)) (path (cond ((string= "seam" link-type) (let ((slug raw-path)) (when-let ((file (seam-lookup-slug slug))) (let ((type (seam-get-note-type file))) (when (and (member type seam-export--types) (file-exists-p (seam-make-file-name slug type))) (concat seam-export--root-path slug (if seam-export--no-extension "" ".html"))))))) ((string= "file" link-type) ;; During publishing, turn absolute file names belonging ;; to base directory into relative file names. Otherwise, ;; append "file" protocol to absolute file name. (setq raw-path (org-export-file-uri (org-publish-file-relative-name raw-path info))) ;; Possibly append `:html-link-home' to relative file ;; name. (let ((home (and (plist-get info :html-link-home) (org-trim (plist-get info :html-link-home))))) (when (and home (plist-get info :html-link-use-abs-url) (not (file-name-absolute-p raw-path))) (setq raw-path (concat (file-name-as-directory home) raw-path)))) ;; Maybe turn ".org" into ".html". (setq raw-path (funcall link-org-files-as-html-maybe raw-path info)) ;; Add search option, if any. A search option can be ;; relative to a custom-id, a headline title, a name or ;; a target. (let ((option (org-element-property :search-option link))) (if (not option) raw-path (let ((path (org-element-property :path link))) (concat raw-path "#" (org-publish-resolve-external-link option path t)))))) (t (url-encode-url (concat link-type ":" raw-path))))) (attributes-plist (org-combine-plists ;; Extract attributes from parent's paragraph. HACK: Only ;; do this for the first link in parent (inner image link ;; for inline images). This is needed as long as ;; attributes cannot be set on a per link basis. (let* ((parent (org-element-parent-element link)) (link (let ((container (org-element-parent link))) (if (and (org-element-type-p container 'link) (org-html-inline-image-p link info)) container link)))) (and (eq link (org-element-map parent 'link #'identity info t)) (org-export-read-attribute :attr_html parent))) ;; Add Seam internal link class if appropriate. (when (and seam-export--internal-link-class (string= "seam" link-type)) (list :class seam-export--internal-link-class)) ;; Also add attributes from link itself. Currently, those ;; need to be added programmatically before `org-html-link' ;; is invoked, for example, by backends building upon HTML ;; export. (org-export-read-attribute :attr_html link))) (attributes (let ((attr (org-html--make-attribute-string attributes-plist))) (if (org-string-nw-p attr) (concat " " attr) "")))) (cond ;; Link type is handled by a special function. ((org-export-custom-protocol-maybe link desc 'html info)) ;; Image file. ((and (plist-get info :html-inline-images) (org-export-inline-image-p link (plist-get info :html-inline-image-rules))) (org-html--format-image path attributes-plist info)) ;; Radio target: Transcode target's contents and use them as ;; link's description. ((string= link-type "radio") (let ((destination (org-export-resolve-radio-link link info))) (if (not destination) desc (format "%s" (org-export-get-reference destination info) attributes desc)))) ;; Links pointing to a headline: Find destination and build ;; appropriate referencing command. ((member link-type '("custom-id" "fuzzy" "id")) (let ((destination (if (string= link-type "fuzzy") (org-export-resolve-fuzzy-link link info) (org-export-resolve-id-link link info)))) (pcase (org-element-type destination) ;; ID link points to an external file. (`plain-text (let ((fragment (concat org-html--id-attr-prefix raw-path)) ;; Treat links to ".org" files as ".html", if needed. (path (funcall link-org-files-as-html-maybe destination info))) (format "%s" path fragment attributes (or desc destination)))) ;; Fuzzy link points nowhere. (`nil (;; format "%s" identity (or desc (org-export-data (org-element-property :raw-link link) info)))) ;; Link points to a headline. (`headline (let ((href (org-html--reference destination info)) ;; What description to use? (desc ;; Case 1: Headline is numbered and LINK has no ;; description. Display section number. (if (and (org-export-numbered-headline-p destination info) (not desc)) (mapconcat #'number-to-string (org-export-get-headline-number destination info) ".") ;; Case 2: Either the headline is un-numbered or ;; LINK has a custom description. Display LINK's ;; description or headline's title. (or desc (org-export-data (org-element-property :title destination) info))))) (format "%s" href attributes desc))) ;; Fuzzy link points to a target or an element. (_ (if (and destination (memq (plist-get info :with-latex) '(mathjax t)) (org-element-type-p destination 'latex-environment) (eq 'math (org-latex--environment-type destination))) ;; Caption and labels are introduced within LaTeX ;; environment. Use "ref" or "eqref" macro, depending on user ;; preference to refer to those in the document. (format (plist-get info :html-equation-reference-format) (org-html--reference destination info)) (let* ((ref (org-html--reference destination info)) (seam-html-standalone-image-predicate #'org-html--has-caption-p) (counter-predicate (if (org-element-type-p destination 'latex-environment) #'org-html--math-environment-p #'org-html--has-caption-p)) (number (cond (desc nil) ((seam-html-standalone-image-p destination info) (org-export-get-ordinal (org-element-map destination 'link #'identity info t) info '(link) 'seam-html-standalone-image-p)) (t (org-export-get-ordinal destination info nil counter-predicate)))) (desc (cond (desc) ((not number) "No description for this link") ((numberp number) (number-to-string number)) (t (mapconcat #'number-to-string number "."))))) (format "%s" ref attributes desc))))))) ;; Coderef: replace link with the reference name or the ;; equivalent line number. ((string= link-type "coderef") (let ((fragment (concat "coderef-" (org-html-encode-plain-text raw-path)))) (format "%s" fragment (format "class=\"coderef\" onmouseover=\"CodeHighlightOn(this, \ '%s');\" onmouseout=\"CodeHighlightOff(this, '%s');\"" fragment fragment) attributes (format (org-export-get-coderef-format raw-path desc) (org-export-resolve-coderef raw-path info))))) ;; External link with a description part. ((and path desc) (format "%s" (org-html-encode-plain-text path) attributes desc)) ;; External link without a description part. (path (let ((path (org-html-encode-plain-text path))) (format "%s" path attributes path))) ;; No path, only description. (t desc)))) (defun seam-html-src-block (src-block _contents info) "Transcode a SRC-BLOCK element from Org to HTML. CONTENTS holds the contents of the item. INFO is a plist holding contextual information." (if (org-export-read-attribute :attr_html src-block :textarea) (org-html--textarea-block src-block) (let* ((lang (org-element-property :language src-block)) (code (org-html-format-code src-block info)) (label (let ((lbl (org-html--reference src-block info t))) (if lbl (format " id=\"%s\"" lbl) "")))) (format "
%s%s
" ;; Build caption. (let ((caption (org-export-get-caption src-block))) (if (not caption) "" (let ((listing-number (format "%s " (format (org-html--translate "Listing %d:" info) (org-export-get-ordinal src-block info nil #'org-html--has-caption-p))))) (format "" listing-number (org-trim (org-export-data caption info)))))) ;; Contents. (format "%s" ;; Lang being nil is OK. lang label code))))) (org-export-define-derived-backend 'seam 'html :translate-alist `((headline . seam-html-headline) (link . seam-html-link) (section . seam-html-section) (src-block . seam-html-src-block))) (provide 'seam-html) ;;; seam-html.el ends here