# Formatters (/formatters)

Lumis separates parsing from rendering. The parser finds tokens; the formatter decides how those tokens become output.

## Built-in formatter types

| Output | CLI | Rust | Elixir | JavaScript / TypeScript | Java |
| --- | --- | --- | --- | --- | --- |
| HTML with inline styles | `html-inline` | `HtmlInlineBuilder` | `:html_inline` | `htmlInline()` | `Formatter.HTML_INLINE` |
| HTML with CSS classes | `html-linked` | `HtmlLinkedBuilder` | `:html_linked` | `htmlLinked()` | `Formatter.HTML_LINKED` |
| HTML with CSS variables for multiple themes | `html-multi-themes` | `HtmlMultiThemesBuilder` | `:html_multi_themes` | `htmlMultiThemes()` | — |
| ANSI terminal output | `terminal` | `TerminalBuilder` | `:terminal` | `terminal()` | `Formatter.TERMINAL` |
| BBCode with scope tags | `bbcode-scoped` | `BBCodeScopedBuilder` | `:bbcode_scoped` | `bbcodeScoped()` | `Formatter.BBCODE` |

Each built-in formatter has a dedicated page with options and more examples:

- [HTML Inline](/formatters/html-inline)
- [HTML Linked](/formatters/html-linked)
- [HTML Multi-Themes](/formatters/html-multi-themes)
- [Terminal](/formatters/terminal)
- [BBCode Scoped](/formatters/bbcode)

## HTML Inline

Self-contained output with no external CSS.

### JavaScript / TypeScript

```js
import {highlight} from '@lumis-sh/lumis';
import {htmlInline} from '@lumis-sh/lumis/formatters';
import javascript from '@lumis-sh/wasm-javascript';
import frappe from '@lumis-sh/themes/catppuccin_frappe';

const html = await highlight(
  'const x = 1',
  htmlInline({
    language: javascript,
    theme: frappe,
    preClass: 'code-block',
  })
);
```

### Rust

```rust
use lumis::{
    highlight, languages::Language, themes,
    HtmlInlineBuilder,
};

let formatter = HtmlInlineBuilder::new()
    .language(Language::Rust)
    .theme(Some(themes::get("catppuccin_frappe").unwrap()))
    .pre_class(Some("code-block".to_string()))
    .build()
    .unwrap();

let html = highlight("fn main() {}", formatter);
```

### Elixir

```elixir
Lumis.highlight!(
  "fn main() {}",
  formatter:
    {:html_inline,
     language: "rust",
     theme: "catppuccin_frappe",
     pre_class: "code-block"}
)
```

### Java

```java
import io.roastedroot.lumis4j.core.Formatter;
import io.roastedroot.lumis4j.core.Lang;
import io.roastedroot.lumis4j.core.Lumis;
import io.roastedroot.lumis4j.core.Theme;

var lumis = Lumis.builder().build();

var highlighter = lumis.highlighter()
    .withLang(Lang.RUST)
    .withTheme(Theme.CATPPUCCIN_FRAPPE)
    .withFormatter(Formatter.HTML_INLINE)
    .build();

var html = highlighter.highlight("fn main() {}").string();
lumis.close();
```

### CLI

```bash
lumis highlight main.rs \
  --formatter html-inline \
  --theme catppuccin_frappe
```


## HTML Linked

Smaller HTML. Pair with a CSS theme file.

### JavaScript / TypeScript

```js
import '@lumis-sh/themes/css/catppuccin_frappe.css';
import {highlight} from '@lumis-sh/lumis';
import {htmlLinked} from '@lumis-sh/lumis/formatters';
import javascript from '@lumis-sh/wasm-javascript';

const html = await highlight(
  'const x = 1',
  htmlLinked({language: javascript, preClass: 'code-block'})
);
```

### Rust

```rust
use lumis::{
    highlight, languages::Language, HtmlLinkedBuilder,
};

let formatter = HtmlLinkedBuilder::new()
    .language(Language::Rust)
    .pre_class(Some("code-block".to_string()))
    .build()
    .unwrap();

let html = highlight("fn main() {}", formatter);
```

### Elixir

```elixir
Lumis.highlight!(
  "fn main() {}",
  formatter:
    {:html_linked,
     language: "rust", pre_class: "code-block"}
)
```

### Java

