newline

Writing your own (derived) Emacs Org export backend

Emacs

September 27, 2024

I frequently use Emacs to write things that eventually need to be entered in some other tool (Jira requirements, Confluence pages, etc.), mainly because of Org mode’s great support for text format conversions (and it’s more comfortable for writing). It means I can just write things in Org mode, then export them to a temporary buffer, and copy-paste them to whatever tool they need to end up in. For Confluence, there’s already a very nice Org export backend, as part of the org-contrib package. If you use that to export to a temporary buffer, yank it, then press control-shift-d in Confluence, and paste it in to get nicely formatted text – that already feels a bit like a superpower. However, I noticed the export is lacking some things that I wanted, and including some things I didn’t want. I could make changes to its code, open a pull request, and get it upstreamed; that’s more work though, requires a response from the owner, and I’m not sure if other users even want these changes. I could fork it, but then I have to maintain a fork. Emacs is extensible, so there’s a better way to do this, as part of my config – here’s how.

First, here’s what I want to change. When ox-confluence creates an export, it includes the Emacs theme in code blocks; I don’t want that, I just want the default Confluence theme. I also want to translate those expandable Org drawers (:DRAWER:...:END:) to Confluence expand macros, which create hidden-by-default sections that you can click to expand (in Confluence markup, {expand:DRAWER}...{expand}). So what I want to do is create a derived backend: an export backend that uses some existing backend, and modifies it.

I’m assuming you know Emacs and Elisp, I won’t cover the basics here. I’m using GNU Emacs 29.4.

Let’s start by installing the org-contrib package, which contains ox-confluence; do that in whichever way you install Emacs packages (I use use-package). Then, start changing the Emacs config file (typically ~/.emacs.d/init.el or ~/.config/emacs/init.el). Add this:

(require 'org) ; only needed if you haven't loaded Org yet
(require 'ox-confluence)

That loads the code for ox-confluence (ox means “org export”).

Next, we define the new backend, calling it confluence-ext for ‘confluence extended’:

(org-export-define-derived-backend 'confluence-ext 'confluence
  :translate-alist '((drawer . za/org-confluence-drawer))
  :filters-alist '((:filter-src-block . za/org-confluence--code-block-remove-theme))
  :menu-entry
  '(?F "Export to Confluence (ext)"
       ((?F "As Confluence buffer (ext)" za/org-confluence-export-as-confluence))))

That function call means: we define a derived backend “confluence-ext”, based on the existing parent backend “confluence”. We give it a list of translation functions, a list of filters, and a menu entry definition.

The :translate-alist is what dictates how parts of org buffers get converted to the output format. It’s an alist of an org element symbol, and the corresponding function with which to translate the element. For example, here we say that any drawer elements should be translated with the za/org-confluence-drawer function (we’ll define that later).

The :filters-alist is similar, except it’s an alist of symbols and functions that post-process any translated elements (“filters”). So in this case, the src-block element will be translated with the parent backend (confluence), and then filtered through za/org-confluence--code-block-remove-theme (we’ll also define that later). Any elements not mentioned in this alist will be translated according to the parent backend (in this case, confluence). The best way (in my opinion) to find out what elements are recognized is to look at an existing backend, e.g. the ascii backend.

We also give the new backend a menu entry in the Org export dispatcher, so we can call it with C-c C-e F F. The :menu-entry calls the entrypoint za/org-confluence-export-as-confluence. Let’s define that, continuing in the same init file:

(defun za/org-confluence-export-as-confluence
  (&optional async subtreep visible-only body-only ext-plist)
  (interactive)
  (org-export-to-buffer 'confluence-ext "*org CONFLUENCE Export*"
    async subtreep visible-only body-only ext-plist (lambda () (text-mode))))

We define a function that takes a bunch of options, passed in automatically through the Org Export dispatcher. It only calls org-export-to-buffer, using our confluence-ext to export to a new temporary buffer *org CONFLUENCE Export*, passing in the options from the dispatcher, and setting the resulting buffer to text-mode.

Now, we have to define the custom element-processing functions, i.e. how to actually convert an org element to our desired output. First, in the backend definition, we referenced a za/org-confluence-drawer function to process drawers; let’s define that:

(defun za/org-confluence-drawer (drawer contents _info)
  "Handle custom drawers"
  (let ((name (org-element-property :drawer-name drawer)))
    (concat
     (format "\{expand:%s\}\n" name)
     contents
     "\{expand\}")))

The function gets the drawer element, its contents, and the info “communication channel” as arguments (I don’t use the last one, so I prefix it with an underscore to indicate that). First, we grab the name of the drawer from the element (that’s the “THING” in :THING:...:END:), and then we output an expand macro that Confluence understands. That’s really all you need to translate an element, at least in the simple cases – take an Org element, build a string from it, and return the string. Technically the concat is extra, I could do everything with format, but I prefer to split it logically like this.

The other thing I wanted to do was to remove the theme from generated code blocks. However, I don’t want to completely override the export function for code blocks that ox-confluence defines; I just want to modify its output. Hence, I use put it in the :filter-alist instead of the :translate-alist, and define it like this:

(defun za/org-confluence--code-block-remove-theme (block _backend _info)
  "Remove the theme from the block"
  (replace-regexp-in-string (rx "\{code:theme=Emacs" (? "|")) "\{code:" block))

It’s pretty straightforward, just a regexp replace. Filter functions take three arguments (the element, the name of the backend, and the “communication channel” info), but I only need the block itself. Sidenote, here I use rx to build a regular expression; if you don’t know it, it’s an intuitive DSL for creating regular expressions and it’s amazing, it’s part of Emacs and one of the things I really miss outside of Emacs.

And that’s it: reload your Emacs config (or evaluate the expressions manually), and you should be able to use the new backend in an Org buffer by pressing C-c C-e F F.