Publishing with org-mode
Intended for use with: Emacs configuration for writing.
org-mode works perfectly as a minimal static site generator.
(setq package-user-dir (expand-file-name "~/.config/emacs-writing/elpa"))
(package-initialize)
(require 's)
(require 'org)
(setq org-link-file-path-type 'relative)
(require 'ox)
(setq org-export-headline-levels 10)
(setq org-export-with-toc t)
(require 'org-contrib)
(require 'ox-extra)
(ox-extras-activate '(ignore-headlines))
(require 'ox-html)
(setq org-html-with-latex t)
;; (setq org-html-with-latex 'dvisvgm)
(setq org-html-doctype "html5"
org-html-html5-fancy t
org-html-prefer-user-labels t)
(require 'ox-publish)
(require 'oc)
(setq org-cite-global-bibliography
(mapcar 'file-truename
(directory-files-recursively "~/documents/bibliographies/" "\\.bib$")))
(require 'oc-csl)
(setq org-cite-export-processors
'((t . (csl "~/downloads/ieee.csl"))))
(require 'org-ref)
(require 'htmlize)
(setq org-html-htmlize-output-type 'css
org-html-head-include-default-style nil)
(defun my/org-html-eqref-export (path desc format info)
"Export `eqref:path` links to HTML, replacing text with the mapped equation number."
(cond
((eq format 'html)
(let* (;; 1. Look up the label (path) in our preprocessed hash table
(eqnum (when (boundp 'my/org-latex-eqnum-hash)
(gethash path my/org-latex-eqnum-hash)))
;; 2. Set anchor text: (eqnum) if found, otherwise fallback to (??)
(anchor-text (if eqnum
(format "(%s)" eqnum)
"(???)")))
;; 3. Return the formatted HTML link
(format "<a href=\"#eqnum-%s\" class=\"eqref\">%s</a>" path anchor-text)))
;; For LaTeX export, we just let the native \eqref macro handle it
((eq format 'latex)
(format "\\eqref{%s}" path))
;; Fallback for other export backends (like plain text)
(t (or desc (format "(%s)" path)))))
;; Register the link parameter as before
(org-link-set-parameters "eqref"
:follow (lambda (path _) (org-link-search path))
:export #'my/org-html-eqref-export)
;; (defun my/org-html-eqref-export (path desc format info)
;; "Export `eqref:path` links to HTML as <a href=\"#path\">↑</a> or custom description."
;; (cond
;; ((eq format 'html)
;; (let ((anchor-text (or desc "(↑)")))
;; (format "<a href=\"#%s\" class=\"eqref\">%s</a>" path anchor-text)))
;; ((eq format 'latex)
;; (format "\\eqref{%s}" path))
;; (t (or desc path))))
;; (org-link-set-parameters "eqref"
;; :follow (lambda (path _) (org-link-search path))
;; :export #'my/org-html-eqref-export)
(require 'org-mem)
(setq org-mem-watch-dirs '("~/giabao.xyz/org/")
org-mem-do-look-everywhere nil
org-mem-do-sync-with-org-id t)
(org-mem-updater-mode)
(require 'org-mem-roamy)
(org-mem-roamy-db-mode)
(require 'org-node)
(org-node-cache-mode)
(require 'org-node-backlink)
(setq org-node-backlink-drawer-formatter
#'org-node-backlink-format-as-bullet-with-time)
(defun org-node-backlink--fix-nearby-drawer (&optional remove)
"Update nearby backlinks drawer so it reflects current reality.
If REMOVE non-nil, remove it instead."
(if remove
(org-node--delete-drawer "BACKLINKS")
(let* ((id (org-entry-get nil "ID"))
(entry (org-mem-entry-by-id id))
(portal-ids (mapcar #'car
(emacsql (org-mem-roamy-db)
[:select node_id
:from tags
:where (in tag $v1)]
(vconcat org-excluded-tags))))
(all-origin-ids (when entry
(thread-last
(append (org-mem-id-links-to-entry entry)
(org-mem-roam-reflinks-to-entry entry))
(seq-keep #'org-mem-link-nearby-id)
(delete id)
(delete-dups)
;; Shouldn't be necessary if tables are correct, but
;; don't assume `org-mem-updater-mode' is flawless
(seq-filter #'org-mem-entry-by-id))))
(origin-ids (seq-difference all-origin-ids portal-ids))
(org-node-backlink--inhibit-flagging t))
(if (null origin-ids)
(org-node--delete-drawer "BACKLINKS")
(save-excursion
(save-restriction
(org-node-narrow-to-drawer-create
"BACKLINKS" org-node-backlink-drawer-positioner)
(let* ((col (current-indentation))
(lines (split-string (buffer-string) "\n" t))
(already-present-ids
(seq-keep #'org-node-backlink--extract-id lines))
(to-add (seq-difference origin-ids already-present-ids))
(to-remove (seq-difference already-present-ids origin-ids))
(to-reformat (seq-intersection already-present-ids origin-ids)))
;; Add new, remove stale, reformat the rest
(dolist (id to-remove)
(save-excursion
(search-forward id)
(delete-line)))
(dolist (id to-reformat)
(save-excursion
(search-forward id)
(back-to-indentation)
(let* ((line (buffer-substring-no-properties (point) (pos-eol)))
(reformatted (org-node-backlink--reformat-line line)))
(unless (equal line reformatted)
(atomic-change-group
(delete-region (point) (pos-eol))
(insert reformatted))))))
(dolist (id to-add)
(let ((title (org-mem-title-maybe (org-mem-entry-by-id id))))
(insert (funcall org-node-backlink-drawer-formatter id title))
(newline)
(indent-to col)))
;; Membership is correct, now re-sort
(let ((sorted-lines (sort (split-string (buffer-string) "\n" t)
org-node-backlink-drawer-sorter)))
(when org-node-backlink-drawer-sort-in-reverse
(setq sorted-lines (nreverse sorted-lines)))
(unless (equal lines sorted-lines)
(atomic-change-group
(delete-region (point-min) (point-max))
(insert (string-join sorted-lines "\n"))))))))))))
1. org-mode
(defvar org-excluded-tags)
(setq org-excluded-tags '("portal"))
2. KaTeX
Disable MathJax! Use KaTex sparingly!
(setq org-html-mathjax-template "")
Re-define some functions for more expressive sectioning of the page.
(defun org-html-latex-environment (latex-environment _contents info)
"Transcode a LATEX-ENVIRONMENT element from Org to HTML.
CONTENTS is nil. INFO is a plist holding contextual information."
(let ((processing-type (plist-get info :with-latex))
(latex-frag (org-remove-indentation
(org-element-property :value latex-environment)))
(label (org-html--reference latex-environment info t)))
(if (memq processing-type '(t mathjax))
(let ((rendered (org-html--as-latex latex-environment info latex-frag)))
(if (and (org-string-nw-p label) label)
;; (format "<div data-eqnum=\"%d\" id=\"eqnum-%s\" class=\"latex-environment\">\n%s\n</div>"
(format "<div style=\"--eqnum: '%d';\" id=\"eqnum-%s\" class=\"latex-environment\">\n%s\n</div>"
(gethash label my/org-latex-eqnum-hash) label rendered)
rendered))
(org-html--wrap-latex-environment
(org-html--as-latex latex-environment info latex-frag)
label))))
2.1. Labelling and referecing KaTeX equations
3. siunitx replacement
(defun my/siunitx-format-number (num-str)
"Convert numbers and scientific notation like '-3.14e6' to LaTeX math notation.
Handles negative numbers, decimals, and plain numbers without exponents."
(save-match-data
;; Regex breakdown:
;; ^\\([-+]?[0-9.]+\\) -> Group 1: The base number (optional sign, digits/decimals)
;; \\(?:[eE]\\([-+]?[0-9]+\\)\\)?$ -> Group 2: The exponent (optional sign, digits).
;; The \\(?:...\\)? makes the whole 'e' block optional.
(if (string-match "^\\([-+]?[0-9.]+\\)\\(?:[eE]\\([-+]?[0-9]+\\)\\)?$" num-str)
(let ((base (match-string 1 num-str))
(exp (match-string 2 num-str)))
(if exp
;; If there is an 'e' part, format as scientific notation
(format "%s \\times 10^{%s}" base exp)
;; If there is no 'e' part, just return the base number
base))
;; Fallback: If it's not a recognized number at all, return it unchanged
num-str)))
(defvar my/siunitx-unit-mapping
'(;; Prefixes
("femto" . "f")
("pico" . "p")
("nano" . "n")
("micro" . "\\mu ")
("milli" . "m")
("centi" . "c")
("deci" . "d")
("kilo" . "k")
("mega" . "M")
("giga" . "G")
("tera" . "T")
;; Base Units
("second" . "s")
("gram" . "g")
("meter" . "m")
("hertz" . "Hz")
("joule" . "J")
("newton" . "N")
("watt" . "W")
("volt" . "V")
("ampere" . "A")
("celsius". "^{\\circ}C")
;; Modifiers (Very handy!)
("per" . "/")
("squared". "^2")
("cubic" . "^3")
;; Unit abbreviations
("MeV" . "MeV")
)
"Dictionary mapping siunitx macros (without backslash) to their replacements.")
(defun my/siunitx-format-unit (unit-str)
"Convert siunitx macros to mathrm strings in a single pass using a dictionary."
(replace-regexp-in-string
"\\\\\\([A-Za-z]+\\)"
(lambda (match)
;; `match` is the matched string (e.g., "\femto").
;; `(substring match 1)` strips the leading backslash, giving "femto".
(let* ((key (substring match 1))
(val (cdr (assoc key my/siunitx-unit-mapping))))
;; If we found a replacement in the table, return it.
;; If not, fallback to returning the key (stripping the backslash anyway).
(if val val key)))
unit-str
t t)) ;; 't t' ensures the replacement is treated as a literal string
(defun my/siunitx-html-filter (text backend info)
"Filter to replace siunitx commands in LaTeX fragments during HTML export."
(let ((modified nil)
(orig-text text))
;; 1. Process \qty{num}{unit}
;; FIX: \\qty in a string evaluates to \qty. We need \\\\qty to search for literal \qty.
(when (string-match "\\\\qty{" text)
(setq text (replace-regexp-in-string
"\\\\qty{\\([^}]+\\)}{\\([^}]+\\)}"
(lambda (m)
(format "%s \\, \\mathrm{%s}"
(my/siunitx-format-number (match-string 1 m))
(my/siunitx-format-unit (match-string 2 m))))
text nil t))
(setq modified t))
;; 2. Process \num{num}
(when (string-match "\\\\num{" text)
(setq text (replace-regexp-in-string
"\\\\num{\\([^}]+\\)}"
(lambda (m)
(my/siunitx-format-number (match-string 1 m)))
text nil t))
(setq modified t))
;; 3. Process \unit{unit}
(when (string-match "\\\\unit{" text)
(setq text (replace-regexp-in-string
"\\\\unit{\\([^}]+\\)}"
(lambda (m)
(format "\\mathrm{%s}"
(my/siunitx-format-unit (match-string 1 m))))
text nil t))
(setq modified t))
;; Smart Math Wrapper:
(if (and modified
(not (string-match-p "^\\s-*\\(\\\\[([]\\|\\$\\|\\\\begin{\\)" orig-text)))
(format "\\( %s \\)" text)
text)))
;; Add our filter function to the LaTeX fragment filters
;; (add-to-list 'org-export-filter-plain-text-functions 'my/siunitx-html-filter)
(add-to-list 'org-export-filter-latex-fragment-functions 'my/siunitx-html-filter)
(add-to-list 'org-export-filter-latex-environment-functions 'my/siunitx-html-filter)
4. org-publish
(defvar org-publish-localhost)
(defvar org-publish-dir)
(defvar org-publish-url)
(setq org-publish-localhost nil)
(setq org-publish-dir "/home/bao/giabao.xyz/")
(if (equal org-publish-localhost t)
(setq org-publish-url "http://localhost:8080")
(setq org-publish-url "https://giabao.xyz"))
(if (equal org-publish-localhost t)
(setq org-link-abbrev-alist
'(("img" . "http://localhost:8080/img/%s")
("vid" . "http://localhost:8080/vid/%s")))
(setq org-link-abbrev-alist
'(("img" . "https://giabao.xyz/img/%s")
("vid" . "https://giabao.xyz/vid/%s"))))
4.1. Preprocessing
(defvar my/org-latex-eqnum-hash nil
"Hash table mapping Org LaTeX environment names to equation numbers.")
(defun my/org-preprocess-latex-equations (&optional backend)
"Scan the current Org buffer for labeled latex-environments and assign equation numbers.
Only equations with a `#+name:` label are counted and stored in the hash table.
BACKEND is accepted so this can be attached to `org-export-before-parsing-hook'."
;; Initialize/clear the hash table
(setq my/org-latex-eqnum-hash (make-hash-table :test 'equal))
(let ((counter 1)
;; Parse the current buffer into an abstract syntax tree (AST)
(tree (org-element-parse-buffer)))
;; Map over all 'latex-environment elements in the AST
(org-element-map tree 'latex-environment
(lambda (env)
(let ((label (org-element-property :name env)))
;; ONLY proceed if the equation actually has a #+name: label
(when label
;; Push the pair (label, counter) to the hash table
(puthash label counter my/org-latex-eqnum-hash)
;; Increment the counter for the next *labeled* environment
(setq counter (1+ counter)))))))
;; Return the hash table purely for convenience/debugging
my/org-latex-eqnum-hash)
(add-hook 'org-export-before-parsing-functions #'my/org-preprocess-latex-equations)
(defun my/org-html-proof-block-filter (text backend info)
"Convert #+begin_proof HTML blocks to <details class=\"proof\">."
(when (org-export-derived-backend-p backend 'html)
(replace-regexp-in-string
;; Match opening <div ... class="proof" ...> with optional whitespace,
;; OR closing </div> with optional whitespace before end-of-string.
"\\`<div[^>]*class=\"proof\"[^>]*>[ \t\n]*\\|</div>[ \t\n]*\\'"
(lambda (match)
(if (string-prefix-p "<div" match)
"<details class=\"proof\">\n<summary class=\"proof-text\">Proof</summary>\n<div class=\"proof-content\">\n"
"</div>\n</details>\n"))
text)))
(add-hook 'org-export-filter-special-block-functions
#'my/org-html-proof-block-filter)
(defun my/org-html-algorithm-block-filter (text backend info)
"Convert HTML <div class=\"algorithm\"> blocks to <pre class=\"algorithm\"> with \\begin{algorithm}."
(when (org-export-derived-backend-p backend 'html)
(replace-regexp-in-string
"<div[^>]*class=\"algorithm\"[^>]*>\\(\\(?:.\\|\n\\)*?\\)\n</div>"
(lambda (match)
(format "<pre class=\"algorithm\">\n\\begin{algorithm}%s\\end{algorithm}</pre>"
(match-string 1 match)))
text nil 'literal)))
(add-hook 'org-export-filter-special-block-functions
#'my/org-html-algorithm-block-filter)
(defun my/org-preprocess-block-title (backend)
"Preprocess custom block's title."
(when (org-export-derived-backend-p backend 'html)
(goto-char (point-min))
(while (re-search-forward "^#\\+begin_\\([a-z]+\\) \\[\\([a-zA-Z0-9,='(/)\{\}\\ -]+\\)\\]" nil t)
(let ((env (match-string 1))
(name (match-string 2)))
(replace-match "")
(insert (format "#+attr_html: :title %s\n#+begin_%s" name env))))))
(add-hook 'org-export-before-processing-functions
#'my/org-preprocess-block-title)
(defun my/org-export-format-backlinks-drawer (backend)
"Preprocess :BACKLINKS: drawer."
(when (org-export-derived-backend-p backend 'html)
(save-excursion
(goto-char (point-min))
;; Search for :BACKLINKS: drawer syntax
(while (re-search-forward "^[ \t]*:BACKLINKS:[ \t]*$" nil t)
(let ((drawer-start (match-beginning 0))
(contents-start (line-beginning-position 2))
(drawer-end (re-search-forward "^[ \t]*:END:[ \t]*$" nil t)))
(when drawer-end
(let ((contents (buffer-substring-no-properties contents-start (match-beginning 0))))
;; Replace drawer with a custom block
(delete-region drawer-start (point))
(goto-char (point-max))
(insert (format "* Backlinks
:PROPERTIES:
:UNNUMBERED: notoc
:END:
%s" contents)))))))))
(add-hook 'org-export-before-processing-functions
#'my/org-export-format-backlinks-drawer)
(defun my/org-html-katex-options (backend)
"#+katex_options: pseudocode:t"
(when (org-export-derived-backend-p backend 'html)
(save-excursion
(goto-char (point-min))
(when (re-search-forward
(rx line-start "#+katex_options:" (0+ space) "pseudocode:t")
nil t)
(beginning-of-line 2)
(insert "#+html_head_extra: <link rel=\"stylesheet\" href=\"../css/pseudocode.css\">\n")
(insert "#+html_head_extra: <script src=\"../js/pseudocode.js\"></script>\n\n")
(goto-char (point-max))
(insert "\n@@html:<script>pseudocode.renderElement(document.getElementsByClassName(\"algorithm\")[0],
{ lineNumber: true });</script>@@\n")))))
(add-hook 'org-export-before-processing-functions
#'my/org-html-katex-options)
4.2. Content processing
(defun my/org-html--build-head (plist)
(let* ((dir (expand-file-name (plist-get plist :publishing-directory)))
(org-css (file-relative-name "html/css/org.css" dir))
(math-css (file-relative-name "html/css/math.css" dir))
(folding-js (file-relative-name "html/js/folding.js" dir)))
(concat
(format "<link rel=\"stylesheet\" href=\"%s\" type=\"text/css\" />
<link rel=\"stylesheet\" href=\"%s\" type=\"text/css\" />
<script src=\"%s\" defer></script>\n" org-css math-css folding-js)
;; "<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">
;; <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>
;; <link href=\"https://fonts.googleapis.com/css2?family=League+Spartan:wght@100..900&display=swap\" rel=\"stylesheet\">"
"<style>"
"/* vietnamese */
@font-face {
font-family: 'League Spartan';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/leaguespartan/v15/kJEqBuEW6A0lliaV_m88ja5TwvZ_LZmFDA.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'League Spartan';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/leaguespartan/v15/kJEqBuEW6A0lliaV_m88ja5TwvZ-LZmFDA.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'League Spartan';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/leaguespartan/v15/kJEqBuEW6A0lliaV_m88ja5TwvZwLZk.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
"
;; "<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">
;; <link rel=\"preconnepct\" href=\"https://fonts.gstatic.com\" crossorigin>
;; <link href=\"https://fonts.googleapis.com/css2?family=League+Spartan:wght@100..900&family=Libre+Baskerville:ital,wght@0,400..700;1,400..700&display=swap\" rel=\"stylesheet\">"
"/* vietnamese */
@font-face {
font-family: 'League Spartan';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/leaguespartan/v15/kJEqBuEW6A0lliaV_m88ja5TwvZ_LZmFDA.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'League Spartan';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/leaguespartan/v15/kJEqBuEW6A0lliaV_m88ja5TwvZ-LZmFDA.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'League Spartan';
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url(https://fonts.gstatic.com/s/leaguespartan/v15/kJEqBuEW6A0lliaV_m88ja5TwvZwLZk.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Libre Baskerville';
font-style: italic;
font-weight: 400 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/librebaskerville/v24/kmKhZrc3Hgbbcjq75U4uslyuy4kn0qNcWx8QDP2V.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Libre Baskerville';
font-style: italic;
font-weight: 400 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/librebaskerville/v24/kmKhZrc3Hgbbcjq75U4uslyuy4kn0qNcWxEQDA.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* latin-ext */
@font-face {
font-family: 'Libre Baskerville';
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/librebaskerville/v24/kmKnZrc3Hgbbcjq75U4uslyuy4kn0qNXaxMICA.woff2) format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Libre Baskerville';
font-style: normal;
font-weight: 400 700;
font-display: swap;
src: url(https://fonts.gstatic.com/s/librebaskerville/v24/kmKnZrc3Hgbbcjq75U4uslyuy4kn0qNZaxM.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
"
"</style>"
;; "<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/katex@0.18.4/dist/katex.min.css\" integrity=\"sha384-u1zONI5gPXUx0UKI62c75/zww972y0v2rSK5ZYlVdS6xEuWDeZWUI66v6t1gvlXJ\" crossorigin=\"anonymous\">
;; <script src=\"https://cdn.jsdelivr.net/npm/katex@0.18.4/dist/katex.min.js\" integrity=\"sha384-ykMNcWQhhTUb0YV9SPpPUFURHZ+tWmubkakGBP+OgNK/UXdO2gtzglWx0Rj9hnO3\" crossorigin=\"anonymous\"></script>
;; <script src=\"https://cdn.jsdelivr.net/npm/katex@0.18.4/dist/contrib/auto-render.min.js\" integrity=\"sha384-bjyGPfbij8/NDKJhSGZNP/khQVgtHUE5exjm4Ydllo42FwIgYsdLO2lXGmRBf5Mz\" crossorigin=\"anonymous\"></script>
;; <script>
;; document.addEventListener('DOMContentLoaded', function() {
;; renderMathInElement(document.body, {
;; macros: {
;; \"\\\\arccot\": \"\\\\operatorname{arccot}\"
;; }
;; });
;; });
;; </script>\n"
;; "<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/pseudocode@2.4.1/build/pseudocode.min.css\">
;; <script src=\"https://cdn.jsdelivr.net/npm/pseudocode@2.4.1/build/pseudocode.min.js\"></script>\n"
)))
(defun org-html--tags (tags info)
"Format TAGS into HTML.
INFO is a plist containing export options."
(when tags
(format "<span class=\"tag\">%s</span>"
(mapconcat
(lambda (tag)
(format "<span class=\"tag-%s\">%s</span>"
(concat (plist-get info :html-tag-class-prefix)
(org-html-fix-class-name tag))
tag))
tags " "))))
4.3. Postprocessing
(defun my/org-export-filetags-inject (text backend info)
"Extract #+FILETAGS and inject an HTML string into the final output."
(if (org-export-derived-backend-p backend 'html)
(let* ((all-tags (org-node--get-filetags))
(tags (seq-difference all-tags org-excluded-tags)))
;; Use org-node--get-filetags
(if tags
(let ((tags-html (concat "<div id=\"filetags\">\n"
"<ul id=\"filetags-list\"><li id=\"filetags-text\">Tags:</li>"
(mapconcat (lambda (tag)
(format "<li><a href=\"%s/tags/tag-%s.html\" class=\"tag\">#%s</a></li>"
org-publish-url tag tag))
tags "\n")
"</ul>"
"</div>\n")))
(replace-regexp-in-string "</h1>" (concat "</h1>\n" tags-html) text))
text))
text))
(defun my/org-export-html-link (text backend info)
"Filter for HTML link export: strip the directory from `src' attributes."
(when (org-export-derived-backend-p backend 'html)
(let ((dir (expand-file-name (plist-get info :publishing-directory))))
(replace-regexp-in-string
"src=\".*\\(ltximg/org-tex-.*\\.svg\\)\""
"src=\"\\1\""
text
t
nil
))))
(setq org-export-filter-final-output-functions
'(my/org-export-filetags-inject))
;; my/org-export-html-link
4.4. Template
(defun org-html--build-navbar (info)
"Return navbar HTML for exported document.
INFO is a plist used as a communication channel."
(let* ((file (file-name-base
(plist-get info :input-file)))
(dir (file-name-nondirectory
(directory-file-name
(file-name-directory (plist-get info :input-file)))))
(buttons '("home" "journal" "notes"))
(buttons-html
(mapconcat
(lambda (button)
(if (or (string-equal-ignore-case button file) (string-equal-ignore-case button dir))
(format " <li class=\"navbar-active\">\n <a class=\"navbar-link\" href=\"%s/%s.html\">%s</a>\n </li>"
org-publish-url button (s-capitalize button))
(format " <li class=\"navbar-item\">\n <a class=\"navbar-link\" href=\"%s/%s.html\">%s</a>\n </li>"
org-publish-url button (s-capitalize button))))
buttons "\n")))
(concat
(format "<div id=\"navbar\">
<ul class=\"navbar-left\">
%s
</ul>" buttons-html)
(format "<ul class=\"navbar-right\">
<li class=\"navbar-item\">
<a class=\"navbar-link\" href=\"%s/search.html\">Search</a>
</li>
</ul>
</div>" org-publish-url))))
(defun org-html-toc (depth info &optional scope)
"Build a table of contents.
DEPTH is an integer specifying the depth of the table. INFO is
a plist used as a communication channel. Optional argument SCOPE
is an element defining the scope of the table. Return the table
of contents as a string, or nil if it is empty."
(let ((toc-entries
(mapcar (lambda (headline)
(cons (org-html--format-toc-headline headline info)
(org-export-get-relative-level headline info)))
(org-export-collect-headlines info depth scope))))
(when toc-entries
(let* ((toc-id-counter (plist-get info :org-html--toc-counter))
(toc (concat (format "<div id=\"text-table-of-contents%s\" role=\"doc-toc\">"
(if toc-id-counter (format "-%d" toc-id-counter) ""))
(org-html--toc-text toc-entries scope)
"</div>\n")))
(plist-put info :org-html--toc-counter (1+ (or toc-id-counter 0)))
(if scope toc
(concat (format "<details open id=\"table-of-contents%s\" role=\"doc-toc\">\n"
(if toc-id-counter (format "-%d" toc-id-counter) ""))
"<summary>"
(org-html--translate "Table of Contents" info)
"</summary>"
toc
"</details>\n"))))))
(defun org-html-inner-template (contents info)
"Return body of document string after HTML conversion.
CONTENTS is the transcoded contents string. INFO is a plist
holding export options."
(concat
;; Document contents.
contents
;; Footnotes section.
(org-html-footnote-section info)))
(defun my/org-html--build-katex (plist)
"Insert the user setup into the KaTeX template.
INFO is a plist used as a communication channel."
(concat "<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/katex@0.18.4/dist/katex.min.css\" integrity=\"sha384-u1zONI5gPXUx0UKI62c75/zww972y0v2rSK5ZYlVdS6xEuWDeZWUI66v6t1gvlXJ\" crossorigin=\"anonymous\">
<script defer src=\"https://cdn.jsdelivr.net/npm/katex@0.18.4/dist/katex.min.js\" integrity=\"sha384-ykMNcWQhhTUb0YV9SPpPUFURHZ+tWmubkakGBP+OgNK/UXdO2gtzglWx0Rj9hnO3\" crossorigin=\"anonymous\"></script>
<script defer src=\"https://cdn.jsdelivr.net/npm/katex@0.18.4/dist/contrib/auto-render.min.js\" integrity=\"sha384-bjyGPfbij8/NDKJhSGZNP/khQVgtHUE5exjm4Ydllo42FwIgYsdLO2lXGmRBf5Mz\" crossorigin=\"anonymous\"></script>
<script defer>
document.addEventListener('DOMContentLoaded', function() {
renderMathInElement(document.body, {
macros: {
\"\\\\arccot\": \"\\\\operatorname{arccot}\",
}
});
});
</script>\n"
))
(defun my/org-html-template (contents info)
"Return complete document string after HTML conversion.
CONTENTS is the transcoded contents string. INFO is a plist
holding export options."
(concat
(when (and (not (org-html-html5-p info)) (org-html-xhtml-p info))
(let* ((xml-declaration (plist-get info :html-xml-declaration))
(decl (or (and (stringp xml-declaration) xml-declaration)
(cdr (assoc (plist-get info :html-extension)
xml-declaration))
(cdr (assoc "html" xml-declaration))
"")))
(when (not (or (not decl) (string= "" decl)))
(format "%s\n"
(format decl
(or (and org-html-coding-system
(coding-system-get org-html-coding-system :mime-charset))
"iso-8859-1"))))))
(org-html-doctype info)
"\n"
(concat "<html"
(cond ((org-html-xhtml-p info)
(format
" xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"%s\" xml:lang=\"%s\""
(plist-get info :language) (plist-get info :language)))
((org-html-html5-p info)
(format " lang=\"%s\"" (plist-get info :language))))
">\n")
"<head>\n"
(org-html--build-meta-info info)
(org-html--build-head info)
(my/org-html--build-katex info)
"</head>\n"
"<body>\n"
(org-html--build-navbar info)
(let ((link-up (org-trim (plist-get info :html-link-up)))
(link-home (org-trim (plist-get info :html-link-home))))
(unless (and (string= link-up "") (string= link-home ""))
(format (plist-get info :html-home/up-format)
(or link-up link-home)
(or link-home link-up))))
;; Preamble.
(org-html--build-pre/postamble 'preamble info)
;; Document contents.
"<div id=\"content-container\">"
(let ((div (assq 'content (plist-get info :html-divs))))
(format "<%s id=\"%s\" class=\"%s\">\n"
(nth 1 div)
(nth 2 div)
(plist-get info :html-content-class)))
;; Document title.
(when (plist-get info :with-title)
(let ((title (and (plist-get info :with-title)
(plist-get info :title)))
(subtitle (plist-get info :subtitle))
(html5-fancy (org-html--html5-fancy-p info)))
(when title
(format
(if html5-fancy
"<header>\n<h1 class=\"title\">%s</h1>\n%s</header>"
"<h1 class=\"title\">%s%s</h1>\n")
(org-export-data title info)
(if subtitle
(format
(if html5-fancy
"<p class=\"subtitle\" role=\"doc-subtitle\">%s</p>\n"
(concat "\n" (org-html-close-tag "br" nil info) "\n"
"<span class=\"subtitle\">%s</span>\n"))
(org-export-data subtitle info))
"")))))
;; Table of contents.
(let ((depth (plist-get info :with-toc)))
(when depth (org-html-toc depth info)))
contents
(format "</%s>\n" (nth 1 (assq 'content (plist-get info :html-divs))))
"</div>"
;; Postamble.
"<div id=\"postamble-container\">"
(org-html--build-pre/postamble 'postamble info)
"</div>"
;; Possibly use the Klipse library live code blocks.
(when (plist-get info :html-klipsify-src)
(concat "<script>" (plist-get info :html-klipse-selection-script)
"</script><script src=\""
org-html-klipse-js
"\"></script><link rel=\"stylesheet\" type=\"text/css\" href=\""
org-html-klipse-css "\"/>"))
;; Spacer to prevent view jumping
"<div id=\"spacer\" style=\"height:50vh;\"></div>"
"<div id=\"folding\">
<div id=\"statePills\" class=\"pill-group\">
<span class=\"pill\" data-state=\"overview\">Overview</span>
<span class=\"pill\" data-state=\"contents\">Contents</span>
<span class=\"pill pill-active\" data-state=\"show-all\">Show All</span>
</div>
</div>\n"
;; <button id=\"cycleBtn\">Cycle visibility (Overview)</button>
;; Closing document.
"</body>\n</html>\n"))
(defun my/org-html-headline-transcoder (headline contents info)
"Transcode level 1 HEADLINE into a details/summary block."
(let ((level (1+ (org-element-property :level headline))))
;; Check if it is a top-level (first level) heading
(let* ((title (org-export-data (org-element-property :title headline) info))
(tags (and (plist-get info :with-tags)
(org-export-get-tags headline info)))
(ftags (concat "["
(mapconcat
(lambda (tag)
(format "%s" tag))
tags ", ")
"]")))
(if (equal tags nil)
(format "\n<details class=\"org-details\" open>\n<summary class=\"org-summary\"><h%d>%s</h%d></summary>\n%s\n</details>\n"
level title level (or contents ""))
(format "\n<details class=\"org-details\" open>\n<summary class=\"org-summary\"><span class=\"org-summary-content\"><h%d>%s</h%d></span><span class=\"org-summary-tags\">%s</span></summary>\n%s\n</details>\n"
level title level ftags (or contents ""))))))
(defun my/org-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."
(unless (org-element-property :footnote-section-p headline)
(let* ((numberedp (org-export-numbered-headline-p headline info))
(numbers (org-export-get-headline-number headline info))
(level (+ (org-export-get-relative-level headline info)
(1- (plist-get info :html-toplevel-hlevel))))
(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)))
(text (org-export-data (org-element-property :title headline) info))
(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))
(contents (or contents ""))
(id (org-html--reference headline info))
(formatted-text
(if (plist-get info :html-self-link-headlines)
(format "<a href=\"#%s\">%s</a>" id full-text)
full-text)))
(if (org-export-low-level-p headline info)
;; This is a deep sub-tree: export it as a list item.
(let* ((html-type (if numberedp "ol" "ul")))
(concat
(and (org-export-first-sibling-p headline info)
(apply #'format "<%s class=\"org-%s\">\n"
(make-list 2 html-type)))
(org-html-format-list-item
contents (if numberedp 'ordered 'unordered)
nil info nil
(concat (org-html--anchor id nil nil info) formatted-text)) "\n"
(and (org-export-last-sibling-p headline info)
(format "</%s>\n" html-type))))
;; Standard headline. Export it as a section.
(let ((extra-class
(org-element-property :HTML_CONTAINER_CLASS headline))
(headline-class
(org-element-property :HTML_HEADLINE_CLASS headline))
(first-content (car (org-element-contents headline))))
(format "<details id=\"%s\" class=\"%s\" open>%s%s</details>\n"
(format "outline-container-%s" id)
(concat (format "outline-%d" level)
(and extra-class " ")
extra-class)
(format "\n<summary id=\"%s\"%s>%s</summary>\n"
id
(if (not headline-class) ""
(format " class=\"%s\"" headline-class))
(concat
(and numberedp
(format
"<span class=\"section-number-%d\">%s</span> "
level
(concat (mapconcat #'number-to-string numbers ".") ".")))
formatted-text))
;; When there is no section, pretend there is an
;; empty one to get the correct <div
;; class="outline-...> which is needed by
;; `org-info.js'.
(if (org-element-type-p first-content 'section) contents
(concat (org-html-section first-content "" info) contents))))))))
(org-export-define-derived-backend 'my/html 'html
:translate-alist '(;; (headline . my/org-html-headline)
(template . my/org-html-template)))
(defun my/org-html-publish-to-html (plist filename pub-dir)
"Publish an org file to HTML.
FILENAME is the filename of the Org file to be published. PLIST
is the property list for the given project. PUB-DIR is the
publishing directory.
Return output file name."
(org-publish-org-to 'my/html filename
(concat (when (> (length org-html-extension) 0) ".")
(or (plist-get plist :html-extension)
org-html-extension
"html"))
plist pub-dir))
4.5. Journal-specific export
(defun my/org-html-journal-headline-transcoder (headline contents info)
"Transcode level 1 HEADLINE into a details/summary block."
(let ((level (org-element-property :level headline)))
;; Check if it is a top-level (first level) heading
(if (= level 1)
(let* ((title (org-export-data (org-element-property :title headline) info))
(tags (and (plist-get info :with-tags)
(org-export-get-tags headline info)))
(ftags (concat "["
(mapconcat
(lambda (tag)
(format "%s" tag))
tags ", ")
"]")))
(if (equal tags nil)
(format "\n<details class=\"org-details\" open>\n<summary class=\"org-summary\">%s</summary>\n%s\n</details>\n"
title (or contents ""))
(format "\n<details class=\"org-details\" open>\n<summary class=\"org-summary\"><span class=\"org-summary-content\">%s</span><span class=\"org-summary-tags\">%s</span></summary>\n%s\n</details>\n"
title ftags (or contents ""))))
;; Fall back to standard HTML export behavior for levels 2+
(org-export-with-backend 'html headline contents info))))
(org-export-define-derived-backend 'my/html-journal 'html
:translate-alist '((headline . my/org-html-journal-headline-transcoder)
(template . my/org-html-template)))
(defun my/org-html-publish-journal-to-html (plist filename pub-dir)
"Publish an org file to HTML.
FILENAME is the filename of the Org file to be published. PLIST
is the property list for the given project. PUB-DIR is the
publishing directory.
Return output file name."
(org-publish-org-to 'my/html-journal filename
(concat (when (> (length org-html-extension) 0) ".")
(or (plist-get plist :html-extension)
org-html-extension
"html"))
plist pub-dir))
4.6. org-node backlink sync
(cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t))
((symbol-function 'y-or-n-p) (lambda (&rest _) t))
(use-dialog-box nil)
(inhibit-message t)) ; Optional: suppresses annoying output
(ignore-error user-error
(org-node-backlink-mass-update-drawers)))
(org-save-all-org-buffers)
(defun my/org-mem-export-compressed-json (output-file)
"Dump `org-mem` structures into a lean JSON file for client-side web search."
(interactive "fSave JSON to: ")
(let ((nodes '()))
(maphash
(lambda (id entry)
(push `((id . ,(or id ""))
(title . ,(or (org-mem-entry-title entry) ""))
(file . ,(or (string-replace
".org"
".html"
(string-replace
"~/giabao.xyz/org" org-publish-url
(org-mem-entry-file entry))) ""))
(tags . ,(vconcat (or (org-mem-entry-tags entry) '()))))
nodes))
org-mem--id<>entry)
(with-temp-file output-file
(insert (json-encode nodes)))))
(my/org-mem-export-compressed-json (expand-file-name "~/giabao.xyz/data/org-mem.json"))
4.7. Project publish
(setq org-publish-project-alist
`(("css"
:base-directory "css/"
:base-extension "css"
:publishing-directory "html/css/"
:recursive t
:publishing-function org-publish-attachment)
("js"
:base-directory "js/"
:base-extension "js"
:publishing-directory "html/js/"
:recursive t
:publishing-function org-publish-attachment)
("data"
:base-directory "data/"
:base-extension "json"
:publishing-directory "html/data/"
:recursive t
:publishing-function org-publish-attachment)
("img"
:base-directory "img/"
:base-extension "png"
:publishing-directory "html/img/"
:recursive t
:publishing-function org-publish-attachment)
("home and notes"
:base-directory "org/"
:base-extension "org"
:publishing-directory "html/"
:exclude ,(regexp-opt '("journal.org"))
:recursive nil
:publishing-function my/org-html-publish-to-html
:html-head my/org-html--build-head
:auto-sitemap nil)
("journal"
:base-directory "org/"
:base-extension "org"
:publishing-directory "html/"
:exclude ,(regexp-opt '("home.org" "notes.org"))
:recursive nil
:publishing-function my/org-html-publish-journal-to-html
:html-head my/org-html--build-head
:auto-sitemap nil)
("notes"
:base-directory "org/notes/"
:base-extension "org"
:publishing-directory "html/notes/"
:recursive nil
:publishing-function my/org-html-publish-to-html
:html-head my/org-html--build-head
:auto-sitemap nil)
("all" :components ("home and notes"
"css" "js" "data" "img"
"journal"
"notes"))))
5. Uploading
rsync -rtv --no-g --delete /home/bao/giabao.xyz/html/* giabao.xyz:/var/www/html/giabao.xyz/
rsync -rtv --no-g --delete /home/bao/giabao.xyz/img/ bao@giabao.xyz:/var/www/resources/giabao.xyz/img/
rsync -rtv --no-g --delete /home/bao/giabao.xyz/data/ bao@giabao.xyz:/var/www/html/giabao.xyz/data/
rsync -rtv --no-g --delete /home/bao/giabao.xyz/js/ bao@giabao.xyz:/var/www/html/giabao.xyz/js/
rsync -rtv --no-g --delete /home/bao/giabao.xyz/css/ bao@giabao.xyz:/var/www/html/giabao.xyz/css/
rsync -rtv --no-g /home/bao/giabao.xyz/img/ giabao.xyz:/var/www/resources/img/
rsync -rtv --no-g /home/bao/giabao.xyz/css/ giabao.xyz:/var/www/html/giabao.xyz/
rsync -rtv --no-g /home/bao/giabao.xyz/js/ giabao.xyz:/var/www/html/giabao.xyz/
rsync -rtv --no-g /home/bao/giabao.xyz/data/ giabao.xyz:/var/www/html/giabao.xyz/data/