```java
import io.roastedroot.lumis4j.core.Formatter;
import io.roastedroot.lumis4j.core.Lang;
import io.roastedroot.lumis4j.core.Lumis;

var lumis = Lumis.builder().build();

var highlighter = lumis.highlighter()
    .withLang(Lang.RUST)
    .withFormatter(Formatter.HTML_LINKED)
    .build();

var html = highlighter.highlight("fn main() {}").string();
lumis.close();
```

### CLI

```bash
lumis highlight main.rs --formatter html-linked
```


## HTML Multi-Themes

One block, multiple themes via CSS custom properties.

### JavaScript / TypeScript

```js
import {highlight} from '@lumis-sh/lumis';
import {htmlMultiThemes} from '@lumis-sh/lumis/formatters';
import javascript from '@lumis-sh/wasm-javascript';
import latte from '@lumis-sh/themes/catppuccin_latte';
import frappe from '@lumis-sh/themes/catppuccin_frappe';

const html = await highlight(
  'const x = 1',
  htmlMultiThemes({
    language: javascript,
    themes: {light: latte, dark: frappe},
    defaultTheme: 'light-dark()',
  })
);
```

### Rust

```rust
use lumis::{
    highlight, languages::Language, themes,
    HtmlMultiThemesBuilder,
};
use std::collections::HashMap;

let mut theme_map = HashMap::new();
theme_map.insert(
    "light".to_string(),
    themes::get("catppuccin_latte").unwrap(),
);
theme_map.insert(
    "dark".to_string(),
    themes::get("catppuccin_frappe").unwrap(),
);

let formatter = HtmlMultiThemesBuilder::new()
    .language(Language::JavaScript)
    .themes(theme_map)
    .default_theme("light-dark()")
    .build()
    .unwrap();

let html = highlight("const x = 1", formatter);
```

### Elixir

```elixir
Lumis.highlight!(
  "const x = 1",
  formatter:
    {:html_multi_themes,
     language: "javascript",
     themes: [
       light: "catppuccin_latte",
       dark: "catppuccin_frappe"
     ],
     default_theme: "light-dark()"}
)
```

### Java

```java
import io.roastedroot.lumis4j.core.Formatter;
import io.roastedroot.lumis4j.core.Lang;
import io.roastedroot.lumis4j.core.Lumis;
import io.roastedroot.lumis4j.core.Theme;

var lumis = Lumis.builder().build();

var highlighter = lumis.highlighter()
    .withLang(Lang.RUST)
    .withTheme(Theme.CATPPUCCIN_FRAPPE)
    .withFormatter(Formatter.TERMINAL)
    .build();

var ansi = highlighter.highlight("fn main() {}").string();
lumis.close();
```

### CLI

```bash
lumis highlight main.rs \
  --formatter html-multi-themes \
  --themes light:catppuccin_latte \
  --themes dark:catppuccin_frappe \
  --default-theme light-dark()
```


Java does not support HTML Multi-Themes in `lumis4j` at the moment.

## Tag attributes

All three HTML formatters take `pre_attrs` and `code_attrs`, so a copy button, a
tab panel or an analytics hook can carry its own `id`, `data-*` or ARIA
attributes without rewriting the tags afterwards.

Three rules decide what the tag ends up with:

- `class` is unioned with the classes Lumis generates, which are `lumis`,
  `pre_class`, the theme names on a multi-theme block, and `language-*` on
  `<code>`
- `style` is appended after the theme's own declarations
- everything else replaces the generated value, including `translate` and
  `tabindex` on `<code>`

A value is a string, or a boolean for the two things HTML spells without one:
`true` writes the bare `name` that `hidden`, `inert` and `open` take, and
`false` removes an attribute Lumis would otherwise generate, which is how
`tabindex` comes off `<code>`.

Values are escaped, so an attribute can hold quotes and angle brackets. Names
are checked instead, because escaping cannot make one safe: a space needs no
escaping and splits one name into two attributes, so `x onclick=alert(1)` would
be an event handler however its value was treated. A name carrying a space, a
quote, `=`, `/` or `>` is an error rather than markup.

### JavaScript / TypeScript

```js
htmlLinked({
  language: javascript,
  preClass: 'code-block',
  preAttrs: {
    id: 'example',
    'data-panel': 'output',
    class: 'overflow-x-auto',
  },
  codeAttrs: {id: 'example-code', tabindex: false}
})
```

### Rust

