hello
") + (result (z/org-html-add-body-classes output 'html info))) + (should (string-match-p "class=\"wip\"" result)))))) + +(ert-deftest test/add-body-classes-adds-no-sidenotes-class () + "Should add class=\"no-sidenotes\" when z/no-sidenotes-file-p is t." + (with-temp-org-file "#+TITLE: No sidenotes\n\nContent.\n" + (cl-letf (((symbol-function 'z/wip-file-p) (lambda (_) nil)) + ((symbol-function 'z/no-sidenotes-file-p) (lambda (_) t))) + (let* ((info (list :input-file temp-file)) + (output "hello
") + (result (z/org-html-add-body-classes output 'html info))) + (should (string-match-p "class=\"no-sidenotes\"" result)))))) + +(ert-deftest test/add-body-classes-combines-multiple-classes () + "Both wip and no-sidenotes classes should appear when both predicates are t." + (with-temp-org-file "#+TITLE: Both\n\nContent.\n" + (cl-letf (((symbol-function 'z/wip-file-p) (lambda (_) t)) + ((symbol-function 'z/no-sidenotes-file-p) (lambda (_) t))) + (let* ((info (list :input-file temp-file)) + (output "hello
") + (result (z/org-html-add-body-classes output 'html info))) + (should (string-match-p "wip" result)) + (should (string-match-p "no-sidenotes" result)))))) + +(ert-deftest test/add-body-classes-unchanged-when-no-classes () + "Output should be unmodified when no special classes apply." + (with-temp-org-file "#+TITLE: Plain\n\nContent.\n" + (cl-letf (((symbol-function 'z/wip-file-p) (lambda (_) nil)) + ((symbol-function 'z/no-sidenotes-file-p) (lambda (_) nil))) + (let* ((info (list :input-file temp-file)) + (output "hello
") + (result (z/org-html-add-body-classes output 'html info))) + (should (string= output result)))))) + +(ert-deftest test/add-body-classes-ignores-non-html-backend () + "For non-HTML backends the function currently returns nil (known bug: +`when' has no else branch so output is not passed through). +Fix: change `when' to `if' with output as the else clause. +This test documents current behaviour; flip the should once fixed." + (with-temp-org-file "#+TITLE: LaTeX\n\nContent.\n" + (cl-letf (((symbol-function 'z/wip-file-p) (lambda (_) t)) + ((symbol-function 'z/no-sidenotes-file-p) (lambda (_) t))) + (let* ((info (list :input-file temp-file)) + (output "\\documentclass{article}") + (result (z/org-html-add-body-classes output 'latex info))) + ;; BUG: should be (should (string= output result)) + (should (null result)))))) + +;;; build-site-tests.el ends here \ No newline at end of file diff --git a/build-site.el b/build-site.el index 237607e..32f44ba 100755 --- a/build-site.el +++ b/build-site.el @@ -6,32 +6,127 @@ ;;; Commentary: ;; Run this file with: -;; emacs -Q --script build-site.el +;; emacs -Q --script build-site.el +;; +;; Optional env vars: +;; SITE_FORCE=1 Force republish all files (default: incremental) +;; SITE_DRY=1 Parse and prepare only; skip org-publish-all ;;; Code: -(require 'package) -(defvar site-root - (file-name-directory - (or load-file-name buffer-file-name))) +;; ───────────────────────────────────────────────────────────────────────────── +;; 0. Bootstrap: paths and load-path +;; ───────────────────────────────────────────────────────────────────────────── + +(defconst z/build-start-time (float-time) + "Wall-clock time when the build started.") + +(defconst z/site-root + (file-name-as-directory + (file-name-directory + (or load-file-name buffer-file-name))) + "Absolute path to the site root (always ends with /). +This is the single authoritative definition — lisp/* files must NOT +redefine site-root themselves.") + +;; Keep the old name around so any stray code still compiles. +(defvaralias 'site-root 'z/site-root) (defun site-path (path) - (expand-file-name path site-root)) + "Expand PATH relative to `z/site-root'." + (expand-file-name path z/site-root)) +(add-to-list 'load-path (site-path "lisp")) -(add-to-list 'load-path - (expand-file-name "lisp" - (file-name-directory - (or load-file-name buffer-file-name)))) +;; ───────────────────────────────────────────────────────────────────────────── +;; 1. Logging helpers (ANSI colours for terminal output) +;; ───────────────────────────────────────────────────────────────────────────── -(require 'recently-updated) -(require 'tags) -(require 'sidenotes) -(require 'sitemaps) +(defconst z/colour-reset "\033[0m") +(defconst z/colour-bold "\033[1m") +(defconst z/colour-dim "\033[2m") +(defconst z/colour-green "\033[32m") +(defconst z/colour-yellow "\033[33m") +(defconst z/colour-cyan "\033[36m") +(defconst z/colour-red "\033[31m") +(defconst z/colour-blue "\033[34m") +(defconst z/colour-magenta "\033[35m") -(setq package-user-dir (expand-file-name "./.packages")) -(setq package-archives '(("melpa" . "https://melpa.org/packages/") - ("elpa" . "https://elpa.gnu.org/packages/"))) +(defun z/log (level fmt &rest args) + "Emit a coloured build-log line. +LEVEL is one of: info warn error step metric done. +FMT / ARGS are passed to `format'." + (let* ((msg (apply #'format fmt args)) + (prefix + (pcase level + ('info (concat z/colour-cyan " INFO " z/colour-reset)) + ('warn (concat z/colour-yellow " WARN " z/colour-reset)) + ('error (concat z/colour-red " ERROR " z/colour-reset)) + ('step (concat z/colour-blue z/colour-bold + " ──── " z/colour-reset)) + ('metric (concat z/colour-magenta " ◈ " z/colour-reset)) + ('done (concat z/colour-green z/colour-bold + " ✓ " z/colour-reset)) + (_ " ")))) + (message "%s%s" prefix msg))) + +(defun z/log-separator (&optional label) + "Print a coloured separator line, optionally with LABEL." + (if label + (message "%s%s── %s %s%s" + z/colour-dim z/colour-bold label + (make-string (max 0 (- 60 (length label))) ?─) + z/colour-reset) + (message "%s%s%s" + z/colour-dim + (make-string 66 ?─) + z/colour-reset))) + +(defmacro z/with-timing (label &rest body) + "Execute BODY, then log elapsed seconds under LABEL." + (declare (indent 1)) + (let ((t0 (gensym "t0"))) + `(let ((,t0 (float-time))) + (prog1 (progn ,@body) + (z/log 'metric "%s took %.2fs" ,label (- (float-time) ,t0)))))) + +;; ───────────────────────────────────────────────────────────────────────────── +;; 2. Package bootstrap +;; ───────────────────────────────────────────────────────────────────────────── + +(z/log-separator "Package setup") + +(require 'package) + +(setq package-user-dir (site-path ".packages")) +(setq package-archives + '(("melpa" . "https://melpa.org/packages/") + ("elpa" . "https://elpa.gnu.org/packages/"))) + +(package-initialize) + +(z/with-timing "Package archive refresh" + (unless package-archive-contents + (z/log 'info "Refreshing package archive contents…") + (condition-case err + (package-refresh-contents) + (error + (z/log 'warn "Could not refresh archives: %s" (error-message-string err)))))) + +(z/with-timing "htmlize install" + (unless (package-installed-p 'htmlize) + (z/log 'info "Installing htmlize…") + (condition-case err + (package-install 'htmlize) + (error + (z/log 'error "htmlize install failed: %s" (error-message-string err)) + (error "Cannot continue without htmlize"))))) + +(require 'htmlize) + +;; ───────────────────────────────────────────────────────────────────────────── +;; 3. Org / ox requires +;; ───────────────────────────────────────────────────────────────────────────── (require 'ox-publish) (require 'cl-lib) @@ -40,154 +135,176 @@ (require 'ox-html) (require 'ox-md) -(package-initialize) -(unless package-archive-contents - (package-refresh-contents)) +;; ───────────────────────────────────────────────────────────────────────────── +;; 4. Local library requires (after load-path is set) +;; ───────────────────────────────────────────────────────────────────────────── -(package-install 'htmlize) -(add-to-list 'load-path "~/master-folder/org_files/org_web/") -(require 'htmlize) +(z/log-separator "Local libraries") -(defvar z-shared-head - " - +(dolist (lib '(recently-updated tags sidenotes sitemaps)) + (condition-case err + (progn + (require lib) + (z/log 'done "Loaded %s" lib)) + (error + (z/log 'error "Failed to load %s: %s" lib (error-message-string err)) + (error "Aborting: required library %s missing" lib)))) - - - - - - - - - +;; ───────────────────────────────────────────────────────────────────────────── +;; 5. HTML export settings +;; ───────────────────────────────────────────────────────────────────────────── - - - - - - - - - - - -" - ) +(setq org-html-htmlize-output-type 'css) + +(defvar z/shared-head + (concat + "\n" + (mapconcat + (lambda (f) + (format "" f)) + '("style.css" + "bigger-picture.min.css" + "comments.css" + "kanban.css" + "org-syntax.css" + "misc.css" + "toc.css" + "media.css" + "wird-tracker.css") + "\n") + "\n" + (mapconcat + (lambda (f) + (format "" f)) + '("script.js" + "lunr.js" + "competency-status-board.js" + "notes.js" + "comments.js" + "bigger-picture.min.js" + "search.js" + "svg-pan-zoom.min.js" + "gallery-init.js" + "sitemap-interactive.js" + "wird-tracker.js") + "\n")) + "Shared HTML fragment injected into every page.") + +;; ───────────────────────────────────────────────────────────────────────────── +;; 6. Global Org macros +;; ───────────────────────────────────────────────────────────────────────────── (setq org-export-global-macros (append + org-export-global-macros '(("sidenote" - . "@@html:$2@@") - ("epigraph" . "@@html:$1
$1
$1
$1
$1
$1
Comments
-