Publish technical notes using Emacs org-mode

This document is a literate version of my publishing script on publishing my technical notes (that uses org-mode).

First let’s import required libraries, which are org related.

(require 'org)
(require 'ox)
(require 'ox-publish)
(require 'org-man)
(require 'org-git-link)

To get syntax coloring on other mode than emacs-lisp, we need to load them too1.

(require 'go-mode)
(require 'css-mode)
(require 'js2-mode)
(require 'rust-mode)
(require 'typescript-mode)
(require 'python-mode)
(require 'nix-mode)
(require 'ob-http)
(require 'yaml-mode)

Now let’s define variables to use later on. Those variables are where to find the project we want to publish and where to publish theme.

(setq org-directory "~/desktop/org/")
(setq site-directory "~/desktop/sites/")

(setq org-default-technical-dir (expand-file-name "technical" org-directory))
(setq org-default-technical-sbr-dir (expand-file-name "sbr.pm" org-default-technical-dir))
(setq org-default-publish-sbr (expand-file-name "sbr.pm" site-directory))
(setq org-default-publish-technical (expand-file-name "technical" org-default-publish-sbr))

We want to republish everything every time — mainly to be used on CI. So we’re telling org-mode to disable the timestamp-based cache.

(setq org-publish-use-timestamps-flag nil)

Let’s also customize a bit the style of what org-mode will generate.

I want clickable headlines, so let’s define a html-format-headline-function for the exporter to use. Not tangle currently as it doesn’t work 😓.

(defun my-org-html-format-headline-function (todo todo-type priority text tags info)
  "Format a headline with a link to itself."
  (let* ((headline (get-text-property 0 :parent text))
         (id (or (org-element-property :CUSTOM_ID headline)
                 (org-export-get-reference headline info)
                 (org-element-property :ID headline)))
         (link (if id
                   (format "<a href=\"#%s\">%s</a>" id text)
                 text)))
    (org-html-format-headline-default-function todo todo-type priority link tags info)))
(setq org-html-format-headline-function 'my-org-html-format-headline-function)

I also use some custom links… They are define in my emacs configuration repository : setup-org.el.

(org-link-set-parameters "tag"
                         :follow #'endless/follow-tag-link)
(defun endless/follow-tag-link (tag)
  "Display a list of TODO headlines with tag TAG.
With prefix argument, also display headlines without a TODO keyword."
  (org-tags-view (null current-prefix-arg) tag))

(org-link-set-parameters "grep"
                         :follow #'vde/follow-grep-link
                         :face '(:foreground "DarkRed" :underline t))
(defun vde/follow-grep-link (regexp)
  "Run `rgrep' with REGEXP and FOLDER as argument,
like this : [[grep:REGEXP:FOLDER]]."
  (setq expressions (split-string regexp ":"))
  (setq exp (nth 0 expressions))
  (grep-compute-defaults)
  (if (= (length expressions) 1)
      (progn
        (rgrep exp "*" (expand-file-name "./")))
    (progn
      (setq folder (nth 1 expressions))
      (rgrep exp "*" (expand-file-name folder))))
  )

(org-link-set-parameters "rg"
                         :follow #'vde/follow-rg-link
                         :face '(:foreground "DarkGreen" :underline t))
(defun vde/follow-rg-link (regexp)
  "Run `ripgrep-regexp` with REXEP and FOLDER as argument,
like this : [[pt:REGEXP:FOLDER]]"
  (setq expressions (split-string regexp ":"))
  (setq exp (nth 0 expressions))
  (if (= (length expressions) 1)
      (progn
        (ripgrep-regexp exp (expand-file-name "./")))
    (progn
      (setq folder (nth 1 expressions))
      (ripgrep-regexp exp (file-name-as-directory (expand-file-name folder)))))
  )

(org-link-set-parameters "gh"
                         :follow #'vde/follow-gh-link
                         :export #'vde/org-gh-export
                         :face '(:foreground "DimGrey" :underline t))
(defun vde/org-gh-export (link description format)
  "Export a github page link from Org files."
  (let ((path (vde/gh-get-url link))
        (desc (or description link)))
    (cond
     ((eq format 'html) (format "<a hrefl=\"_blank\" href=\"%s\">%s</a>" path desc))
     ((eq format 'latex) (format "\\href{%s}{%s}" path desc))
     ((eq format 'texinfo) (format "@uref{%s,%s}" path desc))
     ((eq format 'ascii) (format "%s (%s)" desc path))
     (t path))))
(defun vde/follow-gh-link (issue)
  "Browse github issue/pr specified"
  (browse-url (vde/gh-get-url issue)))

(defun vde/gh-get-url (path)
  "Translate org-mode link `gh:foo/bar#1' to github url."
  (setq expressions (split-string path "#"))
  (setq project (nth 0 expressions))
  (setq issue (nth 1 expressions))
  (format "https://github.com/%s/issues/%s" project issue))

(org-link-set-parameters
 "org"
 :complete (lambda () (+org-link-read-file "org" org-directory))
 :follow   (lambda (link) (find-file (expand-file-name link org-directory)))
 :face     (lambda (link)
             (if (file-exists-p (expand-file-name link org-directory))
                 'org-link
               'error)))
(defun +org-link-read-file (key dir)
  (let ((file (read-file-name (format "%s: " (capitalize key)) dir)))
    (format "%s:%s"
            key
            (file-relative-name file dir))))

Now, let’s define projects.

(setq org-publish-project-alist
      `(("sbr-index"
         :base-directory ,org-default-technical-sbr-dir
         :base-extension "org"
         :publishing-directory ,org-default-publish-sbr
         :publishing-function org-html-publish-to-html
         :html-checkbox-type 'html
         :html-preamble "<div id=\"nav\">
<ul>
<li><a href=\"/\" class=\"home\">Home</a></li>
<li><a href=\"./\" class=\"index\">Index</a></li>
<li><a href=\"../\" class=\"up\">Up</a></li>
</ul>
</div>"
         :html-postamble "<p class=\"creator\">%c</p><p class=\"postamble\">%a. Last Updated %C (exported %T).</p>")
        ("technical-notes"
         :base-directory ,org-default-technical-dir
         :base-extension "org\\|txt"
         :publishing-directory ,org-default-publish-technical
         :recursive t
         :publishing-function org-html-publish-to-html
         :headline-levels 4
         :auto-preamble t
         :auto-sitemap t
         :with-footnotes t
         :with-toc nil
         :with-drawers t
         :exclude "_setup.org\\|sbr.pm"
         :html-checkbox-type 'html
         :html-preamble "<div id=\"nav\">
<ul>
<li><a href=\"/\" class=\"home\">Home</a></li>
<li><a href=\"./\" class=\"index\">Index</a></li>
<li><a href=\"../\" class=\"up\">Up</a></li>
</ul>
</div>"
         :html-postamble "<p class=\"creator\">%c</p><p class=\"postamble\">%a. Last Updated %C (exported %T).</p>")
        ("technical-static"
         :base-directory ,org-default-technical-dir
         :base-extension "html\\|xml\\|css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf\\|md\\|tar\\|gz\\|xz\\|zip\\|csv"
         :publishing-directory ,org-default-publish-technical
         :recursive t
         :publishing-function org-publish-attachment
         )
        ("technical" :components ("technical-notes" "technical-static" "sbr-index"))
        ))

I want the sites folder to be cleaned too, so let’s define a function to be called that will clean a folder and publish.

(defun clean-and-publish ()
  "Clean the site folder and publish all projects"
  (delete-directory org-default-publish-technical t)
  (org-publish-all))

Let’s now create a Makefile so that I can just run make publish to automate that.

publish.el: publish.org
        emacs --batch --eval "(require 'ob-tangle)" --eval '(org-babel-tangle-file "publish.org")'

publish: publish.el
        emacs -batch --load publish.el --eval '(clean-and-publish)'

The rest is just automation 👼. The ~/desktop/sites/ folder is synced across a bunch of my machines, including my servers (using syncthing). This means almost as soon as I run this publish script, it will be available.

Footnotes:

1

Otherwise, emacs doesn’t apply any syntax coloring.

Emacs 24.3.50.3 (Org mode 8.0.3)

Vincent Demeester. Last Updated 2019-03-18 Mon 09:51 (exported 2019-07-12 Fri 16:58).