```rust
use lumis::{HtmlLinkedBuilder, languages::Language};

let formatter = HtmlLinkedBuilder::new()
    .language(Language::JavaScript)
    .pre_class(Some("code-block".to_string()))
    .pre_attrs(vec![
        ("id".to_string(), "example".into()),
        ("data-panel".to_string(), "output".into()),
        ("class".to_string(), "overflow-x-auto".into()),
    ])
    .code_attrs(vec![
        ("id".to_string(), "example-code".into()),
        ("tabindex".to_string(), false.into()),
    ])
    .build()?;
```

### Elixir

```elixir
Lumis.highlight!(code,
  formatter:
    {:html_linked,
     language: "javascript",
     pre_class: "code-block",
     pre_attrs: [
       id: "example",
       "data-panel": "output",
       class: "overflow-x-auto"
     ],
     code_attrs: [id: "example-code", tabindex: false]}
)
```

### CLI

```bash
lumis highlight main.js \
  --formatter html-linked \
  --pre-class code-block \
  --pre-attr id=example \
  --pre-attr data-panel=output \
  --pre-attr class=overflow-x-auto \
  --code-attr id=example-code \
  --no-code-attr tabindex
```


A custom formatter reaches the same values through `pre_attrs`,
`multi_themes_pre_attrs` and `code_attrs`, described in
[Custom Formatters](/formatters/custom).

### Override the block background

Use the `style` entry in `pre_attrs` (`preAttrs` in JavaScript) to set the
background of the wrapping `<pre>`. Lumis appends it after the theme's styles,
so your `background-color` wins while the theme's foreground color stays intact.

### JavaScript / TypeScript

```js
import {htmlInline} from '@lumis-sh/lumis/formatters'
import dracula from '@lumis-sh/themes/dracula'

htmlInline({
  theme: dracula,
  preAttrs: {style: 'background-color: #181818;'},
})
```

### Rust

```rust
use lumis::{HtmlInlineBuilder, themes};

let formatter = HtmlInlineBuilder::new()
    .theme(Some(themes::get("dracula").unwrap()))
    .pre_attrs(vec![("style".into(), "background-color: #181818;".into())])
    .build()
    .unwrap();
```

### Elixir

```elixir
Lumis.highlight!("text",
  formatter: {:html_inline,
    language: "plaintext",
    theme: "dracula",
    pre_attrs: [style: "background-color: #181818;"]}
)
```

### CLI

```sh
lumis highlight main.rs --formatter html-inline --theme dracula --pre-attr 'style=background-color: #181818;'
```


The opening tag contains both declarations; the later one takes effect:

```html
<pre class="lumis" style="color: #f8f8f2; background-color: #282a36; background-color: #181818;">
```

Replace `#181818` with `transparent` to let an enclosing card's background show
through. To use a color defined by your page's CSS, use
`background-color: var(--code-block-background);` and define
`--code-block-background` on the block or an ancestor.

The same attribute option works with `html_multi_themes`. It changes only the
block background; token and highlighted-line backgrounds remain unchanged.

### Adding attributes to a formatter you were handed

The options above are set when a formatter is built. Code that receives one
already built — a renderer integration highlighting many blocks, each with its
own attributes — uses `withAttrs` in the npm package, which returns a new formatter
and leaves the original alone:

```ts
import {
  htmlInline,
  withAttrs,
} from '@lumis-sh/lumis/formatters'

const base = htmlInline({
  language: javascript,
  theme: dracula,
})
const block = withAttrs(base, {
  preAttrs: {id: 'example', class: 'card'},
})
```

The attributes layer over any the formatter already carried, by the same rules,
and a `false` still removes one Lumis generates. A formatter that is not a
built-in HTML one comes back untouched, since a custom `render` has no
attribute contract to honour.

Rust and Elixir have no counterpart because they do not need one: a Rust
builder takes `pre_attrs` before `build()`, and an Elixir formatter is a
keyword list the caller can add to. Only a formatter from the npm package is already
closed over its own options by the time you hold it.

`rehype-lumis` and `markdown-it-lumis` are both built on this, which is how a
`<pre>` carrying an `id`, a `data-panel` and Tailwind classes survives being
highlighted.

## Inline code

The HTML formatters write a code block by default. With `structure` set to
`inline`, they write the token spans and nothing else, for code that goes inside
markup the page already has: a `<code>` in a sentence, a table cell, a button.

### JavaScript / TypeScript

