ox-extend: Structured extensions for ox.el

The ox-extend package requires ox-publish.el to advise org-publish-file and use the org-publish-property:

(require 'ox-publish)

The ox-extend-extensions-alist is an association list for registered extensions: Each extension must register itseld in this list when it’s loaded to be available for use in publishing projects.

(setq ox-extend-extensions-alist '())

Each element consists of a symbol that identifies the extension and a list with an :add function to add the extension’s advise and a :remove function to remove it.

'('ox-html-git-mtime
  :add (lambda () (advice-add 'org-html-title :around #'ox-html-upcase-title))
  :remove (lambda () (advice-remove 'org-html-title #'ox-html-upcase-title))

To extend the functionality of the org-publish-file function, ox-extend--advise provides advise to add and remove the registered extensions. It finds the :add and :remove functions for all extensions added to the project’s :extensions list in the ox-extend-extensions-alist.

For each extenson, the :add function is executed before org-publish-file, and :remove is called immediately after. This adds the extensions when they’re needed, and removes them to prevent them from interfering with future publishing:

(defun ox-extend--advise (orig-fun &rest args)
  "Advise org-publish-file (ORIG-FUN) to add and remove each extension contained in ARGS."
  (let ((extensions (org-publish-property :extensions (nth 1 args))))
    (dolist (extension extensions)
      (ox-extend--apply extension :add))
    (apply orig-fun args)
    (dolist (extension extensions)
      (ox-extend--apply extension :remove))))

(defun ox-extend--apply (extension add_or_remove)
  "Call the ADD_OR_REMOVE function for an EXTENSION."
  (apply (plist-get (cdr (assoc extension ox-extend-extensions-alist))
		    add_or_remove)
	 ()))

The ox-extend-add and ox-extend-remove add and remove the main advise, enabling and disabling all extensions completely. When loading ox-extend.el, ox-extend-add is automatically called to install the main advise. The ox-extend-remove function exists for convenience and shouldn’t have to be used regularly, if ever.

(defun ox-extend-add ()
    (advice-add 'org-publish-file :around #'ox-extend--advise))

(defun ox-extend-remove ()
    (advice-remove 'org-publish-file #'ox-extend--advise))

(ox-extend-add)

Finally, use the provide fucntion to announce ox-extend.el:

(provide 'ox-extend)

Example

The hello world example adds advise to print “hello, world!” before org-publish-file is called, and “goodbye, world!” after.

Start by loading ox-extend.el by adding its directory to the load-path and calling require:

(add-to-list 'load-path ".")
(require 'ox-extend)

With ox-extend.el loaded, add the extension to ox-extend-extensions-alist by passing an association list with an :add and a :remove key. Instead of actually altering the publishing behavior, both functions print a message to the *Messages* buffer:

(add-to-list
 'ox-extend-extensions-alist '('hello-world :add (lambda () (message "hello, world!"))
					    :remove (lambda () (message "goodbye, world!"))))

To try the extension, call org-publish-file with a project that lists 'hello-world as one of its :extensions:

(org-publish-file "ox-extend.org"
		  '("ox-extend-example"
		    :base-directory "."
		    :publishing-directory "dist"
		    :extensions ('hello-world)))

The *Messages* buffer prints both messages:

hello, world!
Publishing file ox-extend.org using ‘org-html-publish-to-html’
goodbye, world!

Writing extensions

As an example, we’re writing an extension named ox-md-title, which adds document titles to markdown files generated with ox-md.

Writing an extension involves advising one or more functions and registering the extension to be available to publishing projects. This extension will advise org-md-template, which currently only returns the generated document contents:

(defun org-md-template (contents _info)
  contents)

Our aim is to prepend the document’s title with the correct markup.

First, require ox-extend. Unlike the example above, actual extensions don’t alter the load path:

(require 'ox-extend)

Then, write an :around advice for org-md-template, which gets the original funcion and arguments. In the function body, we call out to org-md--headline-title to generate the title with arguments we find from the second argument in args:

(defun ox-md-title--advise-template (orig-fun &rest args)
  (let ((info (nth 1 args)))
    (let ((style (plist-get info :md-headline-style))
	  (title (org-export-data (plist-get info :title) info)))
      (concat
       (org-md--headline-title (plist-get info :md-headline-style) 1 (org-export-data (plist-get info :title) info) nil)
       (apply orig-fun args)))))

To enable the extension, add ox-md-title-add and ox-md-title-remove and add them to the ox-extend-extensions-alist. We’re also setting the org-md-toplevel-hlevel to 2, as the extension adds an extra headline to the root of the page:

(defun ox-md-title-add ()
  (setq org-md-toplevel-hlevel 2)
  (advice-add 'org-md-template :around #'ox-md-title--advise-template))

(defun ox-md-title-remove ()
  (setq org-md-toplevel-hlevel 1)
  (advice-remove 'org-md-template #'ox-md-title--advise-template))

(add-to-list
 'ox-extend-extensions-alist '('ox-md-title :add ox-md-title-add
					    :remove ox-md-title-remove))

Finally, provide the ox-md-title.el package:

(provide 'ox-md-title)

And use the extension when publishing1:

(add-to-list 'load-path ".")
(require 'ox-md-title)

(org-publish-file "ox-extend.org"
		  '("ox-extend-markdown"
		    :base-directory "."
		    :publishing-directory "."
		    :publishing-function org-gfm-publish-to-gfm
		    :extensions ('ox-md-title)))

The exporter now prepends the document title in the Markdown export:

head ox-extend.md

# ox-extend: Structured extensions for ox.el

- [Example](#example)
- [Writing extensions](#writing-extensions)

The `ox-extend` package requires `ox-publish.el` to advise `org-publish-file` and use the `org-publish-property`:

```emacs-lisp
(require 'ox-publish)

  1. : This publishing project uses ox-gf instead of Org’s ox-md. That works because ox-gfm is a derived backend which also uses org-md-template as its template function.

    ↩︎