Math

The @samplekit/preprocess-katex npm package and samplekit.svelte-pp-katex VS Code extension allow you to use KaTeX directly within your Svelte template.

Feature Overview

  • Write beautiful math in Svelte templates. This preprocessor wraps KaTeX, a package that supports a subset of TeX macros and some higher order LaTeX macros found in packages like amssymb. If you're not familiar with TeX, it's a typesetting system commonly used in STEM fields. These macros let you easily write math like this x=b±b24ac2ax = {-b pm sqrt{b^2 - 4ac} over 2a}. That equation is written like this:
    \( x = {-b \pm \sqrt{b^2 - 4ac} \over 2a} \)
    .
  • Eliminate flashes of unstyled content. By using a preprocessor directly on the server, delays and flashes of unstyled content are avoided. The page can also be statically generated if desired.
  • Work with existing tooling. There's no need to invent a new file extension or disable other tools. By using regular HTML comments in .svelte files we ensure that Prettier, ESLint, svelte-check, svelte.svelte-vscode, etc. leave our code alone. The code is processed into something Svelte understands, but if the preprocessor isn't installed yet, nothing breaks – the code simply turns into an HTML comment.
  • Code with TextMate support. The VS Code extension provides you with full highlighting support. It injects the LaTeX TextMate grammar scope between the delimiters to work with your VS Code theme.

Installation

  1. Install the preprocessor
    pnpm add -D @samplekit/preprocess-katex
  2. Add to svelte.config.js
    import { processKatex, createKatexLogger } from '@samplekit/preprocess-katex';
    import adapter from '@sveltejs/adapter-auto';
    import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
    
    const preprocessorRoot = `${import.meta.dirname}/src/routes/`;
    const formatFilename = (/** @type {string} */ filename) => filename.replace(preprocessorRoot, '');
    
    /** @type {import('@sveltejs/kit').Config} */
    const config = {
    	preprocess: [
    		processKatex({
    			include: (filename) => filename.startsWith(preprocessorRoot),
    			logger: createKatexLogger(formatFilename),
    		}),
    		vitePreprocess(),
    	],
    	kit: {
    		adapter: adapter(),
    	},
    };
    
    export default config;
  3. Install the VS Code extension (for snippets and syntax highlighting) samplekit.svelte-pp-katex

Svelte Template

Display

There are four wrappers around math:

  1. Display math in Svelte templates:
    	
    <!-- \[ ... \] -->
  2. Inline math in Svelte templates:
    	
    <!-- \( ... \) -->
  3. Display math via JavaScript:
    const eq = LaTeX`...`;
  4. Inline math via JavaScript:
    const eq = LaTeX`...`;

Author with the highlighting extension:

<!-- \[
\begin{align}
\dot{x} & = \sigma(y-x) \\
\dot{y} & = \rho x - y - xz \\
\dot{z} & = -\beta z + xy
\end{align}
\] -->

Author without the highlighting extension:

<!-- \[
\begin{align}
\dot{x} & = \sigma(y-x) \\
\dot{y} & = \rho x - y - xz \\
\dot{z} & = -\beta z + xy
\end{align}
\] -->

After preprocessing:

x˙=σ(yx)y˙=ρxyxzz˙=βz+xyegin{align} dot{x} & = sigma(y-x) \ dot{y} & = ho x - y - xz \ dot{z} & = -eta z + xy end{align}

Inline

With highlighting extension:

<!--\( V={4 \over 3}\pi r^{3} \) -->

Without highlighting extension:

<!-- \( V={4 \over 3}\pi r^{3} \) -->

After preprocessing: V=43πr3V={4 over 3}pi r^{3}

JavaScript Template Literal

The VS Code extension also highlights LaTeX template literals. To use it, it's helpful to make a couple simple wrapper components

Inline.svelte
<script lang="ts">
	import { katex } from '..';

	const { eq }: { eq: string } = $props();
</script>

{@html katex.renderToString(eq)}
Display.svelte
<script lang="ts">
	import { katex } from '..';

	const { eq }: { eq: string } = $props();
</script>

<div class="overflow-x-auto">{@html katex.renderToString(eq, { displayMode: true })}</div>
  1. Create an equation with the LaTeX template tagged literal.
  2. Pass the string to either the Display or Math components.
  3. Enjoy regular variable substitution directly in LaTeXLaTeX!

With highlighting extension:

const eq1 = LaTeX`
	x = {-b \pm \sqrt{b^2-4ac} \over 2a}
`;

Without highlighting extension:

const eq1 = LaTeX`
	x = {-b \pm \sqrt{b^2-4ac} \over 2a}
`;

After preprocessing:

x=b±b24ac2ax = {-b \pm \sqrt{b^2-4ac} \over 2a}

Reactivity

This is Svelte, so let's write reactive LaTeX directly in the Svelte template!

r=1r2=1A=πr2=3.14\begin{align*} r &= 1 \\ r^2 &= 1 \\ A = \pi r^2 &= 3.14 \\ \end{align*}

With Template Literals

With template literals, all the backslashes must be escaped. Considering the amount that LaTeX uses, this can be tedious. It also has the drawback of destroying the syntax highlighting.

<script lang="ts">
	let r = $state(1);
	const A = $derived((Math.PI * r ** 2).toFixed(2));

	const eq = $derived(`\\begin{align*}
r &= ${r} \\\\
r^2 &= ${r ** 2} \\\\
A = \\pi r^2 &= ${A} \\\\
\\end{align*}`);
</script>

<Display {eq}></Display>

To get around this, we can use a special LaTeX command directly in the template.

In the Markup

Use \s to define a reactive Svelte variable.

With hardcoded values.

\begin{align*}
r &= 2 \\
r^2 &= 4 \\
A = \pi r^2 &= 12.57 \\
\end{align*}

With Svelte values.

\begin{align*}
r &= \s{r} \\
r^2 &= \s{r ** 2} \\
A = \pi r^2 &= \s{A} \\
\end{align*}

Hardcoded

r=2r2=4A=πr2=12.57egin{align*} r &= {2} \ r^2 &= {4} \ A = pi r^2 &= {12.57} \ end{align*}

Sveltiful!

r=1r2=1A=πr2=3.14egin{align*} r &= {1} \ r^2 &= {1} \ A = pi r^2 &= {3.14} \ end{align*}

Behind the scenes

(If you care about how it works under the hood).

Because neither Svelte nor KaTeX can handle each other's syntax we can't simply use regular Svelte handlebar substitution. KaTeX would choke on the syntax. Instead, the preprocessor plucks out the Svelte content defined in `\s{}`, stores it, passes the Svelte free content to KaTeX, and then puts the Svelte content back in when KaTeX has finished.

// the nuts and bolts of it
const { svelteFreeString, extractedSvelteContent } = replaceSvelteAndStore(rawInput);
const mathString = katex.renderToString(svelteFreeString)
const parsed = restoreSvelte(mathString, extractedSvelteContent);