Lumis Docs
Integrations

NimblePublisher

Use Lumis in NimblePublisher with MDEx through a custom HTML converter.

Full example on GitHub

NimblePublisher ships with Makeup-based highlighting, but it also exposes a custom html_converter hook. That is the right integration point for Lumis.

Use MDEx as the markdown engine inside that converter. This is cleaner than post-processing HTML, and it keeps syntax highlighting inside the markdown renderer where it belongs.

Keep highlighters empty

Do not configure NimblePublisher's built-in highlighters when Lumis owns code blocks.

use NimblePublisher,
  build: Post,
  from: Application.app_dir(:my_app, "priv/posts/**/*.md"),
  as: :posts,
  html_converter: MyApp.Blog.Markdown,
  highlighters: []

Add dependencies

def deps do
  [
    {:nimble_publisher, "~> 1.0"},
    {:mdex, "~> 0.14"},
    {:lumis, "~> 0.10"},
    {:lumis_wasm_elixir, "~> 0.26.0"},
    {:lumis_wasm_bundle_web, "~> 0.1"}
  ]
end

MDEx takes Lumis as an optional dependency, so :lumis has to be declared — it raises without it — and :mdex_native has to be told which highlighter to download before it compiles:

# config/config.exs
import Config

config :mdex_native, syntax_highlighter: :lumis

MDEx reads lumis_wasm_* parser packages. Add one per language your posts use, or a bundle for a set of them. A fence whose language is not installed renders as plain text. The language catalog lists the Hex package names.

For upgrades from MDEx releases with compiled-in grammars, see MDEx parser packages.

Custom converter

NimblePublisher converts posts while MyApp.Blog compiles, and mix compile starts no applications. Starting :mdex brings up :mdex_native, whose start/2 is what hands its NIF the lumis_wasm_* directories to load parsers from, so a converter has to do it. Skip it and every fence still renders through the formatter — Lumis markup, theme background, one line span — around plain, uncolored code, which reads as a theme problem rather than a missing parser.

defmodule MyApp.Blog.Markdown do
  def convert(_path, body, _attrs, _opts) do
    {:ok, _} = Application.ensure_all_started(:mdex)

    MDEx.to_html!(body,
      syntax_highlight: [
        engine: :lumis,
        opts: [formatter: {:html_inline, theme: "catppuccin_latte"}]
      ]
    )
  end
end

That also means there is nothing to warm at boot for these posts: they were highlighted during mix compile, before start/2 ever runs. Lumis.Languages.async_load/1 from your application's start/2 is for Markdown you render per request. Both sides share the cache of compiled parsers, so the build leaves a language warm for the runtime when that cache persists between them.

Multi-theme version

MDEx.to_html!(body,
  syntax_highlight: [
    engine: :lumis,
    opts: [
      formatter:
        {:html_multi_themes,
         themes: [
           light: "catppuccin_latte",
           dark: "catppuccin_frappe"
         ],
         default_theme: "light-dark()"}
    ]
  ]
)

On this page