The ANSI pieces the built-in :terminal formatter is assembled from.
A module implementing Lumis.Formatter gets an event stream and has to turn
it into output; these helpers apply the same colors, text decorations, and
reset behavior as the built-in formatter.
Example
defmodule MyTerminalFormatter do
@behaviour Lumis.Formatter
alias Lumis.Formatter.ANSI
@impl true
def render(source, events, options) do
theme = Keyword.get(options, :theme)
{output, _scopes, _tables} =
Enum.reduce(events, {[], [], %{}}, fn
{:start, %{scope: scope, language: language}}, {output, scopes, tables} ->
{output, [{scope, language} | scopes], tables}
:end, {output, [_scope | scopes], tables} ->
{output, scopes, tables}
{:source, %{start: start, end: stop}}, {output, scopes, tables} ->
text = binary_part(source, start, stop - start)
case scopes do
[] ->
{[text | output], scopes, tables}
[{scope, language} | _rest] ->
# One table per language, built once and read per token.
tables =
Map.put_new_lazy(tables, language, fn ->
ANSI.styles(theme: theme, language: language)
end)
painted = ANSI.paint(text, ANSI.style_for(tables[language], scope))
{[painted | output], scopes, tables}
end
# Lumis adds event kinds as it grows. Render the ones you know and
# skip the rest, or a newer Lumis raises FunctionClauseError here.
_event, state ->
state
end)
Enum.reverse(output)
end
end
Lumis.highlight!("defmodule App do\nend",
formatter: {MyTerminalFormatter, language: "elixir", theme: "dracula"}
)
Summary
Functions
Converts a six-digit hex color to an RGB tuple.
Paints text with a Lumis.Theme.Style.
Returns the ANSI sequence that clears all formatting.
Builds a 24-bit ANSI color escape sequence from RGB components.
A scope's style, read out of a styles/1 table.
Converts a Lumis.Theme.Style to ANSI escape sequences.
Every scope's style for one theme and language.
Functions
@spec hex_to_rgb(String.t()) :: {0..255, 0..255, 0..255} | nil
Converts a six-digit hex color to an RGB tuple.
A leading # is optional. Returns nil when the color is not six hexadecimal
digits.
iex> Lumis.Formatter.ANSI.hex_to_rgb("#ff79c6")
{255, 121, 198}
iex> Lumis.Formatter.ANSI.hex_to_rgb("fff")
nil
@spec paint(String.t(), Lumis.Theme.Style.t() | nil) :: String.t()
Paints text with a Lumis.Theme.Style.
The result uses the same reset and newline handling as the built-in
:terminal formatter. Text with an empty style, or with the nil that
style_for/2 returns for an unstyled scope, is returned unchanged.
@spec reset() :: String.t()
Returns the ANSI sequence that clears all formatting.
Builds a 24-bit ANSI color escape sequence from RGB components.
Set is_background to true for a background color and false for a
foreground color.
@spec style_for(%{required(String.t()) => Lumis.Theme.Style.t()}, String.t() | nil) :: Lumis.Theme.Style.t() | nil
A scope's style, read out of a styles/1 table.
Returns nil for a scope the theme styles in no way, which paint/2 renders
unchanged.
@spec style_to_ansi(Lumis.Theme.Style.t()) :: String.t()
Converts a Lumis.Theme.Style to ANSI escape sequences.
Covers foreground and background colors, bold, italic, strikethrough, and
every underline style supported by Lumis.Theme.TextDecoration.
@spec styles(keyword()) :: %{required(String.t()) => Lumis.Theme.Style.t()}
Every scope's style for one theme and language.
Resolving a scope is a per-token operation, so this is the whole table: build
it once outside the loop and read it inside with style_for/2. A scope the
theme styles in no way is absent from it.
A scope resolves the way :terminal resolves it, which is not a lookup in
theme.highlights — tag.delimiter falls back to tag, and a theme can
style a scope per language. An injected block carries its own language on its
:start event, so a document with injections needs one table per language.
Options
:theme(Lumis.Theme.t/0or a theme name) — the theme to resolve against. Without one the table is empty, which paints nothing.:language— the language whose specialized scopes to prefer, e.g.comment.elixirovercomment. Defaults to"plaintext".