;; external dependencies: ;; - simple-httpd ;;; utils (defun string-replace-> (alist string) "Return a string after a list of string replacements '((from-string to-string)) have been applied to IN-STRING. Applied oned after the other." (let ((string (copy-sequence string))) (pcase-dolist (`(,from-string ,to-string) alist) (setq string (string-replace from-string to-string string))) string)) (defun file (path) (expand-file-name (if (file-directory-p path) (file-name-as-directory path) (concat (file-name-directory path) (file-name-nondirectory path))))) (defun files (dir &optional regex) (seq-remove #'file-directory-p (directory-files dir t (or regex directory-files-no-dot-files-regexp) t))) (defun dirs-in (dir &optional regex) (seq-filter #'file-directory-p (mapcar #'file-name-as-directory (directory-files dir t (or regex directory-files-no-dot-files-regexp) t)))) (defun all-files (dir &optional regex) (directory-files-recursively dir (or regex directory-files-no-dot-files-regexp) nil)) (defmacro with-temp-file-buffer (file &rest body) "If FILE exists, insert its contents in a temp buffer and call BODY." (declare (indent 1)) `(cl-flet ((get-buffer-or-file (file) (let* ((buffer (get-file-buffer file)) (file-exists (file-exists-p file)) (buffer-modified (buffer-modified-p buffer))) (cond ((or (and file-exists buffer (not buffer-modified) (not (eq buffer-modified 'autosaved))) (and file-exists (not buffer))) (cons #'insert-file-contents file)) (buffer (cons #'insert-buffer buffer)))))) (when-let* ((file-and-function (get-buffer-or-file ,file))) (with-temp-buffer (funcall (car file-and-function) (cdr file-and-function)) (goto-char (point-min)) ,@body)))) (defun ensure-plist (thing) (let ((plist (ensure-list thing))) (if (plistp plist) plist (user-error "Malformed plist: %S" plist)))) (defmacro defplist (var &rest plist) (declare (indent 1)) `(,(if (boundp var) 'setq 'defvar) ,var ',(ensure-plist plist))) (defmacro nah (&rest args) "Ignore all arguments without evaluating." ()) ;;; simple export (defcustom *preprocess-functions* '() "Functions to call on unprocessed org files, should accept the org buffer as argument." :type 'hook) (defcustom *postprocess-functions* '() "Functions to call on org->html exported files, should accept HTML buffer path as argument." :type 'hook) (defcustom html-head " " "Org-wide head definitions for exported HTML files. See `org-html-head'" :type 'string) (defcustom html-footnotes-section "%s" "Format for the footnotes section. Stuffed in a comment to hide it entirely. See `org-html-footnotes-section'" :type 'string) (defun filter-org-html-links (fn &rest args) "Hack to replace file:// protocol links and index.html" (string-replace-> '(("file://" "") ("/index.html" "/") ("../" "/") ("./" "/")) (apply fn args))) (advice-add 'org-html-link :around 'filter-org-html-links) (cl-defun simple-export (file &optional (directory "~/www")) (interactive "fFile: \nDDir: ") (require 'ox-html) (let ((org-html-head-include-default-style nil) (org-export-with-section-numbers nil) (org-html-head-include-scripts nil) (org-html-htmlize-output-type nil) (org-export-with-broken-links t) (org-html-self-link-headlines t) (org-html-style-default nil) (org-export-with-author nil) (org-export-with-title nil) (org-html-html5-fancy t) (org-export-with-toc nil) (org-html-postamble nil) (org-html-style nil) (org-html-validation-link "") (org-html-doctype "html5") (org-html-footnotes-section html-footnotes-section) (org-html-head html-head) (org (file file))) (with-current-buffer (get-buffer (find-file-noselect org)) (run-hook-with-args '*preprocess-functions* (current-buffer)) (org-export-to-file 'html (org-export-output-file-name ".html" nil directory) nil nil nil nil nil '(lambda (file) (with-temp-file-buffer file (save-excursion (while (not (eq (point) (point-max))) (when (re-search-forward "^<[/]?div.*$" nil t 1) (delete-char (1- (- (pos-bol) (pos-eol))))) (goto-char (1+ (pos-eol))))) (run-hook-with-args '*postprocess-functions* (current-buffer)) (write-file file nil))))))) ;;; shortname (defvar *shortnames* (make-hash-table :test 'equal)) (defun retreive-shortname (file) (with-temp-file-buffer file (when (re-search-forward "^#\\+shortname\\s-*:" nil t 1) (string-clean-whitespace (buffer-substring-no-properties (point) (pos-eol)))))) (defun get-shortname (link) (gethash link *shortnames*)) (defun set-shortname (file) (let* ((key (file-name-sans-extension (file-name-nondirectory file))) (value (retreive-shortname file))) (when value (puthash key value *shortnames*)))) (defun use-shortnames (buffer) (save-excursion (while (not (eq (point) (point-max))) (when-let* ((beg (re-search-forward "href=\"" nil t 1)) (end (save-excursion (when (re-search-forward "\"" nil t 1) (backward-char 1) (point)))) (link (file-name-sans-extension (buffer-substring-no-properties beg end))) (shortname (get-shortname link))) (delete-char (- end beg)) (insert (concat shortname) ".html")) (goto-char (1+ (pos-eol)))))) (defun rename-to-shortname (file) (when-let ((file (file file)) (shortname (get-shortname (file-name-base file)))) (rename-file file (string-join `(,(file-name-directory file) ,shortname ".html"))))) (defun rename-to-shortnames (files) (mapcar #'rename-to-shortname files)) (add-hook '*postprocess-functions* 'use-shortnames) ;;; site accessors (defun site->include (plist) (funcall (plist-get plist :include))) (defun site->source (plist) (funcall (plist-get plist :source))) (defun site->templates (plist) (funcall (plist-get plist :templates))) (defun site->template (plist template) (seq-find (lambda (e) (string= template (file-name-base e))) (site->templates plist))) (defun site->out (plist) (plist-get plist :out)) (defun site->url (plist) (plist-get plist :url)) (defun site->tar (plist) (plist-get plist :tar)) (defun site->config (plist) (plist-get plist :config)) ;;; simple-build (defcustom *simple-build-functions* '() "Functions to call at the end of the build process just before serving all the files and killing all associated org buffers. Functions should accept one argument, site, which is a plist containing the various components of the site being built." :type 'hook) (defun simple-build/drop (site) (mapcar #'delete-file (all-files (site->out site))) (mapcar (lambda (dir) (delete-directory dir t)) (dirs-in (site->out site)))) (defun simple-build/copy (site) (let ((output-directory (site->out site))) (mapcar (lambda (include) (let ((dir (file-name-as-directory include))) (if (file-exists-p dir) (copy-directory dir (file-name-as-directory output-directory) t) (copy-file include (file-name-as-directory output-directory) t)))) (site->include site)))) (defun simple-build/make (site) (let ((output-directory (site->out site))) (mapcar #'set-shortname (site->source site)) (mapcar (lambda (org-file) (simple-export org-file output-directory)) (site->source site)))) (defun simple-build/slug (site) (rename-to-shortnames (all-files (site->out site) "\\.html$"))) (defun simple-build/hook (site) (run-hook-with-args '*simple-build-functions* site)) (defun simple-build/demo (site &optional stop) (if (null stop) (httpd-serve-directory (site->out site)) (httpd-stop))) (defun simple-build/kill (site) (dolist (buffer (mapcar #'get-file-buffer (site->source site))) (when buffer (with-current-buffer buffer (save-buffer)) (kill-buffer buffer)))) (defun simple-push (site) (let* ((inhibit-message t) (tarball (format "%s/%s--%s.tar.gz" (site->tar site) (site->url site) (format-time-string "%F@%T.%3N")))) (simple-build site t) (shell-command (format "cd %s && tar -cvz * > %s" (site->out site) tarball)) (async-shell-command (format "hut pages publish -d %s --site-config %s %s" (site->url site) (site->config site) tarball)))) (defun simple-build (site &optional stop-server) (interactive "P") (let ((inhibit-message t) (messages-buffer-max-lines nil)) (when stop-server (simple-build/demo site t)) (unwind-protect (progn (simple-build/drop site) (simple-build/make site) (simple-build/copy site) (simple-build/slug site) (simple-build/hook site) (simple-build/demo site)) (simple-build/kill site))) (message "Built website to: %s" (site->out site))) ;;; zyd.lol (defvar *categories* '("gamedev" "lisp")) (defun links (category) (let ((links '())) (maphash (lambda (longname shortname) (when (string-search (concat "_" category) longname) (push (concat shortname ".html") links))) *shortnames*) links)) (defun gethashv (value table &optional test) (cl-block nil (maphash (lambda (key val) (when (funcall (or test #'string=) value val) (cl-return key))) table))) (defun org-file (shortname site) (seq-find (lambda (source) (string= (file-name-base source) (gethashv (file-name-sans-extension shortname) *shortnames*))) (site->source site))) (defun org-title (link site) (when-let ((org-file (org-file link site))) (with-temp-file-buffer org-file (when (re-search-forward "^#\\+title\\s-*:" nil t 1) (string-clean-whitespace (buffer-substring-no-properties (point) (pos-eol))))))) (defun insert-links (links site) (dolist (link links) (insert "- ") (org-insert-link nil (concat "/" link) (org-title link site)) (newline) (forward-line 1))) (defun insert-category (category) (newline) (forward-line 1) (insert "* ") (goto-char (pos-eol)) (insert (capitalize category)) (goto-char (pos-eol)) (newline 1) (forward-line 1)) (defun generate-index (site) (let ((file (make-temp-file "index" nil ".org"))) (with-temp-file-buffer (site->template site "index") (goto-char (point-max)) (dolist (category *categories*) (insert-category category) (insert-links (links category) site)) (newline) (forward-line 1) (insert "* Other") (newline) (forward-line 1) (insert-links (seq-difference (links "") (mapcan #'links *categories*)) site) (write-file file)) file)) (defun make-index (site) (let ((index (generate-index site))) (set-shortname index) (rename-to-shortname (simple-export index (site->out site))))) (defun disable-headline-links-for-index (buffer) (when (string-search "index" (buffer-name buffer)) (with-current-buffer buffer (setq-local org-html-self-link-headlines nil)))) (add-hook '*preprocess-functions* 'disable-headline-links-for-index) (add-hook '*simple-build-functions* 'make-index) (defplist zyd.lol :url "zyd.lol" :out "~/zyd.lol" :tar "~/.config/hut/backups" :config "~/.config/hut/site-config.json" :source (lambda () (files "~/memex" "_push.*")) :include (lambda () `(,@(files "~/dotfiles/hut/static") ,@(dirs-in "~/dotfiles/hut/static"))) :templates (lambda () (files "~/.config/hut/templates/"))) (defun zyd/build-site () (interactive) (simple-build zyd.lol)) (defun zyd/push-site () (interactive) (simple-build zyd.lol t) (simple-push zyd.lol)) ;;; testing (nah (simple-build/drop zyd.lol) (simple-build/make zyd.lol) (simple-build/copy zyd.lol) (simple-build/slug zyd.lol) (simple-build/hook zyd.lol) (simple-build/demo zyd.lol) (simple-build/demo zyd.lol t) (simple-build/kill zyd.lol) (simple-build zyd.lol t) (simple-push zyd.lol))