```js
import {highlight} from '@lumis-sh/lumis';
import {htmlInline} from '@lumis-sh/lumis/formatters';
import bash from '@lumis-sh/wasm-bash';
import frappe from '@lumis-sh/themes/catppuccin_frappe';

const spans = await highlight(
  'mix igniter.install mdex',
  htmlInline({language: bash, theme: frappe, structure: 'inline'})
);
```

### Rust

```rust
use lumis::{
    formatters::html::HtmlStructure, highlight,
    languages::Language, themes, HtmlInlineBuilder,
};

let formatter = HtmlInlineBuilder::new()
    .language(Language::Bash)
    .theme(Some(themes::get("catppuccin_frappe").unwrap()))
    .structure(HtmlStructure::Inline)
    .build()
    .unwrap();

let spans = highlight("mix igniter.install mdex", formatter);
```

### Elixir

```elixir
Lumis.highlight!(
  "mix igniter.install mdex",
  formatter:
    {:html_inline,
     language: "bash",
     theme: "catppuccin_frappe",
     structure: :inline}
)
```

### CLI

```bash
lumis highlight install.sh \
  --formatter html-inline \
  --theme catppuccin_frappe \
  --structure inline
```


A block of the same source is:

```html
<pre class="lumis" style="color: #c6d0f5; background-color: #303446;"><code class="language-bash" translate="no" tabindex="0"><span class="l-line" data-line="1"><span style="color: #8caaee;">mix</span> <span style="color: #a6d189;">igniter.install</span> <span style="color: #a6d189;">mdex</span></span></code></pre>
```

The inline output is what the line held:

```html
<span style="color: #8caaee;">mix</span> <span style="color: #a6d189;">igniter.install</span> <span style="color: #a6d189;">mdex</span>
```

It goes inside an element of yours, where a `<pre>` or a line element would not
be valid HTML:

```html
<p>Install it with <code><span style="color: #8caaee;">mix</span> …</code>, then restart the server.</p>
```

Source with several lines keeps its newlines. Lines are separated by `\n`, the
last one has no terminator, and a token that crosses a line is closed before the
newline and reopened after it, as in a block. What belongs to the block is left
out:

- the `<pre>`, the `<code>` and the `<span class="l-line">` elements
- the theme's text and background color, which a block writes on its `<pre>`.
  Text outside a token takes the color of the element around it
- whatever `pre_class`, `pre_attrs`, `code_attrs`, `highlight_lines`,
  `line_numbers` and `header` would add. These options are ignored rather than
  rejected, so code that highlights both code blocks and inline code can pass
  the same options to both and add `structure` for the inline ones
- the `data-lumis-budget` marker, which is an attribute of the `<pre>`

### Keeping the theme's colors

To give inline code the theme's text and background color, put them on your own
element. `pre_attrs` and `multi_themes_pre_attrs` return the class and style a
block's `<pre>` would get, and `open_tag` writes them on any tag:

### JavaScript / TypeScript

```js
import {openTag, preAttrs} from '@lumis-sh/lumis/formatters/html';

const html = `${openTag('code', preAttrs({theme: frappe}))}${spans}</code>`;
// <code class="lumis" style="color: #c6d0f5; background-color: #303446;">…</code>
```

### Rust

```rust
use lumis::formatters::html::{open_tag, pre_attrs};

let theme = themes::get("catppuccin_frappe").unwrap();
let mut html = Vec::new();
open_tag(&mut html, "code", &pre_attrs(None, Some(&theme), &[]))?;
html.extend_from_slice(spans.as_bytes());
html.extend_from_slice(b"</code>");
```

### Elixir

```elixir
alias Lumis.Formatter.HTML

attrs = HTML.pre_attrs(theme: "catppuccin_frappe")
html = HTML.open_tag("code", attrs) <> spans <> "</code>"
```


In JavaScript, `toInline` from `@lumis-sh/lumis/formatters` derives both from
a formatter you already have: the same formatter with `structure: 'inline'`, and
the attributes for your element, merged with any you pass:

```js
import {toInline} from '@lumis-sh/lumis/formatters';

const inline = toInline(htmlInline({language: bash, theme: frappe}), {id: 'install'});
const html = `${openTag('code', inline.attrs)}${await highlight(source, inline.formatter)}</code>`;
```

