>> Problem

So in making the page for my watch list for the other watcher blog, I have to convert my unofficial list in Org-Table format to a YAML format. There are other default formats such as CSV that Jekyll reads but in keeping with the spirit, I opt to do it as such. The sad thing about this though is that there is no YAML exporter for org-table-export, so I made my own. Here is the snippet to do and a demonstration.

 

And to use it, you point the mark at a table and use org-table-export and at the format selection you type fn/orgtbl-to-yaml. With Jekyll, you have to export it to your _data Jekyll directory to work as stated by the manual. But of course, we are here to explore further to what it means.

>> Org-Table Export

Initially, I thought I had to create a table parser and transformer but there is usually a command or function that does what you want albeit with a little tweaking. The command org-table-export is our friend here but it is lacking when presented with the format: CSV, TSV, HTML, LaTeX and others but not YAML. One thing to look at where this option comes from is org-export-backends but might be overkill to configure. As an caveat, if you add a backend to it you need to run this script.

(progn
  (setq org-export-registered-backends
     (cl-remove-if-not
      (lambda (backend)
        (let ((name (org-export-backend-name backend)))
          (or (memq name val)
             (catch 'parentp
               (dolist (b val)
                 (and (org-export-derived-backend-p b name)
                    (throw 'parentp t)))))))
      org-export-registered-backends))
  (let ((new-list (mapcar #'org-export-backend-name
                        org-export-registered-backends)))
    (dolist (backend val)
      (cond
       ((not (load (format "ox-%s" backend) t t))
        (message "Problems while trying to load export back-end `%s'"
                 backend))
       ((not (memq backend new-list)) (push backend new-list))))
    (set-default 'org-export-backends new-list)))

Something simple would be better for our cause. Is crafting a custom exporter instead an option? Thankfully, the manual tells us how to create a simple custom exporter according to this link:

(defun orgtbl-to-language (table params)
  "Convert the orgtbl-mode TABLE to language."
  (orgtbl-to-generic
   table
   (org-combine-plists
    '(:tstart "!BTBL!" :tend "!ETBL!" :lstart "!BL!" :lend "!EL!" :sep "\t")
    params)))

After reading the documentation, we are interested in two properties: :skip and :lfmt. The later is more important which controls how a line is formatted. It accepts a function which takes a list of row values and returns the record line it represents. Using it as function, the first value are the row headers and the remaining row values. We mainly want to focus the function on the value so that is why have :skip to ignore the first header row. Aside from that, we can craft our row formatter.

Since this is YAML, our record line is a bit more complex. Each record starts with a dash and space, ends with a newline, separated by a newline and two spaces for alignment. Each value must be in a <key>: <value> format and escaped by ". With a sample data, it would look something like this:

- id: "1"
  name: "Muffin"
  description: "Khajit Assassin"
- id: "2"
  name: "Marble"
  description: "Barbarian Alchemist"

To accomplish this, we need to get the row headers which can be also found with the car of the table parameter. We then use a functional zip with the headers and each row values, format accordingly and profit. With a quick zip shiv, it looks like this.

(lambda (values)
  (concat
   "- "
   (string-join
    (mapcar
     (lambda (pair)
       (lexical-let ((header (car pair))
           (value (cdr pair)))
         (format "%s: \"%s\"" header value)))
     (funcall zip headers values))
    "\n  ")))

That finishes the core formatter.

>> Front Matter and Jekyll

So this exporter is aimed for Jekyll and I know little about YAML since I rarely use it.

I only want to discuss how the key is produced. You can simply take the key as is but what if it has spaces? I find it odd having spaces within object keys, so I prefer to hyphenate it and lowercase for my ease but obviously you can change it to be camelCase if you want. Using this in Jekyll, it will look like this:

(lexical-let ((headers
     (mapcar
      (lambda (header) ;; Shiv camel casing
        (replace-regexp-in-string
         " " "-"
         (downcase header)))
      (car table))))
  ;; Rest of the code
  )
{% for watch in site.data.official-watch-list %}
<p>{{ watch.field }}</p>
{% endfor %}

As an example on this, my watch list has a Public Rating field and my question is how will you access it with a dot notation? You are welcome to inform me how but I wouldn't like how it would feel and I am comfortable writing watch.public-rating instead. Aside from that I have nothing much to say about the naming convention and quoting the value just in case it gets too long.

>> Default Properties

As a final exploration, let's see how to set the default export options for a table. I thought setting a header property would be good enough but according to org-set-property I have to put it under a header which I find weird since the file represents a single solitary data set but not a big deal. To show that off from my official file:

* Official Watch List
   :PROPERTIES:
   :TABLE_EXPORT_FILE: official-watch-list.yaml
   :TABLE_EXPORT_FORMAT: fn/orgtbl-to-yaml
   :END:

   Yes, I have an unofficial watch list for myself

For my use case when updating my watch list, I want to auto export then publish the table when it is saved just like my org-jekyll-blogger-auto-publish-on-save from org-jekyll-blogger.el. It takes a few minutes to write that glue code:

(defun fn/org-jekyll-blogger-export-and-publish ()
  "Export a table and publish the file accordingly."
  (interactive)
  (if (not (org-at-table-p))
      (if (not (called-interactively-p 'interactive))
          (message "No table at point to publish.")
        (error "Point is not at a table"))
    (org-table-export)

    (lexical-let ((export-file (org-entry-get (point) "TABLE_EXPORT_FILE" t)))
      (if (not export-file)
          (message "No TABLE_EXPORT_FILE property")
        (lexical-let ((export-buffer (find-file-noselect export-file)))
          (with-current-buffer export-buffer
            (org-publish-current-file))
          (message "Table published."))))))

(defun fn/org-jekyll-blogger-auto-publish-table-on-save ()
  "Auto export and publish table on save."
  (interactive)
  (add-hook 'after-save-hook #'fn/org-jekyll-blogger-export-and-publish t t))

Not the best code but it does the job and not that hard to write although I had to peek at org-table-export to determine the name of the file. A small point in this code is using called-interactively-p which merely indicates if the containing function is called as a command(via M-x or execute-extended-command) or a function(via Elisp); this allows me to either throw an error message or an info message depending on the context. For example, using it directly you should get an error that you need to mark what table you should export with the point; but if it is called by a hook, you wouldn't want it to throw an error since there might be other hooks in play so better an info message.

>> Conclusion

The two things I deliberately ignored are performance and escaping but I pray I not see more than 10,000 visual experiences before I worry about it. With this base, you can come up with a JSON exporter from a quick read. For me, I can continue to work with my org workflow and not worry about the export format.