Generating ConTeXt documents from Julia with multiple dispatch

I built a Julia pipeline that processes survey data and produces a PDF report for each course: pull the data, build frequency tables and charts with CairoMakie, then compile the final report with ConTeXt. I already knew ConTeXt from before this project, so it was the obvious choice for the typesetting side.

For anyone who doesn’t know it (I’d guess most people here come from LaTeX): ConTeXt is another TeX-based typesetting system, written by Hans Hagen, with a different macro syntax. Instead of \begin{itemize}...\end{itemize} it uses \startitemize/\stopitemize pairs, and the naming is fairly predictable: \setupXXX configures something globally, \defineXXX creates a new environment. A simple example:

\starttext
  \startitemize
    \item One
    \item Two
  \stopitemize
\stoptext

A table:

\bTABLE
  \bTABLEhead
    \bTR
      \bTH Name \eTH
      \bTH Age  \eTH
    \eTR
  \eTABLEhead
  \bTABLEbody
    \bTR
      \bTD Ana \eTD
      \bTD 30  \eTD
    \eTR
  \eTABLEbody
\eTABLE

You can also define your own environments. In my report I needed label/value pairs where the value sometimes runs long and wraps to a second line, and I wanted that second line to stay aligned under the first instead of falling back to the margin. \definedescription handles that out of the box:

\definedescription[metadato][alternative=left, width=6cm, headstyle=\bold]

\startmetadato{Course}
Long course name that might wrap onto a second line
\stopmetadato

One practical advantage over LaTeX I noticed right away: character handling. My whole report is in Spanish, accented vowels, ñ, ¿, ¡ everywhere, and with ConTeXt (the LuaMetaTeX engine) that just works, no encoding declarations needed. With traditional LaTeX you eventually run into inputenc/fontenc, or have to switch to xelatex/lualatex for the same thing.

The problem I had

Generating all of this from Julia by just hacking together strings, things like "\\startbloque[title={$titulo}]" * content * "\\stopbloque", works at first but falls apart fast once the document has any real structure: nested blocks, tables, figures, conditional cases. I’d end up with brackets and parens everywhere and no idea if it was balanced until the compile failed.

What I built

A module (ConTeXtJuliaDocument) with one function per macro I use, each with two methods via multiple dispatch: one that returns the markup as a String, and one that takes an IO and writes straight to it (so I’m not holding giant strings in memory for long documents).

startbloque(setup::String)::String = "\\startbloque[$setup]"
startbloque(io::IO, setup::String) = (write(io, startbloque(setup)); io)

stopbloque()::String = "\\stopbloque"
stopbloque(io::IO)   = (write(io, stopbloque()); io)

To be clear, each function’s implementation is trivial, it’s literally a one-line string interpolation. There’s nothing clever in the code itself, what I want to talk about is the general pattern, not the implementation.

Higher-level functions are built by composing the low-level ones, for example a paragraph:

paragraph(content::String)::String = startparagraph() * content * stopparagraph()
paragraph(io::IO, content::String) = (write(io, paragraph(content)); io)

And here’s what actually using it looks like:

const C = ConTeXtJuliaDocument

C.startbloque(io, "title={Results}")
C.paragraph(io, "Block text.")
C.stopbloque(io)

On top of that I built higher-level things, like a natural_table function that assembles a whole table from a header, a matrix, and a footer, without the caller needing to know anything about \bTABLE.

Something I really liked about multiple dispatch for this specific case: ConTeXt macros don’t have one fixed way of being called, and it’s not just “more or fewer arguments.” Sometimes the meaning changes depending on how many bracket groups you pass. I have a real example of this in my code with definecolorgroup: \definecolorgroup[name] references a group that’s already defined, but \definecolorgroup[name][specification] defines it from scratch, and there the first argument stops being “the settings” and becomes a name/tag instead:

definecolorgroup(name::String) = "\\definecolorgroup[$name]"
definecolorgroup(name::String, setup::String) = "\\definecolorgroup[$name][$setup]"

In another language I’d handle this with optional arguments and an if inside the function checking how many showed up. In Julia each form is just another method, with a different arity, under the same name, and dispatch picks the right one without me asking anything.

One honest note: this isn’t an original idea of mine. ConTeXt already has something similar in Lua (the LuaMetaTeX engine has its own interface for building documents by calling functions that mirror each macro, things like context.startbloque()). What I did is basically the same thing in Julia, leaning on multiple dispatch instead of whatever Lua does under the hood.

Something that’s been nagging at me

I never thought of this as something reusable, it was just for my own project. At some point I wondered if this would actually fit better as Julia macros instead of functions, so I wouldn’t have to pair up start/stop by hand. For example, for a paragraph:

\startparagraph
Paragraph content.
\stopparagraph

could be something like

@paragraph begin
    "Paragraph content."
end

And for something smaller, like a list item (\startitem Content \stopitem), just

@item "Content"

But I never actually tried it, it’s just an idea that’s been rattling around.

One more thing I’m clear on: I only wrapped the handful of macros I actually use in my report. ConTeXt has an enormous number of macros (hundreds, between the core and all the modules), so if this were ever turned into a real library it would need to be extremely consistent in how each wrapper is named and structured, because writing them all by hand one by one doesn’t scale. It would probably need to generate them somehow from ConTeXt’s own definitions, instead of copying macro by macro the way I did.