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
    $$
    
7 Likes