Files
org_roam/lisp/build.el

378 lines
13 KiB
EmacsLisp
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
;; -----------------------------
;; Package bootstrap (BATCH SAFE)
;; -----------------------------
(require 'package)
(setq package-user-dir (expand-file-name "./.packages"))
(setq package-archives
'(("melpa" . "https://melpa.org/packages/")
("gnu" . "https://elpa.gnu.org/packages/")))
(package-initialize)
(unless package-archive-contents
(package-refresh-contents))
(unless (package-installed-p 'org-roam)
(package-install 'org-roam))
(require 'ox-publish)
(require 'org)
(require 'ox-html)
(require 'json)
(require 'org-roam)
(require 'ob-latex)
(require 'seq)
(require 'cl-lib)
(setq org-id-locations-file "~/.emacs.d/.org-id-locations")
(defun z/org-html-wrap-content-for-stack (orig-fun contents info)
"Wrap Org HTML #content in stack container, leaving postamble outside."
(let* ((html (funcall orig-fun contents info))
(page (file-name-base (plist-get info :output-file)))
(input-file (plist-get info :input-file))
;; Extract planted date from filename (format: YYYYMMDDHHMMSS-title)
(planted-date
(if (string-match "^\\([0-9]\\{14\\}\\)" page)
(let ((date-str (match-string 1 page)))
(condition-case nil
(format-time-string "%Y-%m-%d"
(encode-time
(string-to-number (substring date-str 12 14)) ; second
(string-to-number (substring date-str 10 12)) ; minute
(string-to-number (substring date-str 8 10)) ; hour
(string-to-number (substring date-str 6 8)) ; day
(string-to-number (substring date-str 4 6)) ; month
(string-to-number (substring date-str 0 4)))) ; year
(error "Unknown")))
;; Fallback: try to get creation date from input file
(let ((file-path (if input-file
(expand-file-name input-file (plist-get info :base-directory))
nil)))
(if (and file-path (file-exists-p file-path))
(format-time-string "%Y-%m-%d" (nth 4 (file-attributes file-path)))
"Unknown"))))
;; Get last modified date (last tended to)
(last-tended
(let ((file-path (if input-file
(expand-file-name input-file (plist-get info :base-directory))
nil)))
(if (and file-path (file-exists-p file-path))
(format-time-string "%Y-%m-%d" (nth 5 (file-attributes file-path)))
"Unknown")))
(stack-close
"</div></article></div></div>"))
(setq html
(replace-regexp-in-string
"<div id=\"content\" class=\"content\">"
(format
"<div id=\"stack-root\">
<div class=\"stack-track\">
<article class=\"stack-pane pane-root\" data-url=\"/%s.html\">
<div id=\"content\" class=\"content\">"
page)
html
nil t))
;; Wrap title with metadata section - use lambda to properly capture title
;; Handle titles with nested HTML tags by matching everything between opening and closing h1 tags
;; Use .* to match any characters (greedy) - will match up to the last </h1> which should be our closing tag
(setq html
(replace-regexp-in-string
"<h1 class=\"title\">\\(.*\\)</h1>"
(lambda (match)
(let ((title-text (match-string 1 match)))
(format
"<div class=\"title-section\">
<div class=\"title-controls\">
<button class=\"pane-fullscreen\" aria-label=\"Fullscreen\">F</button>
<button class=\"pane-edit\" aria-label=\"Edit pane\">E</button>
<button class=\"pane-close\" aria-label=\"Close pane\">×</button>
</div>
<h1 class=\"title\">%s</h1>
<div class=\"title-metadata\">
<span class=\"metadata-item\">
<span class=\"metadata-label\">planted:</span>
<span class=\"metadata-value\">%s</span>
</span>
<span class=\"metadata-item\">
<span class=\"metadata-label\">last tended to:</span>
<span class=\"metadata-value\">%s</span>
</span>
</div>
</div>"
title-text planted-date last-tended)))
html
nil t))
(setq html
(replace-regexp-in-string
"<div id=\"postamble\" class=\"status\">"
(concat stack-close "\n<div id=\"postamble\" class=\"status\">")
html
nil t))
html))
(advice-add 'org-html-template :around #'z/org-html-wrap-content-for-stack)
(add-to-list 'load-path default-directory)
(defvar z-shared-head
"
<link rel=\"stylesheet\" href=\"/assets/styles/style.css\" />
<link rel=\"stylesheet\" href=\"/assets/styles/bigger-picture.min.css\" />
<link rel=\"stylesheet\" href=\"/assets/styles/media.css\" />
<script src=\"/assets/scripts/script.js\" defer></script>
<script src=\"/assets/scripts/search.js\" defer></script>
<script src=\"/assets/scripts/bigger-picture.min.js\" defer></script>
<script src=\"/assets/scripts/svg-pan-zoom.min.js\" defer></script>
<script src=\"/assets/scripts/gallery-init.js\" defer></script>
"
)
(defvar z-preamble
"
<div class=\"banner-header\">
<div class=\"banner-left\">
<a href=\"/20241210233721-brain_moc.html\"> <img src=\"/assets/gr.png\" alt=\"Site Logo\" class=\"banner-logo\" /> </a>
<div id=\"updated\">Updated: %C</div>
</div>
<div class=\"banner-search\">
<input id=\"search-box\"
type=\"search\"
placeholder=\"Search notes…\"
aria-label=\"Search notes\"
autocomplete=\"off\" />
<div id=\"search-results\"></div>
</div>
<a href=\"https://zainezq.com\"> <img src=\"/assets/2_to_1.svg\" alt=\"Web logo\" class=\"web-logo\" /> </a>
<button id=\"close-all\">Close All</button>
</div>
"
)
(defvar z-postamble
"<footer>
<div class=\"copyright-container\">
<div class=\"copyright\">
Copyright &copy; 2022-2025 Zaine Qayyum. All rights reserved unless otherwise noted.</div></div>
<div class=\"generated\">
Created with %c on <a href=\"https://www.archlinux.org/\">Arch</a> <a href=\"https://www.gnu.org\">GNU</a>/<a href=\"https://www.kernel.org/\">Linux</a>
</div>
</footer>")
(defvar z/build-full-rebuild t
"If non-nil, generate expensive artifacts like search-index.json.")
(defun z/generate-all-files-page ()
"Generate all-files.org from search-index.json."
(let* ((json-file "~/master-folder/org_files/org_roam/output/search-index.json")
(out-file "~/master-folder/org_files/org_roam/all-files.org")
(data (append (json-read-file json-file) nil)))
(message "🔎 Loaded %d entries from search-index.json" (length data))
(with-temp-file out-file
(insert "#+title: All Files\n")
(insert "#+options: toc:nil\n\n")
(insert "A complete alphabetical index of all published pages.\n\n")
(setq data
(sort data
(lambda (a b)
(string-lessp
(downcase (cdr (assoc 'title a)))
(downcase (cdr (assoc 'title b)))))))
(let ((current-letter nil))
(dolist (entry data)
(let* ((title (cdr (assoc 'title entry)))
(raw-url (cdr (assoc 'url entry)))
(id (cdr (assoc 'id entry)))
(letter (upcase (substring title 0 1))))
(unless (equal letter current-letter)
(setq current-letter letter)
(insert (format "* %s\n" letter)))
(insert (format "- [[id:%s][%s]]\n" id title))))))))
;; Babel language support
(org-babel-do-load-languages
'org-babel-load-languages
'((latex . t)
(org . t)))
(setq org-confirm-babel-evaluate nil ;; Disable confirmation for code block evaluation
org-export-with-smart-quotes t ;; turns " into curly quotes
org-html-self-link-headlines t ;; headlines link to self
org-html-validation-link nil ;; dont validate html (in order to check it is up to web standards)
org-html-inline-images t ;; self-expl
org-export-with-sub-superscripts t ;; render h_x as large h with subscript x
)
(load-file (expand-file-name "lisp/macros.el" default-directory))
;; Configure LaTeX image generation
(setq org-latex-create-formula-image-program 'imagemagick)
(defun my/get-org-roam-backlinks (filename)
"Return backlinks for the given org-roam file. FILENAME should be the full path to the org-roam file."
(save-excursion
(with-current-buffer (find-file-noselect filename)
(goto-char (point-min))
(if (and (featurep 'org-roam) (org-roam-node-at-point))
(let* ((node (org-roam-node-at-point))
(backlinks (org-roam-backlinks-get node)))
(mapcar (lambda (backlink)
(let ((source-node (org-roam-backlink-source-node backlink)))
(list
:file (org-roam-node-file source-node)
:title (org-roam-node-title source-node)
:content (with-current-buffer
(find-file-noselect (org-roam-node-file source-node))
(goto-char (org-roam-backlink-point backlink))
(thing-at-point 'sentence t))
:link (format "[[id:%s][%s]]"
(org-roam-node-id source-node)
(org-roam-node-title source-node)))))
backlinks)
)
nil))))
(defun my/org-export-insert-backlinks (_backend)
"Insert Org-roam backlinks before export, with HTML attributes."
(when (and (featurep 'org-roam)
(org-roam-node-at-point))
(let ((backlinks (my/get-org-roam-backlinks (buffer-file-name))))
(when backlinks
(goto-char (point-max))
(insert "\n#+ATTR_HTML: :class backlinks-section :id backlinks\n Backlinks\n")
(insert "#+ATTR_HTML: :class backlinks-list\n")
(dolist (bl backlinks)
(insert (format "- %s\n"
(plist-get bl :link))))))))
(add-hook 'org-export-before-processing-hook
#'my/org-export-insert-backlinks)
(setq org-id-locations-file "~/.emacs.d/.org-id-locations")
(setq org-roam-directory "~/master-folder/org_files/org_roam/")
(org-id-update-id-locations)
(setq org-html-link-home "")
(setq org-html-link-up "")
(defun z/generate-search-index (&rest _)
"Generate search-index.json with title, url, and id."
(when z/build-full-rebuild
(message "Generating search-index.json...")
(let* ((org-dir "~/master-folder/org_files/org_roam/")
(out-file "~/master-folder/org_files/org_roam/output/search-index.json")
(files
(seq-filter
(lambda (f)
(not (string-match-p "/\\.#[^/]+\\.org$" f)))
(directory-files-recursively org-dir "\\.org$")))
(index '()))
(dolist (file files)
(with-current-buffer (find-file-noselect file)
(let ((id (org-id-get nil)))
(when id
(push `((title . ,(or (org-get-title) "Untitled"))
(url . ,(concat "/" (file-name-base file) ".html"))
(id . ,id))
index)))))
;; De-duplicate by ID
(setq index
(cl-remove-duplicates
index
:key (lambda (e) (cdr (assoc 'id e)))
:test #'string=))
(with-temp-file out-file
(insert (json-encode index)))
(message "search-index.json generated (%d entries)." (length index)))))
(add-hook 'org-publish-after-publishing-hook
#'z/generate-search-index)
(setq org-publish-project-alist
`(
("org-roam"
:base-directory "~/master-folder/org_files/org_roam/"
:base-extension "org"
:publishing-directory "~/master-folder/org_files/org_roam/output/"
:recursive t
:publishing-function org-html-publish-to-html
:with-author nil
:with-creator nil
:with-date t
:with-toc nil
:section-numbers nil
:html-preamble ,z-preamble
:html-postamble ,z-postamble
:html-head ,z-shared-head)
("org-static"
:base-directory "~/master-folder/org_files/org_roam/assets/"
:base-extension "css\\|js\\|png\\|jpg\\|gif\\|svg"
:publishing-directory "~/master-folder/org_files/org_roam/output/assets/"
:recursive t
:publishing-function org-publish-attachment)))
;(delete-directory "~/master-folder/org_files/org_roam/output/" t)
(message "Directory deleted")
(defun z/build-full ()
"Full rebuild: Org, assets, and search index."
(interactive)
(setq z/build-full-rebuild t)
(org-id-update-id-locations)
(z/generate-all-files-page)
(org-publish "org-roam" t)
(org-publish "org-static" t)
(message "✅ Full rebuild complete"))
(defun z/build-fast ()
"Fast rebuild: Org + assets only (no JSON)."
(interactive)
(setq z/build-full-rebuild nil)
(z/generate-all-files-page)
(org-publish "org-roam" t)
(org-publish "org-static" t)
(message "⚡ Fast rebuild complete (no search index)"))
(org-id-update-id-locations)
(cond
((member "--fast" command-line-args-left)
(z/build-fast))
(t
(z/build-full)))