LaTeX to MathML Rendering
Written by Dominik Pantůček on 2025-02-14
racketlatexmathmljavascriptDuring the years, there were many cryptography-related articles published on our blog. Many of those contain mathematical formulae using LaTeX notation. The time has come to render those again and one JavaScript package turned out to be just the right ingredient we needed.
Originally our website used WordPress where the LaTeX syntax was parsed client-side using MathJax which therefore it needed JavaScript. As we are trying to provide our website without any client-side scripting and MathML is the next big thing, it is only appropriate to convert all the mathematical notation on our site to MathML.
Firstly we tried the LaTeXML package, however as it is written in Perl in a pretty conservative way, it is rather slow implementation. Generating the whole website using this tool took suddenly almost an hour which was not acceptable.
However then we found TEMML and everything turned upside down. Using it was absolutely straightforward, a script that accepts an expression as argv and produces mathml was as simple as the following:
const all_argv = process.argv;
const no_node_argv = /node$/.test(all_argv[0]) ? all_argv.slice(1) : all_argv;
const my_argv = no_node_argv[0] == __filename ? no_node_argv.slice(1) : no_node_argv;
if (my_argv.length != 1) {
console.log("Error. Usage: " + __filename + " LaTeX-Math-Expression");
process.exit(1);
}
const latex_math_expr = my_argv[0];
const is_block = (latex_math_expr[0] == '$') && (latex_math_expr[1] == '$');
const inner_latex_math_expr = latex_math_expr.replace(/^[$]+/, '').replace(/[$]+$/, '');
const temml = require('./temml.cjs');
const mathML = temml.renderToString(inner_latex_math_expr, { displayMode: is_block});
console.log(mathML);
The only remaining problem was - how to integrate it into our Racket-based publishing system. We are not very fond of Javascript, however in the publishing pipeline it should not be much of a hassle. Therefore we have implemented a simple wrapper around node runtime running the script, aptly named "latexmathml.js":
(define (do-string->mathml str)
(define this-program (path->complete-path (resolve-path (find-system-path 'run-file))))
(define-values (base name flag?) (split-path this-program))
(define epath (build-path (path->complete-path base) "node" "latexmathml.js"))
(define nodepath (find-executable-path "node"))
(match-define (list stdout stdin pid stderr pctl)
(process* nodepath epath str))
(close-output-port stdin)
(pctl 'wait)
(define res (port->string stdout))
(close-input-port stdout)
(for ((line (in-lines stderr)))
(displayln (format "mathml: ~a" line)
(current-error-port)))
(close-input-port stderr)
res)
Although it is already pretty fast, there is no need to repeatedly convert the same math expressions. Using the good old delay form and force procedure a simple caching mechanism can be implemented:
(define (string->mathml str)
(define cache (mathml-cache))
(define promise
(call-with-semaphore
(mmlc-sema cache)
(thunk
(define promises (mmlc-promises cache))
(define key (hex-name-digest str))
(cond ((hash-has-key? promises key)
(hash-ref promises key))
(else
(define new-promise
(delay
(do-string->mathml str)))
(set-mmlc-promises! cache (hash-set promises key new-promise))
new-promise)))))
(force promise))
For easy integration in a xexpr-based publishing system some convenience wrappers are useful:
(define (mathml->xexpr str)
(string->xexpr str))
(define (string->mathml-xexpr str)
(mathml->xexpr
(string->mathml str)))
The actual usage for replacing all strings delimited by "" or "" with MathML markup can be as easy as:
(define (string->mathml-list str)
(cond ((wdoc-convert-mathml)
(define lst (regexp-match* #px"[^$]+|[$]+[^$]+[$]+" str))
(for/list ((formula (in-list lst)))
(if (regexp-match #rx"[$]+[^$]+[$]+" formula)
(string->mathml-xexpr formula)
formula)))
(else
(list str))))
In the end it was pretty easy, wasn't it? Hope you enjoyed using node scripts from Racket and see ya next time for more!