Files
org_roam/lisp/build.el
2026-03-08 17:38:16 +00:00

370 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)
(defvar site-root
(file-name-directory
(or load-file-name buffer-file-name)))
(defun site-path (path)
(expand-file-name path site-root))
(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)
(use-package htmlize
:ensure t)
;(setq org-export-with-broken-links t)
(setq org-id-track-globally t)
(setq org-id-locations-file
"~/master-folder/org_files/org_roam/.org-id-locations")
(org-id-update-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 — edit button removed
(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\">⤢</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\">tended</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=\"icon\" type=\"image/png\" sizes=\"48x48\" href=\"/assets/icons/icons8-z-key-64.png\" />
<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\" />
<link rel=\"stylesheet\" href=\"/assets/styles/cookbook.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>
<script src=\"/assets/scripts/cookbook.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>
<div class=\"banner-actions\">
<button id=\"theme-toggle\" aria-label=\"Toggle theme\" title=\"Toggle light/dark\"></button>
<button id=\"close-all\">close all</button>
</div>
</div>
"
)
(defvar z-postamble
"<footer>
<div class=\"copyright-container\">
<div class=\"copyright\">
Copyright &copy; 2022-2026 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.")
;; 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."
(when (and (buffer-file-name)
(not (string-match-p "all-files\\.org$" (buffer-file-name)))
(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")
(insert "* Backlinks\n")
(insert "#+ATTR_HTML: :class backlinks-list\n")
(dolist (bl backlinks)
(insert (format "- %s\n" (plist-get bl :link))))))))
(setq org-id-locations-file
(expand-file-name ".org-id-locations"
(getenv "HOME")))
(setq org-roam-directory site-root)
(org-id-update-id-locations)
(setq org-html-link-home "")
(setq org-html-link-up "")
(defun z/generate-all-files-org ()
"Generate all-files.org from all-files.json."
(let* ((base-dir site-root)
(json-file (expand-file-name "all-files.json" base-dir))
(org-file (expand-file-name "all-files.org" base-dir)))
(when (file-exists-p json-file)
(message "🧩 Generating all-files.org from JSON…")
(let* ((json-object-type 'alist)
(json-array-type 'list)
(json-key-type 'symbol)
(data (json-read-file json-file))
(groups (alist-get 'groups data)))
(with-temp-file org-file
(insert "#+title: All Files\n")
(insert "#+options: toc:nil num:nil\n\n")
(insert "#+begin_comment\n")
(insert "This file is auto-generated. Do not edit manually.\n")
(insert "#+end_comment\n\n")
(dolist (group groups)
(let ((letter (symbol-name (car group)))
(entries (cdr group)))
(insert (format "* %s\n" letter))
(dolist (entry entries)
(let ((id (alist-get 'id entry))
(title (alist-get 'title entry)))
(insert (format "- [[id:%s][%s]]\n" id title))))
(insert "\n")))))
(message "✅ all-files.org generated"))))
(setq org-publish-project-alist
`(
("org-roam"
:base-directory "/home/zaine/master-folder/org_files/org_roam/"
:base-extension "org"
:publishing-directory "/home/zaine/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 "/home/zaine/master-folder/org_files/org_roam/assets/"
:base-extension "css\\|js\\|png\\|jpg\\|gif\\|svg\\|mp4\\|mp3"
:publishing-directory "/home/zaine/master-folder/org_files/org_roam/output/assets/"
:recursive t
:publishing-function org-publish-attachment)))
(defun z/org-publish-quiet (project force)
"Publish PROJECT quietly. If FORCE is non-nil, force publishing."
(let ((inhibit-message t)
(message-log-max nil))
(org-publish project force)))
(defun z/refresh-org-roam ()
"Force Org-roam and Org ID caches to refresh (needed for synced notes)."
(message "starting to refresh...")
;; Update ID locations
(org-id-update-id-locations)
;; Rebuild roam database
(when (featurep 'org-roam)
(org-roam-db-clear-all)
(org-roam-db-sync))
(message "✅ Org-roam DB refreshed"))
(defun z/build-full ()
"Full rebuild: Org, assets, and search index."
(interactive)
(let* ((start-time (current-time)))
(message "Starting full rebuild at %s"
(format-time-string "%Y-%m-%d %H:%M:%S" start-time))
;; IMPORTANT: refresh roam DB after syncthing updates
(org-id-update-id-locations)
;(z/refresh-org-roam)
(setq z/build-full-rebuild t)
(z/generate-all-files-org)
(z/org-publish-quiet "org-roam" t)
(z/org-publish-quiet "org-static" t)
(let ((elapsed (float-time (time-subtract (current-time) start-time))))
(message "✅ Full rebuild complete in %.2f seconds" elapsed))))
(defun z/build-fast ()
"Fast rebuild: Org + assets only (no JSON)."
(interactive)
(setq z/build-full-rebuild nil)
(org-publish "org-roam" t)
(org-publish "org-static" t)
(message "⚡ Fast rebuild complete (no search index)"))
(cond
((member "--fast" command-line-args-left)
(z/build-fast))
(t
(z/build-full)))