Guide: Render typst math in MkDocs

Introduction

MkDocs is an open source Python static site generator focused for Markdown, and Material for MkDocs is a popular theme. This ecosystem is very popular, e.g., arXiv help pages uses it.

It is possible write LaTeX math in *.md, and let MathJax/KaTeX render formulae in the browser. See Arithmatex - PyMdown Extensions Documentation for more info.

However, once you have written $cal(T): {e_n}_n -> CC$, it is hard to write $\mathcal{T}: \{e_n\}_n \to \mathbb{C}$ again…

Result

  • Render typst math at the build time.

    图片

    <!-- Source markdown -->
    
    用压缩映射原理分析关于 $phi$ 的积分方程
    
    $$
    phi (x) equiv f(x) + integral K(x,y) phi(y) dif y
    $$
    
    的解的存在性,其中 $abs(K)$ 有上界 $M$。
    
  • Integrate into the MkDocs site: dark theme, site search, permalink, admonitions, back-to-top button, …

    图片

  • Coexist with LaTeX math. You can write typst math on some pages and write LaTeX math on other pages.

  • Faster loading time than MathJax.

  • Minimum hack.

    The most hacky part is the CSS style to make formulae copiable, but that’s quite common in the front-end world.

However, I just finished it yesterday. Use it at your own risk.
I will iterate and test, and publish it to PyPI under MIT License when appropirate.

How to render typst math in MkDocs

  1. Edit mkdocs.yml and add the following.

    markdown_extensions:
      - pymdownx.arithmatex:
          generic: true
    
    hooks:
      - hooks/typst_math.py
    
    extra_css:
      - stylesheets/extra.css
    
  2. Save the following to docs/stylesheets/extra.css.

    .typst-math > svg {
        overflow: visible;
    }
    div.typst-math {
        display: flex;
        justify-content: center;
    }
    
    /*
        https://v3.tailwindcss.com/docs/screen-readers
        will interrupt the selection, thus unusable
    */
    .sr-only {
        position: absolute;
        left: -9999px;
    }
    
  3. Save the following to hooks/typst_math.py

    """Render math with typst
    
    ## Usage
    
    1. Install the markdown extensions pymdownx.arithmatex.
    2. Add `math: typst` to pages' metadata.
    
    ## Requirements
    
    - typst
    
    """
    
    from __future__ import annotations
    
    import html
    import re
    from functools import cache
    from subprocess import CalledProcessError, run
    from typing import TYPE_CHECKING
    
    if TYPE_CHECKING:
        from mkdocs.config.defaults import MkDocsConfig
        from mkdocs.structure.files import Files
        from mkdocs.structure.pages import Page
    
    
    def should_render(page: Page) -> bool:
        return page.meta.get("math") == "typst"
    
    
    def on_page_markdown(
        markdown: str, page: Page, config: MkDocsConfig, files: Files
    ) -> str | None:
        if should_render(page):
            assert "pymdownx.arithmatex" in config.markdown_extensions, (
                "Missing pymdownx.arithmatex in config.markdown_extensions. "
                "Setting `math: typst` requires it to parse markdown."
            )
    
    
    def on_post_page(output: str, page: Page, config: MkDocsConfig) -> str | None:
        if should_render(page):
            output = re.sub(
                r'<span class="arithmatex">(.+?)</span>', render_inline_math, output
            )
    
            output = re.sub(
                r'<div class="arithmatex">(.+?)</div>',
                render_block_math,
                output,
                flags=re.MULTILINE | re.DOTALL,
            )
            return output
    
    
    def render_inline_math(match: re.Match[str]) -> str:
        src = html.unescape(match.group(1)).removeprefix(R"\(").removesuffix(R"\)").strip()
        typ = f"${src}$"
        return (
            '<span class="typst-math">'
            + fix_svg(typst_compile(typ))
            + for_screen_reader(typ)
            + "</span>"
        )
    
    
    def render_block_math(match: re.Match[str]) -> str:
        src = html.unescape(match.group(1)).removeprefix(R"\[").removesuffix(R"\]").strip()
        typ = f"$ {src} $"
        return (
            '<div class="typst-math">'
            + fix_svg(typst_compile(typ))
            + for_screen_reader(typ)
            + "</div>"
        )
    
    
    def for_screen_reader(typ: str) -> str:
        return f'<span class="sr-only">{html.escape(typ)}</span>'
    
    
    def fix_svg(svg: bytes) -> str:
        """Fix the compiled SVG to be embedded in HTML
    
        - Strip trailing spaces
        - Support dark theme
        """
        return re.sub(
            r' (fill|stroke)="#000000"',
            r' \1="var(--md-typeset-color)"',
            svg.decode().strip(),
        )
    
    
    @cache
    def typst_compile(
        typ: str,
        *,
        prelude="#set page(width: auto, height: auto, margin: 0pt, fill: none)\n",
        format="svg",
    ) -> bytes:
        """Compile a Typst document
    
        https://github.com/marimo-team/marimo/discussions/2441
        """
        try:
            return run(
                ["typst", "compile", "-", "-", "--format", format],
                input=(prelude + typ).encode(),
                check=True,
                capture_output=True,
            ).stdout
        except CalledProcessError as err:
            raise RuntimeError(
                f"""
    Failed to render a typst math:
    
    ```typst
    {typ}
    ```
    
    {err.stderr.decode()}
    """.strip()
            )
    
  4. Start writing.

    ---
    math: typst
    ---
    
    # Heading
    
    Inline math: $phi$.
    
    Block math:
    
    $$
    phi (x) equiv f(x) + integral K(x,y) phi(y) dif y
    $$
    
10 Likes

Thanks it does work!

2 Likes

plz public it as a mkdocs plugin or something :heart_eyes:

3 Likes

I’ve been testing it these days and fixed a minor bug of the color in hyperlinks.

In addition, I realize that inline SVGs are not very performant.

Suppose there are n formulas, then the overhead of the MathJax/KaTeX approach is 3000 + n, but that of the inline SVG + Typst approach is 1 + 10 n.
If there aren’t too many formulae, then inline SVG + Typst works gracefully; but if there’re 300+ formulae, then inline SVGs will bloat the size of the HTML, and web browsers are not good at parsing/rendering it.
(The above are not precise statistics, of course.)

The following are the situations of two real documents. Although they have different contents, it is sufficient to demonstrate the problem of file size.

File MathJax inline SVG + Typst
The source markdown 37 KiB 32 KiB (0.8×)
Generated html 120 KiB 2.6 MiB (22×)
Generated html, zipped 22 KiB (18%) 93 KiB (4×, 3%)

For reference, mathjax@3/es5/tex-mml-chtml.js is 1.1 MiB (204 KiB zipped).


Unfortunately, as far as I know, inline SVG is the only way to make a single SVG adapt to the color scheme…