MDExKatex (MDExKatex v0.2.1)

Copy Markdown View Source

MDEx plugin for KaTeX.

Usage

Mix.install([
  {:mdex_katex, "~> 0.1"}
])

markdown = """
# Einstein's Mass-Energy Equivalence

In text, Euler's identity is $e^{i\\pi} + 1 = 0$.

```math
E = mc^2
```

The quadratic formula:

```math
x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}
```
"""

mdex =
  MDEx.new(markdown: markdown, extension: [math_dollars: true])
  |> MDExKatex.attach()

MDEx.to_html!(mdex) |> IO.puts()
#=>
# <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.css">
# <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.js"></script>
# <script defer src="https://cdn.jsdelivr.net/npm/katex@0.16/dist/contrib/auto-render.min.js" onload="renderMathInElement(document.body, {delimiters: [{left: '$$', right: '$$', display: true}]});"></script>
# <script>
#   document.addEventListener("DOMContentLoaded", () => {
#     document.querySelectorAll('.katex-block, .katex-inline').forEach(el => {
#       const latex = el.dataset.latex;
#       const mathStyle = el.dataset.mathStyle;
#       if (latex && mathStyle) {
#         const displayMode = mathStyle == "display" ? true : false
#         katex.render(latex, el, {
#           displayMode: displayMode,
#           throwOnError: false,
#           trust: true,
#         });
#       }
#     });
#   });
# </script>
# <h1>Einstein's Mass-Energy Equivalence</h1>
# <p>In text, Euler's identity is <span id="katex-inline-1" class="katex-inline" phx-update="ignore" data-math-style="inline" data-latex="e^{i\pi} + 1 = 0"></span>.</p>
# <div id="katex-1" class="katex-block" phx-update="ignore" data-math-style="display" data-latex="E = mc^2"></div>
# <p>The quadratic formula:</p>
# <div id="katex-2" class="katex-block" phx-update="ignore" data-math-style="display" data-latex="x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"></div>

To customize KaTeX without replacing the whole init script:

MDEx.new(markdown: markdown)
|> MDExKatex.attach(katex_options: [trust: false, output: "mathml"])
|> MDEx.to_html!()

See KaTeX Options for the supported render options.

For KaTeX options that require JavaScript functions, pass a raw object expression string:

MDEx.new(markdown: markdown)
|> MDExKatex.attach(
  katex_options: "{strict: (errorCode) => 'ignore', trust: (context) => false}"
)
|> MDEx.to_html!()

Note: math and katex code fences render as display math. Dollar math is also supported when MDEx enables extension: [math_dollars: true]; inline formulas use the .katex-inline class and display formulas use .katex-block.

Quick reference:

Inline math: $e^{i\pi} + 1 = 0$

```math
E = mc^2
```

See attach/2 for integration examples (static HTML, Phoenix LiveView, custom styling) and configuration options.

Summary

Functions

Attaches the MDExKatex plugin into the MDEx document.

Types

katex_block_attrs()

@type katex_block_attrs() :: (seq :: pos_integer() -> String.t())

katex_inline_attrs()

@type katex_inline_attrs() :: (seq :: pos_integer() -> String.t())

katex_options()

@type katex_options() :: keyword() | map() | String.t()

Functions

attach(document, options \\ [])

@spec attach(
  MDEx.Document.t(),
  keyword()
) :: MDEx.Document.t()

Attaches the MDExKatex plugin into the MDEx document.

Options

  • :katex_block_attrs (katex_block_attrs/0) - Function that generates the display math tag attributes for math/katex code fences and $$...$$ expressions.
  • :katex_inline_attrs (katex_inline_attrs/0) - Function that generates the inline math tag attributes for $...$ expressions.
  • :katex_options (katex_options/0) - KaTeX render options merged with displayMode for each formula. Accepts a keyword list, map, or raw JavaScript object expression. See KaTeX Options.
  • :katex_init (String.t/0) - The HTML tag(s) to inject into the document to initialize KaTeX. If nil, the default script is used (see below).

:katex_block_attrs

Whenever a display math node is found, it gets converted into a <div> tag using the following function to generate its attributes:

block_attrs = fn seq -> ~s(id="katex-#{seq}" class="katex-block" phx-update="ignore") end
mdex = MDEx.new() |> MDExKatex.attach(katex_block_attrs: block_attrs)

Which results in:

<div id="katex-1" class="katex-block" data-math-style="display" data-latex="E = mc^2" phx-update="ignore"></div>

You can override it to include or manipulate the attributes but it's important to maintain unique IDs for each instance, otherwise the KaTeX rendering will not work correctly, for eg:

