Releasing pandoc-amsthm v3
pandoc-amsthm v3 is released!
pandoc-amsthm is a pandoc filter that reimplements the functionality of the LaTeX amsthm package, but makes it available to any pandoc output format. In LaTeX output it uses amsthm itself. It aims to be general: anything you can do with amsthm in LaTeX, you should be able to do with pandoc-amsthm, including defining arbitrary environments.
I originally wrote it in 2016, and it has gone through three iterations. v1 was built on pandocfilters, with a less ergonomic UX: it supported HTML and LaTeX only, and you needed to include the right templates and header-includes here and there. In 2022 I reimplemented it using panflute as a standalone pandoc filter, which hugely improved the UX. v3 is reimplemented again as a Lua filter, and fixes multiple bugs of v2 along the way.
Why the migration? pandoc’s filter ecosystem is very useful, except when the pandoc API changes and the filter frameworks (pandocfilters, panflute, etc.) need to catch up. Implementing it in Lua with no external dependencies makes it simpler. It also eliminates the conversion between pandoc’s internal AST and the JSON AST, so it should generally be more performant.
v3 is also agentically engineered, carried across the finish line by Claude Opus 5.5, released today.1 It is a test case for me to explore how agents nowadays can pretty much autonomously translate between languages, as in the Bun rewrite I cited in the digression of my JIT talk. Of course this is a relatively trivial example, at under 1k lines of Lua.
It might also be a perfect example of the implementation not mattering. It started as a faithful port of v2, and when I asked Opus 5.5 for a code review, it caught some bugs, which were fixed along the way.2 Why don’t I care about the implementation? Because when I first implemented it, I reverse engineered amsthm and tried to match all of its behaviour with native pandoc constructs. I didn’t design amsthm, and that’s why my implementation had latent bugs. The implementation details can be complicated, e.g. keeping our own internal counters so that numbering such as Theorem 3.2 works across chapters. But the important properties really are:
- provide an interface to define everything definable in amsthm;
- map that to amsthm itself in LaTeX output (the special case);
- reimplement the expected behaviour in all formats (the general case).
One of the checks I did in the old days is that the general case in LaTeX output, where pandoc-amsthm emits plain commands such as emphasis, should match the special case, which calls amsthm. Given these properties, it is closer to the Bun rewrite than to, let’s say, reimplementing pantable, whose behaviour I alone designed. There I’d like to retain more control over the translation, if we ever do that.
As a demo, below is adapted from the recent counterexample to the Jacobian conjecture (“A Counterexample to the Jacobian Conjecture” 2026), which you can find digested in Tao (2026). The motivation is in the title of John D. Cook’s post on it: locally everywhere does not imply everywhere (Cook 2026). This page is rendered by Quarto with the extension installed by quarto add ickc/pandoc-amsthm. See the documentation for the details.
Definition 1. A polynomial map \(F = (F_1, \dots, F_n) : \mathbb{C}^n \to \mathbb{C}^n\) is a map whose \(n\) components \(F_i\) are polynomials in \(n\) complex variables \(z_1, \dots, z_n\). Its Jacobian matrix \(DF\) is the \(n \times n\) matrix of partial derivatives, \((DF)_{ij} = \partial_j F_i\), and its Jacobian is the determinant \(\det DF\).
Definition 2. \(F\) is locally invertible at a point \(p\) if it has a differentiable inverse on some neighbourhood of \(p\), and locally invertible everywhere if it is so at every point of \(\mathbb{C}^n\). \(F\) is globally invertible if it has an inverse \(G : \mathbb{C}^n \to \mathbb{C}^n\), i.e. \(F \circ G\) and \(G \circ F\) are the identity.
Theorem 1 (Inverse function theorem). \(F\) is locally invertible at \(p\) if and only if \(\det DF(p) \neq 0\).
Proof. “If” is the inverse function theorem. For “only if”, differentiate \(G \circ F = \mathrm{id}\) at \(p\) using the chain rule, \(DG(F(p)) \, DF(p) = I\), so \(\det DF(p) \neq 0\).◻
Lemma 2. If \(F\) is a polynomial map whose Jacobian vanishes nowhere on \(\mathbb{C}^n\), then \(\det DF\) is a non-zero constant.
Proof. \(\det DF\) is itself a polynomial in \(n\) complex variables. If it were not constant, fixing all but one of the variables at suitable values would leave a non-constant polynomial in one variable, which has a zero by the fundamental theorem of algebra.◻
Conjecture 3 (Jacobian conjecture). If a polynomial map \(F : \mathbb{C}^n \to \mathbb{C}^n\) is locally invertible everywhere, then it is globally invertible, with polynomial inverse.
Corollary 4. A polynomial map \(F\) is locally invertible everywhere if and only if \(\det DF\) is a non-zero constant. So Conjecture 3 can be stated as: if \(\det DF\) is a non-zero constant, then \(F\) is invertible, with polynomial inverse.
Proof. By Theorem 1, \(F\) is locally invertible everywhere if and only if \(\det DF\) vanishes nowhere, which by Lemma 2 means it is a non-zero constant.◻
Theorem 5 (Alpöge, with Claude Fable 5). Conjecture 3 is false for \(n = 3\), and hence for every \(n \geq 3\). Take \[ \begin{aligned} F(z_1,z_2,z_3) = \big(&(1+z_1 z_2)^3 z_3 + z_2^2 (1+z_1z_2) (4+3z_1z_2), \\ &z_2 + 3 z_1 (1+z_1z_2)^2 z_3 + 3 z_1 z_2^2 (4+3z_1z_2), \\ &2 z_1 - 3 z_1^2 z_2 - z_1^3 z_3\big). \end{aligned} \] Then \(\det DF = -2\), so \(F\) is locally invertible everywhere, but \(F\) is not globally invertible, with a polynomial inverse or otherwise.
Proof. A brief calculation gives \(\det DF = -2\), and \[F(0,0,-1/4) = F(1,-3/2,13/2) = F(-1,3/2,13/2) = (-1/4,0,0),\] so \(F\) is not injective. For \(n > 3\), extend \(F\) by the identity in the remaining variables.◻
Remark. While Theorem 5 is quick to verify, it looks like a miracle. \(F\) has degree 7, so a priori \(\det DF\) could have degree up to \(3 \times 6 = 18\). All of its non-constant coefficients vanishing is \(\binom{21}{3} - 1 = 1329\) equations, against the \(3 \times \binom{10}{3} = 360\) degrees of freedom of a generic degree 7 polynomial map in three variables.
Remark. Conjecture 3 remains open for \(n = 2\).
P.S. I was a bit disappointed to discover that Quarto now has a theorem system of its own, with a few predefined environments only. It claims syntax pandoc-amsthm has been using since 2016, such as the proof class, and there is no option to turn that off. Since I now use Quarto to generate this site, I need to avoid the clashes: under Quarto, v3 runs before Quarto’s own processing and converts a proof div into its own amsthm-proof internally. The details are in paper cuts when composing with Quarto.
References
Footnotes
The port started back in June in a Claude cloud session, presumably with Opus 4.8, the latest Opus at the time, and I then got that branch into shape with Opus 5. Why today? Not because of Opus 5.5. It so happened that Dependabot alerted me to an issue in
nbconvert, a dependency of this package that it wasn’t even actively using.↩︎Opus 5 didn’t flag them, but I didn’t ask it to review either.↩︎