Syntax highlighting

Copy Markdown View Source

Syntax highlighting is off by default and turned on with the :syntax_highlight option. Two engines are available:

With it off, fenced code blocks still render as code blocks with their content unchanged, and the language name goes on the <pre> class.

Lumis

Add a parser to your deps for every language you highlight:

{:lumis_wasm_rust, "~> 0.26"},
{:lumis_wasm_elixir, "~> 0.26"}

The full list is at docs.lumis.sh/reference/languages.

Configure MDExNative before compiling dependencies:

config :mdex_native, syntax_highlighter: :lumis

Then pass syntax_highlight when rendering:

markdown = """
```rust
fn main() {
    println!("Hello from Lumis");
}
```
"""

html = MDExNative.Comrak.markdown_to_html!(markdown,
  syntax_highlight: [
    engine: :lumis,
    opts: [
      formatter: {:html_inline, theme: "catppuccin_macchiato"}
    ]
  ]
)

Lumis formatters and options are documented in Lumis. MDExNative reads :formatter, :rainbow_brackets, and :word_diff with the same Rust code as the :lumis package, so the :lumis package itself is not needed. Install it only to build a custom %Lumis.Theme{}.

Set word_diff: true in the engine options to mark changed words in diff fences. This requires the :lumis_wasm_diff parser package.

Inline code

Inline code is highlighted when it names a language, with a {:lang} suffix or, with extension: [inline_code_attributes: true], a language-* class:

Add `{:mdex, "~> 0.14"}{:elixir}` to your deps, then call `MDEx.to_html!(md)`{.language-elixir}.

Unlabeled inline code is left alone. Pass inline_code: false to turn it off.

Highlighting budgets

Pass a budget in the engine options to bound each code block:

MDExNative.Comrak.markdown_to_html!(markdown,
  syntax_highlight: [
    engine: :lumis,
    opts: [
      formatter: :html_linked,
      budget: [time_limit: 1_000, match_limit: 4096]
    ]
  ]
)

Omitted or nil limits use Lumis's defaults: 5000 milliseconds and 8192 in-progress query matches. time_limit: 0 disables the time limit. The time budget covers parsing and highlighting each block, excluding language loading; it is checked cooperatively, so it is not a hard deadline for the Markdown render.

When time runs out, the whole block is rendered as escaped plain text with data-lumis-budget="time" on its <pre>. Reaching the match limit keeps the block highlighted, with some scopes missing, and marks it data-lumis-budget="matches".

Parsers are dependencies

A parser is a WebAssembly module published as a lumis_wasm_* package. Nothing is compiled in and nothing is fetched at runtime: name a language you haven't installed and that fence comes out as plain text.

If a parser's queries fail to compile or the highlighting runtime fails, that fence also falls back to escaped plain text. Its HTML formatter and options still apply, and the rest of the document renders normally. Invalid options and formatter errors still raise.

Loading happens on first use, which costs a Wasmtime compile the first time. Compiled parsers are cached where :lumis keeps its own, config :lumis, :data_dir or LUMIS_DATA_DIR or the :lumis application's priv, so later runs skip the compile and a project that also uses :lumis compiles each parser once. Without :lumis, the cache is in MDExNative's priv.

Syntect

Configure MDExNative before compiling dependencies:

config :mdex_native, syntax_highlighter: :syntect

Then pass a Syntect theme:

markdown = """
```rust
fn main() {
    println!("Hello from Syntect");
}
```
"""

html = MDExNative.Comrak.markdown_to_html!(markdown,
  syntax_highlight: [
    engine: :syntect,
    opts: [theme: "Catppuccin Macchiato"]
  ]
)

Syntect theme names come from two-face. An unknown theme name renders code as plain text.

Inline code is highlighted as with Lumis, see Inline code. The <code> gets the theme's background, or the syntax-highlighting class without a theme.

Artifact size

Bundle size depends on the selected highlighter:

ConfigCompressed artifact size
syntax_highlighter: :lumis5 MB
syntax_highlighter: :syntect3 MB
syntax_highlighter: nil1 MB

Parsers are not in those numbers. They arrive as their own Hex packages, so you only download the languages you asked for.

Legacy CPUs

Modern CPU features are enabled by default. If your environment has an older CPU, use legacy artifacts:

config :mdex_native, use_legacy_artifacts: true