fn seq -> ~s(id="katex-#{seq}" class="katex-block formula" phx-hook="KaTeXHook" phx-update="ignore") end

:katex_inline_attrs

Whenever inline dollar math is found, it gets converted into a <span> tag using the following function to generate its attributes:

inline_attrs = fn seq -> ~s(id="katex-inline-#{seq}" class="katex-inline" phx-update="ignore") end
mdex = MDEx.new(markdown: "Euler wrote $e^{i\pi} + 1 = 0$", extension: [math_dollars: true])
|> MDExKatex.attach(katex_inline_attrs: inline_attrs)

Which results in:

<p>Euler wrote <span id="katex-inline-1" class="katex-inline" data-math-style="inline" data-latex="e^{ipi} + 1 = 0" phx-update="ignore"></span></p>

:katex_init

The option :katex_init can be used to manipulate how KaTeX is initialized. By default, the following script is injected into the top of the document:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16/dist/contrib/auto-render.min.js"></script>
<script>
  document.addEventListener("DOMContentLoaded", () => {
    document.querySelectorAll('.katex-block, .katex-inline').forEach(el => {
      const latex = el.dataset.latex;
      const mathStyle = el.dataset.mathStyle;
      if (latex && mathStyle) {
        const displayMode = mathStyle == "display" ? true : false
        katex.render(latex, el, {
          displayMode: displayMode,
          throwOnError: false,
          trust: true,
        });
      }
    });
  });
</script>

That script works well on static documents but you'll need to adjust it to initialize KaTeX in environments that requires waiting for the DOM to be ready.

:katex_options

Use :katex_options to customize the options passed to katex.render/3 without replacing the whole init script:

mdex =
  MDEx.new(markdown: markdown)
  |> MDExKatex.attach(katex_options: [trust: false, output: "mathml"])

For options that require JavaScript functions, pass a raw object expression string:

mdex =
  MDEx.new(markdown: markdown)
  |> MDExKatex.attach(
    katex_options: "{strict: (errorCode) => 'ignore', trust: (context) => false}"
  )

displayMode is always controlled by the markdown node type and overrides any user-provided value.

Examples

See the examples directory for complete working examples.

Static HTML

The output includes all necessary scripts and can be used directly:

html = MDEx.new(markdown: markdown, extension: [math_dollars: true]) |> MDExKatex.attach() |> MDEx.to_html!()
File.write!("output.html", html)

For embedding in existing HTML documents, extract content between initialization scripts and your markdown content.

See examples/static.exs for a complete working example.

Phoenix LiveView

To use MDExKatex with Phoenix LiveView, you can:

  1. Load KaTeX (via CDN or npm)
  2. Create a LiveView hook to render formulas
  3. Configure MDExKatex with the appropriate attributes

Option 1: Using CDN

In your layout:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.js"></script>

Option 2: Using npm

Install KaTeX as a dependency:

cd assets && npm install katex

In your assets/js/app.js:

import katex from 'katex';
import 'katex/dist/katex.min.css';

let hooks = {
  KaTeXHook: {
    mounted() {
      const latex = this.el.dataset.latex;
      const mathStyle = this.el.dataset.mathStyle;
      if (latex && mathStyle) {
        const displayMode = mathStyle == "display" ? true : false
        katex.render(latex, this.el, {
          displayMode: displayMode,
          throwOnError: false,
          trust: true,
        });
      }
    },
  }
}

let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: hooks})

Using in LiveView

html =
  MDEx.new(markdown: markdown, extension: [math_dollars: true])
  |> MDExKatex.attach(
    katex_init: "", # already initialized
    katex_block_attrs: fn seq ->
      ~s(id="katex-#{seq}" class="katex-block" phx-hook="KaTeXHook" phx-update="ignore")
    end,
    katex_inline_attrs: fn seq ->
      ~s(id="katex-inline-#{seq}" class="katex-inline" phx-hook="KaTeXHook" phx-update="ignore")
    end
  )
  |> MDEx.to_html!()

assign(socket, html: {:safe, html})}

Note that you can attach a JS hook per formula or in a parent element to handle all formulas at once, depending on your needs.

See examples/live_view.exs for a complete working example with both individual hooks and global hooks patterns.

Custom Styling

Target .katex-block for display math and .katex-inline for inline math:

.katex-block {
  padding: 1em;
  margin: 1em 0;
  background: #f5f5f5;
  border-radius: 4px;
}

.katex-inline {
  padding: 0;
  background: transparent;
}