378 lines
13 KiB
EmacsLisp
Executable File
378 lines
13 KiB
EmacsLisp
Executable File
;; -----------------------------
|
||
;; 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 © 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 "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)))
|