Dynamic MathJax typesetting in Svelte
(TL;DR;give-me-the-code: Dynamic MathJax rendering — Svelte REPL)
I am building a quiz platform for the startup I am working on right now. The questions in the quiz often contain LaTeX math elements which need to be typeset by MathJax.
The setup
MathJax and its config is brought in with the <svelte:head>
tag, like this:
<svelte:head>
<script>
window.MathJax = {
tex: {
inlineMath: [
["$", "$"],
["\\(", "\\)"],
],
displayMath: [["$$", "$$"]],
},
svg: {
fontCache: "global",
},
startup: {
typeset: false,
},
};
</script>
<script
id="MathJax-script"
defer
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"
></script>
</svelte:head>
I have an array of questions (which are just HTML text with LaTeX expressions)
and a variable called current
, which keeps track of the index of the current
question. There are also couple of buttons to switch between the questions.
<script>
const current = 0;
const questions = [
`This is $ e^x $: <br /> $$ {\\bf{e}}^x = \\sum\\limits_{n = 0}^\\infty {\\frac{{{x^n}}}{{n!}}} $$`,
`And this is $ \\cos x $: <br /> $$ \\cos x = \\sum\\limits_{n = 0}^\\infty {\\frac{{{{\\left( { - 1} \\right)}^n}{x^{2n}}}}{{\\left( {2n} \\right)!}}} $$`,
`Finally, this is $ \\sin x $: <br /> $$ \\sin x = \\sum\\limits_{n = 0}^\\infty {\\frac{{{{\\left( { - 1} \\right)}^n}{x^{2n + 1}}}}{{\\left( {2n + 1} \\right)!}}} $$`,
];
</script>
<svelte:head>
...
</svelte:head>
<main>
<button on:click={() => current--}>Prev</button>
<button on:click={() => current++}>Next</button>
<p> {@html questions[current] } </p>
</main>
The problem
MathJax runs the typesetter just once, as soon as it's loaded on the page. At this stage, the question text has not loaded in yet, so the typesetting doesn't take effect.
We need a way to make this dynamic, so that MathJax will re-typeset the text
when the question number (i.e., value of current
) changes.
The solution
We can use two nifty tools that Svelte provides — the use
directive and the
{#key}
block — to solve this problem.
Implementing use:typesetMath
The use
directive
calls the attached function (called an action) when the element is created.
We can use this to create an action that will check if MathJax has been loaded
and run the formatter (window.MathJax.typesetPromise
) on our element.
<script>
// ... //
// Action functions accept the HTML element as an argument when they are called
function typesetMath(node) {
const typesetPromise = window.MathJax.typesetPromise;
// Check if MathJax has actually loaded before calling `typesetPromise`
if (typesetPromise) typesetPromise([node]);
}
//...//
</script>
...
<p use:typesetMath>{@html questions[current] }</p>
...
Now, we need to make sure that this action is fired whenever the variable
current
changes.
Setting up the {#key}
block
The {#key}
block will cause the elements inside it to re-render whenever the
key (in this case, the variable current
) changes, which then goes ahead and
triggers the typesetMath
action.
{#key current}
<p use:typesetMath>{@html questions[current] }</p>
{/key}
So far so good! Everything should work more-or-less as expected now, but there is one little bug.
The typesetting might not happen properly for the question that is shown by
default, because MathJax might not have been loaded by the time the typesetMath
call is made, and the function exits without doing anything.
To fix this, we can use the else
branch in the conditional in typesetMath
to
call itself recursively (with a small delay) until MathJax is loaded.
function typesetMath(node) {
const typesetPromise = window.MathJax.typesetPromise;
if (typesetPromise) typesetPromise([node]);
// recursively call itself every 500ms until MathJax loads
else setTimeout(() => typesetMath(node), 500);
}
And that's it! That's how I hacked it together. It is not an elegant fix but it works pretty well for now.
Click here for the full code.
<script>
import { afterUpdate } from 'svelte';
let current = 0;
const questions = [
`This is $ e^x $: <br /> $$ {\\bf{e}}^x = \\sum\\limits_{n = 0}^\\infty {\\frac{{{x^n}}}{{n!}}} $$`,
`And this is $ \\cos x $: <br /> $$ \\cos x = \\sum\\limits_{n = 0}^\\infty {\\frac{{{{\\left( { - 1} \\right)}^n}{x^{2n}}}}{{\\left( {2n} \\right)!}}} $$`,
`Finally, this is $ \\sin x $: <br /> $$ \\sin x = \\sum\\limits_{n = 0}^\\infty {\\frac{{{{\\left( { - 1} \\right)}^n}{x^{2n + 1}}}}{{\\left( {2n + 1} \\right)!}}} $$`
]
function typesetMath(node) {
const typesetPromise = window.MathJax.typesetPromise;
if (typesetPromise) typesetPromise([ node ]);
else setTimeout(() => typesetMath(node), 500)
}
</script>
<svelte:head>
<script>
window.MathJax = {
tex: {
inlineMath: [
['$', '$'],
['\\(', '\\)']
],
displayMath: [['$$', '$$']]
},
svg: {
fontCache: 'global'
},
startup: {
typeset: false
}
};
</script>
<script
id="MathJax-script"
defer
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"
></script>
</svelte:head>
<main>
<button disabled={current===0} on:click={() => current--}>Prev</button>
<button disabled={current===questions.length-1} on:click={() => current++}>Next</button>
{#key current}
<p use:typesetMath>
{@html questions[current] }
</p>
{/key}
</main>
I have also created a Svelte REPL if you want to play around with this.
Comments
Comment via ...