emacs/org-publish.el
2026-03-31 09:49:05 -05:00

436 lines
18 KiB
EmacsLisp

(require 'ox-publish)
(require 'denote)
(require 'denote-org)
(require 'async)
(require 'dash)
(require 's)
(require 'f)
(require 'comp-run)
(require 'simple-httpd)
(defun quick/async-export-function (chunk index)
"A function to build a future that will export all it's chunk's files"
(let ((func `(lambda ()
(setq lexical-binding t)
(shell-command (format "emacsclient -e '(message \"%s\")'" ',chunk))
(require 's)
(require 'org)
(require 'ox)
(require 'ox-html)
(require 'htmlize)
(require 'denote)
(require 'denote-org)
(cl-loop for file in ',chunk do
(setq find-file-hook '())
(setq org-mode-hook '())
;; (message "%s" file)
(shell-command (format "emacsclient -e '(message \"%s\")" file))
(with-current-buffer (find-file file)
(re-search-forward ".*EXPORT_FILE_NAME:" nil t)
(beginning-of-line)
(let* ((export-line? (search-forward "EXPORT_FILE_NAME" (line-end-position) t))
(export-line (when export-line?
(delete-and-extract-region
(line-beginning-position) (line-end-position))))
(find-file-hook '())
(gc-cons-threshold 50000000)
(org-mode-hook '())
(create-lockfiles nil)
(org-export-with-broken-links 'mark)
(org-export-with-section-numbers nil)
(org-html-html5-fancy t)
(org-html-validation-link nil)
(make-backup-files nil)
(org-export-with-title t)
(org-html-head-include-default-style nil)
(org-html-head "<link rel=\"stylesheet\" href=\"../static/pico.snazzy.css\">")
(denote-directory "/home/chris/docs/site/content")
(export-directory
(concat "/home/chris/"
"docs/site/public/"
(if (s-contains? "content/teaching" file)
"teaching/"
"notes/")))
(html-file (concat export-directory
(org-export-output-file-name ".html")))
(org-export-coding-system org-html-coding-system))
(denote-org-convert-links-to-file-type)
(widen)
(org-update-all-dblocks)
(replace-string-in-region "../site/content/" "./"
(point-min) (point-max))
(org-export-to-file 'html html-file)
(when export-line? (insert export-line))
(save-buffer)
(kill-buffer)))))))
(message "Exporting chunk %s out of %s with files: %s" index (- (length chunks) 1)
(mapcar (lambda (file)
(concat "\n" file)) chunk))
(async-start
func
'ignore)))
(defun quick/org-publish-site ()
"Publish my website by pushing files to specific locations"
(interactive)
(let* ((notes (--remove
(string-match-p "\\(_draft\\|_lesson\\|_sermon\\)" it)
(directory-files "~/docs/notes" t ".*.org$")))
(lessons (--remove
(s-contains? "_draft" it)
(directory-files "~/docs/notes/lessons" t ".*.org$")))
(denote-directory "~/docs/site/content")
(current-notes (directory-files "~/docs/site/content/notes" t ".*.org$"))
(current-lessons (directory-files "~/docs/site/content/teaching" t ".*.org$"))
(warning-minimum-level :error)
(inhibit-message)
(create-lockfiles nil)
(org-export-with-broken-links 'mark)
(org-export-with-section-numbers nil)
(org-export-with-author nil)
(org-export-with-timestamps nil)
(org-export-with-date nil)
(org-export-with-toc nil)
(org-html-html5-fancy t)
(org-html-validation-link nil)
(org-html-postamble "")
(make-backup-files nil)
(org-html-head-include-default-style nil)
(org-html-head "<link rel=\"stylesheet\" href=\"./static/pico.snazzy.css\">
<style>
li:nth-child(n+7) {
display: none;
}
</style>"))
(when (not
(= 0
(shell-command
"cmp --silent ~/docs/site/assets/pico.snazzy.css\
~/docs/site/public/static/pico.snazzy.css")))
(copy-file "~/docs/site/assets/pico.snazzy.css"
"~/docs/site/public/static/pico.snazzy.css" t))
(message "Starting site build")
(cl-loop for file in current-notes
do (delete-file file t))
(message "Removed all notes")
(cl-loop for file in current-lessons
do (delete-file file t))
(message "Removed all lessons")
(cl-loop for file in notes
do (let ((filename (concat "~/docs/site/content/notes/" (f-filename file))))
(copy-file
file filename t t)))
(message "Copied all notes")
(cl-loop for file in lessons
do (let ((filename (concat "~/docs/site/content/teaching/" (f-filename file))))
(copy-file
file filename t t)
(message "copied %s to %s" file filename)))
(message "Copied all lessons")
(find-file "~/docs/site/content/index.org")
(org-update-all-dblocks)
(org-export-to-file 'html "~/docs/site/public/index.html")
(kill-buffer)
(let* ((files (seq-concatenate
'list
(directory-files "~/docs/site/content/notes" t ".*.org$")
(directory-files "~/docs/site/content/teaching" t ".*.org$")))
(workers 8)
(chunks (seq-split files (ceiling (/ (length files) workers))))
(futures (cl-loop for chunk in chunks and index from 0
collect (quick/async-export-function chunk index))))
(mapcar #'async-get futures)
(message "Finished exporting all files"))))
(defun quick/org-copy-notes (plist)
(let ((files (--remove
(string-match-p (plist-get plist :exclude) it)
(directory-files (file-name-as-directory
(plist-get plist :denote-directory)) t ".*.org$")))
(create-lockfiles nil))
(dolist (file files)
(let ((filename (concat (file-name-as-directory
(plist-get plist :base-directory))
(f-filename file))))
(copy-file
file filename t t)))))
(defun chris/site-index-update (plist)
(save-excursion
(let ((warning-minimum-level :error)
(denote-directory "~/docs/site/content")
(file (concat (file-name-as-directory
(plist-get plist :base-directory)) "index.org")))
(find-file file)
(org-update-all-dblocks)
(denote-org-convert-links-to-file-type)
(save-buffer))))
(defun chris/preparation-function (plist)
(quick/org-copy-notes plist)
(chris/site-index-update plist))
(defun chris/ignore-export-name-filter-function (plist _backend)
(when (plist-member plist :export-file-name)
(let ((name (plist-get plist :export-file-name))
(key-gone (remove :export-file-name))
(new-plist (remove name)))
new-plist)))
(defun chris/site-premble (plist)
(let ((project (plist-get plist :title-of-project)))
(format "<nav>
<ul>
<li><strong>%s</strong></li>
</ul>
<ul>
<li><a href=\"/index\">Home</a></li>
<li><a href=\"/notes\">Notes</a></li>
<li><a href=\"/teaching\">Teaching</a></li>
</ul>
</nav>" (if (string=
(when (stringp project) project) "index")
"Chris Cochrun"))))
(setq org-publish-project-alist
`(("notes"
:denote-directory "~/docs/notes/"
:base-directory "~/docs/site/content/notes/"
:base-extension "org"
:publishing-directory "~/docs/site/public/notes/"
:exclude "\\(_draft\\|_lesson\\|_sermon\\)"
:preparation-function chris/preparation-function
:recursive nil
:with-toc t
:with-broken-links t
:with-author nil
:with-timestamps nil
:section-numbers nil
:html-postamble nil
:html-validation-link nil
:exlude-tags "revealonly\\|noexport"
:html-head-include-default-style nil
:html-doctype "html5"
:html-html5-fancy t
:html-self-link-headlines t
:htmlized-source t
;; :html-preamble chris/site-premble
:html-preamble-format (("en" "<nav>
<ul>
<li><strong><a href=\"/\">Chris Cochrun</a> > %t</strong></li>
</ul>
<ul>
<li><a href=\"/notes\">Notes</a></li>
<li><a href=\"/teaching\">Teaching</a></li>
</ul>
</nav>"))
:html-head "<link rel=\"stylesheet\" href=\"../static/pico.snazzy.css\" type=\"text/css\"/>"
:publishing-function org-html-publish-to-html)
("teaching"
:denote-directory "~/docs/notes/lessons/"
:base-directory "~/docs/site/content/teaching/"
:base-extension "org"
:publishing-directory "~/docs/site/public/teaching/"
:exclude "_draft"
:preparation-function chris/preparation-function
:recursive nil
:with-broken-links t
:with-author nil
:with-timestamps nil
:section-numbers nil
;; :html-preamble chris/site-premble
:html-postamble nil
:html-validation-link nil
:exlude-tags "revealonly\\|noexport"
:html-head-include-default-style nil
:html-doctype "html5"
:html-html5-fancy t
:html-self-link-headlines t
:htmlized-source t
:html-preamble-format (("en" "<nav>
<ul>
<li><strong><a href=\"/\">Chris Cochrun</a> > %t</strong></li>
</ul>
<ul>
<li><a href=\"/notes\">Notes</a></li>
<li><a href=\"/teaching\">Teaching</a></li>
</ul>
</nav>"))
:html-head "<link rel=\"stylesheet\" href=\"../static/pico.snazzy.css\" type=\"text/css\"/>"
:publishing-function org-html-publish-to-html)
("static"
:base-directory "~/docs/site/assets/"
:base-extension "css\\|txt\\|jpg\\|gif\\|png\\|webp\\|webm\\|mp4\\|js\\|html"
:recursive t
:publishing-directory "~/docs/site/public/static/"
:publishing-function org-publish-attachment)
("index"
:base-directory "~/docs/site/content"
:title-of-project "index"
:base-extension "org"
:preparation-function chris/site-index-update
:recursive nil
:with-broken-links t
:with-author nil
:with-timestamps nil
:with-toc nil
:section-numbers nil
;; :html-preamble chris/site-premble
:html-preamble-format (("en" "<nav>
<ul>
<li><strong>Chris Cochrun</strong></li>
</ul>
<ul>
<li><a href=\"/notes\">Notes</a></li>
<li><a href=\"/teaching\">Teaching</a></li>
</ul>
</nav>"))
:html-postamble nil
:html-validation-link nil
:exlude-tags "revealonly\\|noexport"
:html-head-include-default-style nil
:html-doctype "html5"
:html-html5-fancy t
:html-self-link-headlines t
:htmlized-source t
:html-head "<link rel=\"stylesheet\" href=\"./static/pico.snazzy.css\" type=\"text/css\"/>
<style>
li:nth-child(n+7) {
display: none;
}
</style>"
:publishing-directory "~/docs/site/public/"
:publishing-function org-html-publish-to-html)
("cochrun.xyz" :components ("index" "notes" "static" "teaching"))))
(defun quick/org-publish-projects (projects &optional workers)
"Publish all files belonging to the PROJECT.
If `:auto-sitemap' is set, publish the sitemap too. If
`:makeindex' is set, also produce a file \"theindex.org\".
If the optional argument WORKERS is used, this many instances
of emacs will be used in order to publish the files, else
it will use the correct amount of "
(dolist (project (org-publish-expand-projects projects))
(let ((plist (cdr project))
(create-lockfiles nil)
(org-publish-timestamp-directory
(concat (xdg-cache-home) "/org/timestamps/")))
(let ((fun (org-publish-property :preparation-function project)))
(cond
((functionp fun) (funcall fun plist))
((consp fun) (dolist (f fun) (funcall f plist)))))
;; Each project uses its own cache file.
(org-publish-initialize-cache (car project))
(when (org-publish-property :auto-sitemap project)
(let ((sitemap-filename
(or (org-publish-property :sitemap-filename project)
"sitemap.org")))
(org-publish-sitemap project sitemap-filename)))
;; Publish all files from PROJECT except "theindex.org". Its
;; publishing will be deferred until "theindex.inc" is
;; populated.
;; (message "%s" (org-publish-get-base-files project))
;; (message "%s" project)
(let* ((theindex
(expand-file-name "theindex.org"
(org-publish-property :base-directory project)))
(attachment-function? (eq #'org-publish-attachment
(org-publish-property :publishing-function project)))
(org-export-filter-options-functions '(#'chris/ignore-export-name-filter-function))
(workers (if workers workers
(* 2 (comp--effective-async-max-jobs))))
(files (org-publish-get-base-files project))
(chunks (if (> workers (length files))
files
(seq-split files (ceiling (/ (length files) workers)))))
(futures (unless attachment-function?
(cl-loop for chunk in chunks
collect (async-start
`(lambda ()
(require 'ox)
(require 'ox-html)
(require 'htmlize)
(let ((org-publish-use-timestamps-flag t)
(org-publish-timestamp-directory
(concat (xdg-cache-home) "/org/timestamps/"))
(create-lockfiles nil)
(org-publish-cache (org-publish-initialize-cache
(car ',project)))
(org-export-filter-options-functions ',org-export-filter-options-functions)
(files (if (listp ',chunk)
',chunk
(list ,chunk))))
(dolist (file files)
(unless (file-equal-p file ,theindex)
(org-publish-file file ',project))))))))))
(message "=========================")
(message "Starting project: %s" (car project))
(message "=========================")
(when attachment-function? (message "OOOOPS AATTTTAAAACH"))
(if attachment-function?
(dolist (file files)
(unless (file-equal-p file theindex)
(org-publish-file file project t)))
(mapcar #'async-wait futures))
;; Populate "theindex.inc", if needed, and publish
;; "theindex.org".
(when (org-publish-property :makeindex project)
(org-publish-index-generate-theindex
project (org-publish-property :base-directory project))
(org-publish-file theindex project t)))
(let ((fun (org-publish-property :completion-function project)))
(cond
((functionp fun) (funcall fun plist))
((consp fun) (dolist (f fun) (funcall f plist))))))))
(defun quick/org-async-function (files project theindex)
"Publish all files with only knowing the files"
(dolist (file files)
(unless (file-equal-p file theindex)
(org-publish-file file project t))))
(defun quick/org-publish (project &optional force async)
"Publish PROJECT. This is a more aggressively async version of
`org-publish'.
PROJECT is either a project name, as a string, or a project
alist (see `org-publish-project-alist' variable).
When optional argument FORCE is non-nil, force publishing all
files in PROJECT. With a non-nil optional argument ASYNC,
publishing will be done asynchronously, using as many workers
on your machine as makes sense."
(interactive
(list (assoc (completing-read "Publish project: "
org-publish-project-alist nil t)
org-publish-project-alist)
current-prefix-arg))
(let ((project (if (not (stringp project)) project
;; If this function is called in batch mode,
;; PROJECT is still a string here.
(assoc project org-publish-project-alist))))
(cond
((not project))
(async
(let ((org-publish-use-timestamps-flag
(and (not force) org-publish-use-timestamps-flag)))
;; Expand components right now as external process may not
;; be aware of complete `org-publish-project-alist'.
(quick/org-publish-projects
(org-publish-expand-projects (list project)))))
(t (save-window-excursion
(let ((org-publish-use-timestamps-flag
(and (not force) org-publish-use-timestamps-flag)))
(org-publish-projects (list project))))))))
(quick/org-publish "cochrun.xyz" nil t)
(httpd-serve-directory "~/docs/site/public")