Cross references in Ulysses

Thomas Mailund
3 min readApr 9, 2019

I am trying out Ulysses again. I gave up on it some years ago because of two things: they started a subscription-based model — and I found it a bit unfair since I had paid for the application already — and it fit very poorly into my writing workflows.

I am trying it again now. If I like the editor, then the yearly subscription is worth it. And I am excited about some of the features. I want to be able to add annotations and in-document notes and comments, I like the goals and deadlines support, I love that it is easy to rearrange text and many more things. I have two dislikes that almost negate all the likes, though.

  1. You can either work with Pandoc or you can use all the features of Ulysses.
  2. You have to export to compile with Pandoc, and that is much more cumbersome than it has to be.

You can write all the Markdown that you want, but Ulysses transforms some of it if you export your document. You can avoid this if you quote text. Inline you can use ~ something ~. For blocks you can type , this will give you a block you can write in. You can use this for cross-references as supported by pandoc-crossref. If you have images, you can insert these via plain old Markdown and add a label there. It works fine for sections and such as long as you remember to escape labels, but if you use Ulysses’ built-in support for images, it will not work. You cannot set a label for the image stored in Ulysses.

Actually, there is all the data you need to define and reference a figure label, it is just not setup correctly in the output. That can be fixed with a Pandoc filter.

I found out that you can get the figure caption (not the figure text, we still want that, but a caption that we can exploit). When you export from Ulysses it will put the caption after the figure filename like this:

# This is a simple document.

![A figure text](many-to-many-compilation.pdf "my_test")

If we can make that caption into a label, we are ready for cross-referencing.

If we can translate the figure code into this, we are good.

![A figure text](many-to-many-compilation.pdf "my_test"){#fig:my_test}

Digging a little into a Pandoc filter solution I found that an image holds three values, attributes, inline text and a target. The target contains the figure file and the caption, at position one and two, respectively. The first attribute seems to be where we can put the label. If we do so, then we get desired output document:

This is a simple document.
==========================

![A figure text](many-to-many-compilation.pdf "my_test"){#fig:my_test}

Now, if I pipe my text through the filter below before I do the rest of the Pandoc processing, then I have the best of both worlds. I have my images inside Ulysses, and I can cross-reference figures.

#!/usr/bin/env python3
import sys
from pandocfilters import toJSONFilter, Image

def separator(key, value, format, meta):
if key == "Image":
assert len(value) == 3
attr, inline, target = value
assert len(attr) == 3
assert len(target) == 2

if attr[0] != "":
return None # there is already a label
label = target[1]
if label == "fig:":
# not really a label
return None

attr[0] = label
return Image(attr, inline, target)

else:
# do not change anything.
return None

if __name__ == '__main__':
toJSONFilter(separator)

Writing the filter was sufficiently fun that I wrote it three times…

In Haskell:

import Text.Pandoc.JSON

main:: IO()
main = toJSONFilter extractFigLabel
where
extractFigLabel (Image (id, classes, keyValPairs)
caption (url, title)) =
Image (title, classes, keyValPairs) caption (url,[])
extractFigLabel x = x

Another Python version (using panflute):

from panflute import *

def add_fig_label(elem, doc):
if type(elem) == Image:
elem.identifier = elem.title

def main():
return run_filter(add_fig_label, doc=None)

if __name__ == "__main__":
main()

--

--

Thomas Mailund

Associate professor in bioinformatics and author of a bunch of text books