The Markdown and HTML integrations use it for their `inlineCode` option; see
[rehype-lumis](/integrations/rehype-lumis#inline-code).

With HTML Linked, add the `lumis` class to your element and the theme stylesheet
colors it. The stylesheet's line layout rules only apply inside `pre.lumis`, so
they leave inline code alone.

`light-dark()` colors from HTML Multi-Themes follow the `color-scheme` of the
element they are in, as they do in a block. Leave it unset to inherit the page's
choice, or set `color-scheme: light dark` on your element to follow the OS
preference. Inline output ignores `pre_attrs` and `code_attrs`; put the scheme
on your own element. See [Light/Dark Mode](/recipes/light-dark#inline-code).

## Terminal

ANSI output for the terminal.

### JavaScript / TypeScript

```js
import {highlight} from '@lumis-sh/lumis';
import {terminal} from '@lumis-sh/lumis/formatters';
import javascript from '@lumis-sh/wasm-javascript';
import frappe from '@lumis-sh/themes/catppuccin_frappe';

const ansi = await highlight(
  'const x = 1',
  terminal({language: javascript, theme: frappe})
);
```

### Rust

```rust
use lumis::{
    highlight, languages::Language, themes, TerminalBuilder,
};

let formatter = TerminalBuilder::new()
    .language(Language::Rust)
    .theme(Some(themes::get("catppuccin_frappe").unwrap()))
    .build()
    .unwrap();

let ansi = highlight("fn main() {}", formatter);
println!("{}", ansi);
```

### Elixir

```elixir
Lumis.highlight!(
  "fn main() {}",
  formatter:
    {:terminal,
     language: "rust", theme: "catppuccin_frappe"}
)
```

### Java

```java
import io.roastedroot.lumis4j.core.Formatter;
import io.roastedroot.lumis4j.core.Lang;
import io.roastedroot.lumis4j.core.Lumis;
import io.roastedroot.lumis4j.core.Theme;

var lumis = Lumis.builder().build();

var highlighter = lumis.highlighter()
    .withLang(Lang.RUST)
    .withTheme(Theme.CATPPUCCIN_FRAPPE)
    .withFormatter(Formatter.TERMINAL)
    .build();

var ansi = highlighter.highlight("fn main() {}").string();
lumis.close();
```

### CLI

```bash
lumis highlight main.rs --theme catppuccin_frappe
```


## BBCode Scoped

Scope-based BBCode output for BBCode-aware renderers and custom pipelines.

### JavaScript / TypeScript

```js
import {highlight} from '@lumis-sh/lumis';
import {bbcodeScoped} from '@lumis-sh/lumis/formatters';
import javascript from '@lumis-sh/wasm-javascript';

const output = await highlight(
  'const x = "[url=x]"',
  bbcodeScoped({language: javascript})
);
```

### Rust

```rust
use lumis::{
    highlight, languages::Language, BBCodeScopedBuilder,
};

let output = highlight(
    "fn main() {}",
    BBCodeScopedBuilder::new()
        .language(Language::Rust)
        .build()
        .unwrap(),
);
```

### Elixir

```elixir
Lumis.highlight!(
  ~s(const x = "[url=x]"),
  formatter: {:bbcode_scoped, language: "javascript"}
)
```

### Java

```java
import io.roastedroot.lumis4j.core.Formatter;
import io.roastedroot.lumis4j.core.Lang;
import io.roastedroot.lumis4j.core.Lumis;

var lumis = Lumis.builder().build();

var highlighter = lumis.highlighter()
    .withLang(Lang.JAVASCRIPT)
    .withFormatter(Formatter.BBCODE)
    .build();

var output =
    highlighter.highlight("const x = \"[url=x]\"").string();
lumis.close();
```

### CLI

```bash
lumis highlight main.rs --formatter bbcode-scoped
```


## Custom formatters

Rust and the npm package expose lower-level formatter APIs so you can render tokens however you want.

- Rust: implement the [`Formatter`](https://docs.rs/lumis/latest/lumis/formatters/trait.Formatter.html) trait
- JavaScript / TypeScript: pass a formatter object with `render(source, events)` and an optional `language`. Lumis supplies the highlighted events; use the sync `highlightIter()` or `highlightEvents()` helpers inside `render()` only if you need to highlight more source — see the [Formatter interface](https://github.com/leandrocp/lumis/blob/main/packages/javascript/lumis/src/types.ts)

For deeper formatter-specific options, use the dedicated pages above. For theme data and formatter helpers, continue with [Themes](/themes).