Current section

Files

Jump to
lumis usage-rules.md
Raw

usage-rules.md

# Lumis - Usage Rules for AI Agents
Lumis is a syntax highlighter for Elixir that uses Tree-sitter parsers and Neovim themes. It provides fast, accurate syntax highlighting with support for 110+ languages and 250+ built-in themes, outputting to HTML (inline or linked) or terminal (ANSI codes).
## Core Concepts
### What Lumis Does
- Highlights source code using Tree-sitter parsers
- Supports 110+ programming languages with auto-detection
- Provides 250+ built-in Neovim themes (light and dark)
- Handles incomplete/malformed code gracefully (useful for streaming scenarios)
- Outputs HTML with inline styles, HTML with CSS classes, or ANSI terminal codes
### What Lumis Does NOT Do
- Does not parse or execute code
- Does not validate code syntax (it highlights even invalid code)
- Does not provide language-specific transformations
- Does not format or prettify code
## Basic Usage
### Simple Highlighting
Always use the 2-arity form with options:
```elixir
# Good - with language specified
Lumis.highlight!("defmodule MyApp do", formatter: {:html_inline, language: "elixir"})
# Good - language auto-detection
Lumis.highlight!("#!/usr/bin/env bash\necho 'hello'")
# Avoid - deprecated 3-arity form
Lumis.highlight!("elixir", "defmodule MyApp do", [])
```
### Return Values
- `highlight/2` returns `{:ok, html_string}` or `{:error, %Lumis.RenderError{}}`
- `highlight!/2` returns `html_string` or raises `Lumis.HighlightError`
A missing parser is not an error. A language whose parser is not installed as a
dependency still returns `{:ok, html}`, rendered as plain text with a warning in
the log. Don't write a `Lumis.ParserError` clause around `highlight/2` — it will
never match. Use `Lumis.Languages.load/1` at boot to make a missing parser fatal.
An error means the formatter itself failed. Match on `:reason`, never on the
message — the reasons are the API, the messages are not.
```elixir
case Lumis.highlight(source, formatter: {:html_inline, language: "elixir"}) do
{:ok, html} -> html
{:error, error} -> Logger.error(Exception.message(error))
end
# Or use the bang version when you expect success
html = Lumis.highlight!(source, formatter: {:html_inline, language: "elixir"})
```
## Language Specification
### Specifying Languages
You can specify languages in multiple ways:
```elixir
# By language name
Lumis.highlight!(code, formatter: {:html_inline, language: "elixir"})
Lumis.highlight!(code, formatter: {:html_inline, language: "javascript"})
Lumis.highlight!(code, formatter: {:html_inline, language: "rust"})
# By file extension
Lumis.highlight!(code, formatter: {:html_inline, language: ".ex"})
Lumis.highlight!(code, formatter: {:html_inline, language: ".js"})
# By filename
Lumis.highlight!(code, formatter: {:html_inline, language: "app.ex"})
Lumis.highlight!(code, formatter: {:html_inline, language: "lib/my_module.ex"})
# Auto-detection (omit language option)
Lumis.highlight!(code)
```
### Discovering Available Languages
```elixir
# Get all available languages, sorted by id
languages = Lumis.available_languages()
# Returns: [%{id: "elixir", name: "Elixir", aliases: [], extensions: ["*.ex", "*.exs"],
# globs: ["*.ex", "*.exs"], emacs_modes: ["elixir"], shebangs: ["elixir"]}, ...]
# Check if a language is supported
Enum.any?(Lumis.available_languages(), &(&1.id == "elixir"))
# Look one up by id or alias, without scanning the catalog
Lumis.Languages.get("js")
# Returns: %{id: "javascript", name: "JavaScript", ...}
Lumis.Languages.get("not-a-language")
# Returns: nil
# Or resolve a name, path or source the way highlighting does
"elixir" = Lumis.Languages.guess("lib/app.ex")
```
### Parser Installation
A parser is an ordinary dependency. Every language the project highlights needs
its `lumis_wasm_*` package in `mix.exs`, including the ones injected inside
another — Markdown fences, `css` and `javascript` inside HTML, `comment` inside
Elixir:
```elixir
def deps do
[
{:lumis, "~> 0.9"},
{:lumis_wasm_elixir, "~> 0.26.0"},
{:lumis_wasm_bundle_web, "~> 0.1"}
]
end
```
A bundle package installs a set of languages at once.
A language no dependency supplies is never fetched. It answers `:not_installed`,
and an injected one leaves its block plain rather than failing the document.
### Parser Loading
Highlighting loads what a document needs out of the installed parsers, including
languages injected inside it. Loading is global to the VM, so the cost is paid
once.
Load ahead of time to keep the first compile off a user's request. Parser bytes
arrive with the `lumis_wasm_*` dependency, so a cold parser costs a Wasmtime
compile and nothing else:
```elixir
:ok = Lumis.Languages.load(["markdown", "elixir", "json"])
```
From an application's `start/2`, use the async variant instead. It returns
immediately, so a slow compile cannot delay or fail the boot, and it is not
matched on:
```elixir
Lumis.Languages.async_load(["markdown", "elixir", "json"])
```
Wasmtime persists compiled modules under the data directory, which resolves in
this order: `config :lumis, data_dir:` wins, `LUMIS_DATA_DIR` is the environment
fallback, and Lumis's own `priv/lumis` is the zero-configuration default. Point
every node at one directory and the compile is paid once for all of them.
## Formatters
Lumis supports five formatters. The formatter option controls the output format.
### HTML Inline (Default)
Generates HTML with inline styles. Best for email, isolated components, or when you don't want external CSS.
```elixir
# Using default formatter
Lumis.highlight!(code, formatter: {:html_inline, language: "elixir"})
# Explicitly specify with options
Lumis.highlight!(code,
formatter: {:html_inline, [
language: "elixir",
theme: "github_light",
pre_class: "my-code",
pre_attrs: [id: "example"],
code_attrs: [title: "Highlighted Elixir"],
italic: true,
include_highlights: false
]}
)
```
Available options for `:html_inline`:
- `:structure` - `:block` (default) writes a `<pre><code>` block; `:inline` writes only the token spans (see Inline Code section)
- `:theme` - Theme name (string) or `Lumis.Theme` struct
- `:pre_class` - CSS class to add to the `<pre>` tag
- `:pre_attrs` - Attributes merged into the `<pre>` tag; `true` writes the bare boolean form, `false` drops a default
- `:code_attrs` - Attributes merged into the `<code>` tag; `true` writes the bare boolean form, `false` drops a default
- `:italic` - Enable italic styles (default: `false`)
- `:include_highlights` - Add `data-highlight` attributes for debugging (default: `false`)
- `:highlight_lines` - Highlight specific lines (see Line Highlighting section)
- `:line_numbers` - Open each line with a line number gutter (see Line Numbers section)
- `:header` - Wrap with custom HTML tags (see Custom Wrappers section)
### HTML Linked
Generates HTML with CSS classes. Requires linking a CSS file from `priv/static/css/`.
```elixir
Lumis.highlight!(code,
formatter: {:html_linked, [
language: "elixir",
pre_class: "my-code"
]}
)
```
**Important**: You must include the CSS file in your application:
For Phoenix apps, add to `endpoint.ex`:
```elixir
plug Plug.Static,
at: "/themes",
from: {:lumis, "priv/static/css/"},
only: ["onedark.css"] # or any other theme
```
Then in your template:
```heex
<link rel="stylesheet" href={~p"/themes/onedark.css"} />
```
Available options for `:html_linked`:
- `:structure` - `:block` (default) writes a `<pre><code>` block; `:inline` writes only the token spans (see Inline Code section)
- `:pre_class` - CSS class to add to the `<pre>` tag
- `:pre_attrs` - Attributes merged into the `<pre>` tag; `true` writes the bare boolean form, `false` drops a default
- `:code_attrs` - Attributes merged into the `<code>` tag; `true` writes the bare boolean form, `false` drops a default
- `:highlight_lines` - Highlight specific lines with CSS class
- `:line_numbers` - Open each line with a line number gutter (see Line Numbers section)
- `:header` - Wrap with custom HTML tags
#### Building scoped CSS
`Lumis.Theme.build_css!/2` builds a stylesheet from a theme name or `Lumis.Theme` struct, for when the bundled CSS files are not enough.
```elixir
css =
Lumis.Theme.build_css!("github_dark",
scope: ~s(html[data-theme="dark"]),
container_selector: ".lumis",
container_style: [
{"background-color", "var(--code-background)"},
{"border-radius", "0.375rem"}
]
)
```
Options: `:scope`, `:container_selector`, `:container_style`, and `:enable_italic`. Use `:container_style` for declarations on the container selector, such as `padding`, `border-radius`, or a replacement `background-color`.
### HTML Multi-Themes
Generates HTML with CSS custom properties (variables) for multiple themes, enabling light/dark mode support. Inspired by [Shiki Dual Themes](https://shiki.style/guide/dual-themes).
```elixir
# Basic dual theme with CSS variables
Lumis.highlight!(code,
formatter: {:html_multi_themes,
language: "elixir",
themes: [light: "github_light", dark: "github_dark"]
}
)
# With light-dark() function for automatic theme switching
Lumis.highlight!(code,
formatter: {:html_multi_themes,
language: "elixir",
themes: [light: "github_light", dark: "github_dark"],
default_theme: "light-dark()"
}
)
# With inline colors for default theme
Lumis.highlight!(code,
formatter: {:html_multi_themes,
language: "elixir",
themes: [light: "github_light", dark: "github_dark"],
default_theme: "light"
}
)
# Multiple themes with custom prefix
Lumis.highlight!(code,
formatter: {:html_multi_themes,
language: "elixir",
themes: [light: "github_light", dark: "github_dark", dim: "catppuccin_frappe"],
css_variable_prefix: "--code"
}
)
```
**How it works:**
- Generates CSS custom properties like `--lumis-light-fg`, `--lumis-dark-fg`, etc.
- Theme identifiers (from the keyword list keys) become CSS class names
- Use CSS media queries or JavaScript to switch between themes
- The `default_theme` option controls inline color rendering:
- `nil` (default): Only CSS variables, no inline colors
- A theme identifier (e.g., `"light"`): Renders inline colors for that theme plus CSS variables for all themes
- `"light-dark()"`: Uses CSS light-dark() function for automatic theme switching
- `light-dark()` is a color function, so it switches `color` and `background-color` only. For `font-weight`, `font-style` and `text-decoration`, a value both themes share is an ordinary declaration, and a value they disagree on is `--lumis-light-*` and `--lumis-dark-*` variables with nothing inline. Switching a disputed one needs the rules below; leaving it out of the style attribute is what keeps them from needing `!important`
**CSS Integration Examples:**
```css
/* Automatic light/dark mode based on system preference */
@media (prefers-color-scheme: light) {
.lumis-themes {
color: var(--lumis-light);
background-color: var(--lumis-light-bg);
}
}
@media (prefers-color-scheme: dark) {
.lumis-themes {
color: var(--lumis-dark);
background-color: var(--lumis-dark-bg);
}
}
/* Manual control with data attributes */
[data-theme="light"] .lumis-themes {
color: var(--lumis-light);
background-color: var(--lumis-light-bg);
}
[data-theme="dark"] .lumis-themes {
color: var(--lumis-dark);
background-color: var(--lumis-dark-bg);
}
/* With default_theme: "light-dark()", only for themes that disagree on one of
these three. No !important: a disputed property is left out of the style
attribute, so there is nothing inline to outrank. */
.lumis span {
font-style: var(--lumis-light-font-style);
font-weight: var(--lumis-light-font-weight);
text-decoration: var(--lumis-light-text-decoration);
}
@media (prefers-color-scheme: dark) {
.lumis span {
font-style: var(--lumis-dark-font-style);
font-weight: var(--lumis-dark-font-weight);
text-decoration: var(--lumis-dark-text-decoration);
}
}
```
Available options for `:html_multi_themes`:
- `:structure` - `:block` (default) writes a `<pre><code>` block; `:inline` writes only the token spans (see Inline Code section)
- `:themes` (required) - Keyword list mapping theme identifiers to theme names or structs, e.g., `[light: "github_light", dark: "github_dark"]`
- `:default_theme` - Controls inline color rendering: theme identifier, `"light-dark()"`, or `nil` (default: `nil`)
- `:css_variable_prefix` - Custom CSS variable prefix (default: `"--lumis"`)
- `:pre_class` - CSS class to add to the `<pre>` tag
- `:pre_attrs` - Attributes merged into the `<pre>` tag; `true` writes the bare boolean form, `false` drops a default
- `:code_attrs` - Attributes merged into the `<code>` tag; `true` writes the bare boolean form, `false` drops a default
- `:italic` - Enable italic styles (default: `false`)
- `:include_highlights` - Add `data-highlight` attributes for debugging (default: `false`)
- `:highlight_lines` - Highlight specific lines (same options as `:html_inline`)
- `:line_numbers` - Open each line with a line number gutter (see Line Numbers section)
- `:header` - Wrap with custom HTML tags (same options as other formatters)
### Terminal
Generates ANSI escape codes for terminal output.
```elixir
Lumis.highlight!(code,
formatter: {:terminal, language: "elixir"}
)
# With theme
Lumis.highlight!(code,
formatter: {:terminal, language: "elixir", theme: "github_light"}
)
```
Available options for `:terminal`:
- `:theme` - Theme name (string) or `Lumis.Theme` struct
- `:background` - Fallback background: `:theme`, a hex colour, or `nil` to inherit the terminal's
- `:width` - Pad each line out to this width, so a background reaches the edge
- `:highlight_lines` - Paint specific lines with a background colour
- `:line_numbers` - Prefix each line with its number (see Line Numbers section)
### BBCode Scoped
Generates nested BBCode tags using highlight scope names from the Rust implementation. Literal `[` and `]` are escaped as `&#91;` and `&#93;` so forum software does not treat source text as markup. It does not emit standard forum-style BBCode like `[b]`, `[color]`, or `[code]`.
```elixir
Lumis.highlight!(code,
formatter: {:bbcode_scoped, language: "elixir"}
)
```
Available options for `:bbcode_scoped`:
- `:highlight_lines` - Wrap specific lines in `[highlighted]...[/highlighted]`
## Themes
### Using Themes
```elixir
# List all available themes, sorted by name
themes = Lumis.available_themes()
# Returns: [%{name: "dracula", appearance: "dark"}, ...]
# Or just the names
names = Enum.map(Lumis.available_themes(), & &1.name)
# Returns: ["adwaita_dark", "adwaita_light", ...]
# Use a theme by name
Lumis.highlight!(code,
formatter: {:html_inline, theme: "dracula"}
)
# Get a theme struct
theme = Lumis.Theme.get("github_light")
Lumis.highlight!(code, formatter: {:html_inline, language: "elixir", theme: theme})
```
### Custom Themes
You can load custom themes from JSON:
```elixir
# From a JSON file
{:ok, theme} = Lumis.Theme.from_file("/path/to/theme.json")
Lumis.highlight!(code, formatter: {:html_inline, theme: theme})
# From a JSON string
json = ~s({"name": "my_theme", "appearance": "dark", "highlights": {...}})
{:ok, theme} = Lumis.Theme.from_json(json)
Lumis.highlight!(code, formatter: {:html_inline, theme: theme})
```
## Advanced Features
### Line Highlighting
Highlight specific lines with custom styling or CSS classes.
#### HTML Inline Line Highlighting
```elixir
# Use theme's highlighted style (default)
Lumis.highlight!(code,
formatter: {:html_inline,
language: "elixir",
highlight_lines: %{lines: [2, 3, 4]}
}
)
# Explicit theme style
Lumis.highlight!(code,
formatter: {:html_inline,
language: "elixir",
highlight_lines: %{lines: [1, 5..10], style: :theme}
}
)
# Custom inline style
Lumis.highlight!(code,
formatter: {:html_inline,
language: "elixir",
highlight_lines: %{
lines: [2..4, 7],
style: "background-color: #fff3cd; border-left: 3px solid #ffc107;"
}
}
)
# CSS class only (no inline colors; the line layout stays)
Lumis.highlight!(code,
formatter: {:html_inline,
language: "elixir",
highlight_lines: %{
lines: [1, 2, 3],
style: nil,
class: "highlighted-line"
}
}
)
```
The `:lines` option accepts:
- Single integers: `[1, 3, 5]`
- Ranges: `[2..10]`
- Mix of both: `[1, 3..5, 8, 10..15]`
#### HTML Linked Line Highlighting
```elixir
# Use default "l-highlighted" class from theme CSS
Lumis.highlight!(code,
formatter: {:html_linked,
language: "elixir",
highlight_lines: %{lines: [2..4, 6]}
}
)
# Use custom CSS class
Lumis.highlight!(code,
formatter: {:html_linked,
language: "elixir",
highlight_lines: %{lines: [1, 2, 3], class: "error-line"}
}
)
```
### Line Numbers
`:line_numbers` numbers the lines a formatter renders. The three HTML formatters
and `:terminal` take it; `:bbcode_scoped` has nothing to render a number into.
```elixir
Lumis.highlight!(code, formatter: {:html_inline, language: "elixir", line_numbers: true})
```
HTML opens each line with `<span class="l-line-number" aria-hidden="true">N</span>`,
which uses the theme's `LineNr` style and needs a layout rule to become a column.
A highlighted gutter also has `l-line-number-highlighted` and uses `CursorLineNr`,
falling back to `LineNr`. The terminal uses the same styles and writes the number
right-aligned to the widest one.
### Custom HTML Wrappers
Wrap the highlighted code with custom HTML elements:
```elixir
header = %{
open_tag: "<figure><figcaption>app.ex</figcaption>",
close_tag: "</figure>"
}
Lumis.highlight!(code,
formatter: {:html_inline, language: "elixir", header: header}
)
# Output:
# <figure>
# <figcaption>app.ex</figcaption>
# <pre>...</pre>
# </figure>
```
Both `:open_tag` and `:close_tag` are required when using `:header`.
### Inline Code
`structure: :inline` writes the token spans and nothing else, for code inside
an element the page already has, such as a `<code>` in a sentence:
```elixir
spans =
Lumis.highlight!("mix igniter.install mdex",
formatter: {:html_inline, language: "bash", theme: "dracula", structure: :inline}
)
"<p>Install it with <code>" <> spans <> "</code>.</p>"
```
Lines are separated by `\n` and the last one has no terminator. There is no
`<pre>`, `<code>` or line element, so `:pre_class`, `:pre_attrs`, `:code_attrs`,
`:highlight_lines`, `:line_numbers` and `:header` have no effect, and the theme's
text and background color are not written. To keep them, put
`Lumis.Formatter.HTML.pre_attrs(theme: "dracula")` on your element with
`Lumis.Formatter.HTML.open_tag/2`.
### Custom Formatters
When no built-in formatter emits what you need, implement `Lumis.Formatter` and
pass the module. Lumis highlights the source and hands `render/3` the event
stream, with the resolved language in `options`.
```elixir
defmodule MyFormatter do
@behaviour Lumis.Formatter
alias Lumis.Formatter.HTML
@impl true
def render(source, events, options) do
language = Keyword.fetch!(options, :language)
theme = Keyword.get(options, :theme)
attrs = HTML.span_attrs(theme: theme, language: language)
body =
Enum.map(events, fn
{:start, %{scope: scope}} -> HTML.open_span(attrs, scope)
:end -> "</span>"
{:source, %{start: start, end: stop}} ->
HTML.escape(binary_part(source, start, stop - start))
# Always keep this clause. Lumis adds event kinds as it grows, and
# without it a newer Lumis raises FunctionClauseError instead of
# rendering.
_event -> []
end)
[HTML.open_pre_tag(theme: theme), HTML.open_code_tag(language), body, HTML.closing_tags()]
end
end
Lumis.highlight!(code, formatter: {MyFormatter, language: "elixir", theme: "github_light"})
```
`render/3` returns iodata; Lumis flattens it once at the end.
When the result is not one string, call `Lumis.highlight_events/3` and work on
the events directly rather than splitting a formatter's output. For one HTML
fragment per line:
```elixir
events = Lumis.highlight_events!(code, "elixir")
Lumis.Formatter.HTML.render_lines_from_events(code, events, attrs)
```
Do not hand-roll HTML escaping or scope-to-class mapping. `Lumis.Formatter.HTML`
gives the built-in formatters' pieces:
- `escape/1`, `escape_attr/1`, `escape_braces/1`
- `scope_to_class/1`, `span_linked_attrs/1`, `span_linked/2`
- `span_attrs/1`, `open_span/2`, `span_inline_attrs/2`, `span_inline/3`
- `span_multi_themes_attrs/1`, `span_multi_themes/3`, `sanitize_theme_name/1`
- `style_to_css/2`, `text_decoration/1`
- `open_pre_tag/1`, `open_multi_themes_pre_tag/1`, `open_code_tag/1`
- `close_pre_tag/0`, `close_code_tag/0`, `closing_tags/0`
- `wrap_line/3`, `line_is_highlighted/2`, `highlight_line_class/3`
- `render_lines_from_events/3`
`span_attrs/1`, `span_multi_themes_attrs/1` and `classes/0` return whole tables
because resolving a scope is a per-token operation. Build the table once outside
the loop and read it inside.
For line-based output, reach for `render_lines_from_events/3` rather than
splitting rendered markup on newlines. A `<span>` that crosses a newline has to
be closed and reopened for each line's tags to nest, and that is the part worth
not writing again:
```elixir
attrs = HTML.span_attrs(theme: theme, language: language)
source
|> HTML.render_lines_from_events(events, attrs)
|> Enum.with_index(1)
|> Enum.map(fn {line, number} -> HTML.wrap_line(number, line) end)
|> Enum.intersperse("\n")
```
Each rendered line contains only content, without LF or CRLF terminators.
A final newline terminates the last line rather than adding an empty one.
Wrapped lines are inline spans; join them with `"\n"`. Use `display: inline-block`
for full-width lines, not `display: block`. The CSS theme files guide at
[docs.lumis.sh](https://docs.lumis.sh) covers line layout.
Do not hand-roll ANSI color or text-decoration escape sequences either.
`Lumis.Formatter.ANSI` gives `:terminal`'s pieces:
- `hex_to_rgb/1`, `rgb_to_ansi/4`
- `style_to_ansi/1`, `paint/2`, `reset/0`
- `styles/1`, `style_for/2`
Do not resolve a scope with `Map.get(theme.highlights, scope)`. That misses the
fallbacks `:terminal` applies — `tag.delimiter` is painted by `tag` in a theme
that styles only `tag` — so it paints differently. Use `styles/1` and read it
with `style_for/2`, and build one table per language you meet: a theme can style
a scope per language, and an injected block carries its own language on its
`:start` event.
## HTML Output Structure
Lumis generates semantic HTML with line wrappers:
```html
<pre class="lumis" style="color: #abb2bf; background-color: #282c34;"><code class="language-elixir" translate="no" tabindex="0"><span class="l-line" data-line="1"><span style="color: #c678dd;">defmodule</span> <span style="color: #e5c07b;">MyApp</span> <span style="color: #c678dd;">do</span></span>
<span class="l-line" data-line="2">...</span></code></pre>
```
Key points:
- Each line is a `<span class="l-line" data-line="N">` holding only that line's content
- A `\n` sits between line spans and nothing follows the last one; a final
newline in the source does not add an empty line
- Don't reformat the output: whitespace between the spans shows up in the `<pre>`
- The `data-line` attribute contains the line number (1-indexed)
- The `<code>` tag has `translate="no"` to prevent browser translation
- The `<code>` tag has `tabindex="0"` for keyboard accessibility
## Common Patterns
### Highlighting Code Blocks in Phoenix
```elixir
# In your LiveView or template
def render(assigns) do
~H"""
<div class="code-container">
<%= raw Lumis.highlight!(@code, formatter: {:html_inline, language: @language}) %>
</div>
"""
end
```
**Important**: Always use `raw/1` to prevent double-escaping HTML.
### Streaming Code (ChatGPT-style)
Lumis handles incomplete code gracefully:
```elixir
# This works even though the code is incomplete
Lumis.highlight!("defmodule MyApp do\n def hel", formatter: {:html_inline, language: "elixir"})
```
This is useful for streaming scenarios where code is being typed or generated incrementally.
### Dynamic Theme Selection
```elixir
def highlight_with_user_theme(code, language, user_preferences) do
theme = user_preferences.dark_mode? && "github_dark" || "github_light"
Lumis.highlight!(code,
formatter: {:html_inline, language: language, theme: theme}
)
end
```
### Validating Options
```elixir
# Validate options before using them
options = [
formatter: {:html_inline, language: "elixir", theme: "onedark"}
]
validated = Lumis.validate_options!(options)
# Use validated options...
```
### Getting Default Options
```elixir
# Get the default options used by Lumis
defaults = Lumis.default_options()
# Returns: [language: nil, formatter: {:html_inline, [...]}]
```
## Best Practices
### DO: Specify Language When Known
Always specify the language when you know it:
```elixir
# Good
Lumis.highlight!(code, formatter: {:html_inline, language: "elixir"})
# Less ideal - auto-detection is slower
Lumis.highlight!(code)
```
### DO: Use Pattern Matching for Error Handling
```elixir
# Good
case Lumis.highlight(code, formatter: {:html_inline, language: "unknown"}) do
{:ok, html} -> html
{:error, _} -> fallback_html(code)
end
# Or use with/1
with {:ok, html} <- Lumis.highlight(code, formatter: {:html_inline, language: language}) do
html
end
```
### DO: Cache Highlighted Output
Syntax highlighting is CPU-intensive. Cache the output when possible:
```elixir
# In Phoenix LiveView
def mount(_params, _session, socket) do
code = get_code()
highlighted = Lumis.highlight!(code, formatter: {:html_inline, language: "elixir"})
{:ok, assign(socket, highlighted: highlighted)}
end
```
### DO: Use html_linked for Large Applications
For applications with many code blocks, use `:html_linked` to reduce HTML size:
```elixir
# Smaller HTML output
Lumis.highlight!(code,
formatter: {:html_linked, language: "elixir"}
)
```
### DON'T: Double-escape HTML
```elixir
# Bad - will show escaped HTML entities
~H"""
<div><%= Lumis.highlight!(code, formatter: {:html_inline, language: "elixir"}) %></div>
"""
# Good - use raw/1
~H"""
<div><%= raw Lumis.highlight!(code, formatter: {:html_inline, language: "elixir"}) %></div>
"""
```
### DON'T: Use Deprecated Options
```elixir
# Bad - deprecated
Lumis.highlight!(code, theme: "onedark", inline_style: true)
# Good - use formatter option
Lumis.highlight!(code, formatter: {:html_inline, theme: "onedark"})
```
### DON'T: Mix Formatters and Deprecated Options
```elixir
# Bad - confusing and error-prone
Lumis.highlight!(code,
formatter: :html_inline,
theme: "onedark" # deprecated
)
# Good - everything in formatter options
Lumis.highlight!(code,
formatter: {:html_inline, theme: "onedark"}
)
```
## Common Mistakes to Avoid
### Mistake: Not Using `raw/1` in Phoenix
```elixir
# Wrong - HTML will be escaped
~H"""<div><%= @highlighted_code %></div>"""
# Correct
~H"""<div><%= raw @highlighted_code %></div>"""
```
### Mistake: Forgetting CSS for html_linked
```elixir
# This will output HTML without colors
Lumis.highlight!(code, formatter: :html_linked)
```
Remember to include the CSS file in your application.
### Mistake: Invalid Line Numbers
```elixir
# Wrong - line numbers are 1-indexed
highlight_lines: %{lines: [0, 1, 2]}
# Correct - start from 1
highlight_lines: %{lines: [1, 2, 3]}
```
### Mistake: Incomplete Header Option
```elixir
# Wrong - missing close_tag
header: %{open_tag: "<div>"}
# Correct - both tags required
header: %{open_tag: "<div>", close_tag: "</div>"}
```
### Mistake: Using String for Lines
```elixir
# Wrong - lines must be integers or ranges
highlight_lines: %{lines: ["1", "2"]}
# Correct
highlight_lines: %{lines: [1, 2]}
```
## Function Quick Reference
### Main Functions
```elixir
# Highlight with error handling
{:ok, html} = Lumis.highlight(source, opts)
# Highlight and raise on error
html = Lumis.highlight!(source, opts)
# Render the event stream yourself
html = Lumis.highlight!(source, formatter: {MyFormatter, language: "elixir"})
# Get all available languages
[%{id: _} | _] = Lumis.available_languages()
# Get one language by id or alias
%{id: "javascript"} = Lumis.Languages.get("js")
# Get the ids loaded into this VM
[_ | _] = Lumis.loaded_languages()
# Get all available themes
[%{name: _, appearance: _} | _] = Lumis.available_themes()
# Validate options
opts = Lumis.validate_options!(opts)
# Get default options
opts = Lumis.default_options()
```
### Theme Functions
```elixir
# Get a theme by name
%Lumis.Theme{} = Lumis.Theme.get("github_light")
%Lumis.Theme{} = Lumis.Theme.get("unknown", default_theme)
# Load theme from file
{:ok, theme} = Lumis.Theme.from_file(path)
# Load theme from JSON string
{:ok, theme} = Lumis.Theme.from_json(json_string)
```
## Options Reference
`highlight/2` takes four options: `:formatter`, plus `:rainbow_brackets`,
`:budget` and `:annotations`, which belong beside `:formatter` rather than
inside its option list. Unset, a budget key uses the built-in bound — 5000 ms
and 8192 in-progress query matches.
```elixir
[
# Color nested brackets by depth (default: false)
rainbow_brackets: true,
# Bound one render (default: [time_limit: nil, match_limit: nil])
budget: [time_limit: 2_000, match_limit: 16_384],
# Caller-provided ranges (default: [])
annotations: [[offset: {12, 23}, data: %{change: :added}]],
# Formatter specification
formatter:
:html_inline |
{:html_inline, [
language: "elixir" | ".ex" | "app.ex" | nil,
theme: "onedark" | %Lumis.Theme{},
pre_class: "my-class",
pre_attrs: [id: "example"],
code_attrs: [title: "Highlighted code"],
italic: false,
include_highlights: false,
highlight_lines: %{
lines: [1, 2..5],
style: :theme | "custom-css" | nil,
class: "custom-class"
},
line_numbers: false,
header: %{
open_tag: "<div>",
close_tag: "</div>"
}
]} |
:html_linked |
{:html_linked, [
language: "elixir" | ".ex" | "app.ex" | nil,
pre_class: "my-class",
pre_attrs: [id: "example"],
code_attrs: [title: "Highlighted code"],
highlight_lines: %{
lines: [1, 2..5],
class: "l-highlighted"
},
line_numbers: false,
header: %{
open_tag: "<div>",
close_tag: "</div>"
}
]} |
:terminal |
{:terminal, [
language: "elixir" | ".ex" | "app.ex" | nil,
theme: "onedark" | %Lumis.Theme{},
background: :theme | "#282a36" | nil,
width: 120 | nil,
highlight_lines: %{
lines: [1, 2..5],
background: "#3a3a3a" | nil
},
line_numbers: false
]} |
:html_multi_themes |
{:html_multi_themes, [
language: "elixir" | ".ex" | "app.ex" | nil,
themes: [light: "github_light", dark: "github_dark"], # required
default_theme: "light" | "light-dark()" | nil,
css_variable_prefix: "--custom",
pre_class: "my-class",
pre_attrs: [id: "example"],
code_attrs: [title: "Highlighted code"],
italic: false,
include_highlights: false,
highlight_lines: %{
lines: [1, 2..5],
style: :theme | "custom-css" | nil,
class: "custom-class"
},
line_numbers: false,
header: %{
open_tag: "<div>",
close_tag: "</div>"
}
]} |
:bbcode_scoped |
{:bbcode_scoped, [
language: "elixir" | ".ex" | "app.ex" | nil,
highlight_lines: %{lines: [1, 2..5]}
]}
]
```
## Summary
Lumis is a fast, reliable syntax highlighter for Elixir. Key points to remember:
1. **Always specify language when known** for better performance
2. **Use `raw/1` in Phoenix templates** to prevent HTML escaping
3. **Cache highlighted output** when possible
4. **Use `:html_linked` for large applications** to reduce HTML size
5. **Use `:html_multi_themes` for light/dark mode** support with CSS custom properties
6. **Include CSS files** when using `:html_linked` formatter
7. **Handles incomplete code** gracefully for streaming scenarios
8. **110+ languages** with auto-detection support
9. **250+ built-in Neovim themes** available
10. **Line numbers** are 1-indexed in the `data-line` attribute
11. **Validate options** with `validate_options!/1` when needed
12. **Load parsers at startup** with `async_load/1` so the first request does not compile
13. **Add a `lumis_wasm_*` dependency** for every language you highlight; one that is not installed is never fetched
For more information, see the [HexDocs](https://hexdocs.pm/lumis) or the [GitHub repository](https://github.com/leandrocp/lumis).