Custom Formatters
Write your own formatter when the built-in ones don't produce the output you need. Common reasons:
- wrap output with custom HTML
- emit Markdown, LaTeX, or a structured AST
- add annotations, data attributes, or extra classes
- feed Lumis tokens into an existing rendering pipeline
Rust
Implement the Formatter trait.
use lumis::{
formatters::Formatter,
formatters::html::{open_pre_tag, open_code_tag, closing_tags, span_inline},
highlight::highlight_iter,
languages::Language,
themes,
};
use std::io::{self, Write};
struct MinimalHtmlFormatter {
language: Language,
theme: Option<themes::Theme>,
}
impl Formatter for MinimalHtmlFormatter {
fn format(&self, source: &str, output: &mut dyn Write) -> io::Result<()> {
open_pre_tag(output, None, self.theme.as_ref())?;
open_code_tag(output, &self.language)?;
highlight_iter(source, self.language, self.theme.clone(), |text, language, _range, scope, _style| {
write!(output, "{}", span_inline(text, Some(language), scope, self.theme.as_ref(), false, false))
})
.map_err(io::Error::other)?;
closing_tags(output)?;
Ok(())
}
}
JavaScript
Pass an object with language and format(source). Inside format(), use the sync free functions highlightIter() or highlightEvents() imported from @lumis-sh/lumis.
import {createHighlighter, highlightIter} from '@lumis-sh/lumis'
import type {Formatter} from '@lumis-sh/lumis/formatters'
import {openPreTag, openCodeTag, closingTags, spanInline} from '@lumis-sh/lumis/formatters/html'
import rust from '@lumis-sh/lumis/langs/rust'
import frappe from '@lumis-sh/themes/catppuccin_frappe'
const hl = await createHighlighter({languages: [rust]})
const formatter: Formatter = {
language: rust,
format(source) {
const parts: string[] = []
parts.push(openPreTag({preClass: 'docs-demo', theme: frappe}))
parts.push(openCodeTag(this.language))
highlightIter(source, this.language, frappe, (text, language, _range, scope, _style) => {
if (scope) {
parts.push(spanInline(text, {language, scope, theme: frappe}))
} else {
parts.push(text)
}
})
parts.push(closingTags())
return parts.join('')
},
}
const html = hl.highlight('fn main() {}', formatter)
Highlight options
The built-in formatters take options that change which scopes highlighting produces. Custom formatters take the same options, as a trailing argument to the token iterator.
- JavaScript
- Rust
highlightIter(source, this.language, frappe, (text, language, _range, scope) => {
// scope is punctuation.bracket.rainbow.1 .. .6 on bracket pairs
}, {rainbowBrackets: true})
use lumis::highlight::{highlight_iter_with_options, HighlightOptions};
let options = HighlightOptions { rainbow_brackets: true };
highlight_iter_with_options(source, self.language, self.theme.clone(), options, |text, language, _range, scope, _style| {
// scope is punctuation.bracket.rainbow.1 .. .6 on bracket pairs
write!(output, "{}", span_inline(text, Some(language), scope, self.theme.as_ref(), false, false))
})
.map_err(io::Error::other)?;
Nested events instead of flat tokens
highlightIter / highlight_iter hand you one callback per token. When the
nesting matters — a string scope wrapping injected tag scopes inside a
template literal — take the events instead. A start opens a scope, source
events carry byte ranges, and end closes it.
- JavaScript
- Rust
import {highlightEvents} from '@lumis-sh/lumis'
for (const event of highlightEvents(source, rust, {rainbowBrackets: true})) {
if (event.type === 'start') console.log(event.scope, event.language)
}
use lumis::events::HighlightEvent;
use lumis::highlight::{highlight_events_with_options, HighlightOptions};
use lumis::languages::Language;
let options = HighlightOptions { rainbow_brackets: true };
for event in highlight_events_with_options(source, Language::Rust, options)? {
if let HighlightEvent::Start { .. } = event {
println!("{:?} {:?}", event.scope(), event.language());
}
}
# Ok::<(), lumis::highlight::HighlightError>(())
Rust stores the scope as an index into HIGHLIGHT_NAMES because resolving it
per event costs more than the formatters need. event.scope() returns the same
name JavaScript's event carries directly.
Available helpers
| Runtime | Module | Reference |
|---|---|---|
| JavaScript | @lumis-sh/lumis/formatters/html | source |
| JavaScript | @lumis-sh/lumis/formatters/ansi | source |
| Rust | lumis::formatters::html | docs.rs |
| Rust | lumis::formatters::ansi | docs.rs |
Tip
Copy the built-in formatter closest to what you want, then strip out what you don't need.