Lumis.Formatter.ANSI (Lumis v0.10.0)

Copy Markdown View Source

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.

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

hex_to_rgb(hex)

@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

paint(text, style)

@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.

reset()

@spec reset() :: String.t()

Returns the ANSI sequence that clears all formatting.

rgb_to_ansi(r, g, b, is_background)

@spec rgb_to_ansi(0..255, 0..255, 0..255, boolean()) :: String.t()

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.

style_for(styles, scope)

@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.

style_to_ansi(style)

@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.

styles(options \\ [])

@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/0 or 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.elixir over comment. Defaults to "plaintext".