The HTML pieces the built-in formatters are assembled from.
A module implementing Lumis.Formatter gets an event stream and has to turn
it into markup; these are the parts of that job worth not writing again.
Every function here calls into the same code the built-in :html_inline and
:html_linked formatters use, so output built with them is styled by the same
theme stylesheets and escapes the same characters.
Example
defmodule MyFormatter do
@behaviour Lumis.Formatter
alias Lumis.Formatter.HTML
@impl true
def render(source, events, options) do
language = Keyword.fetch!(options, :language)
attrs = HTML.span_attrs(theme: Keyword.get(options, :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))
# Lumis adds event kinds over time. Render the ones you know.
_event -> []
end)
[
HTML.open_pre_tag(theme: Keyword.get(options, :theme)),
HTML.open_code_tag(language),
body,
HTML.closing_tags()
]
end
end
Summary
Functions
Every highlight scope mapped to the CSS class :html_linked gives it.
The closing </code> tag.
The closing </pre> tag.
Both closing tags, in the order open_code_tag/1 and open_pre_tag/1 opened them.
Attributes for the <code> tag for a language.
Escapes &, <, >, " and '.
Escapes a value going inside a double-quoted attribute.
Escapes { and }, which a templating language such as HEEx would otherwise read.
The CSS class a highlighted line carries, or nil when the line is not highlighted.
Whether a line falls inside a list of line numbers and ranges.
Attributes for the <pre> tag used by a multi-theme block.
The opening <code> tag for a language.
The opening <pre> tag for a multi-theme block.
The opening <pre> tag, carrying a theme's own colors when one is given.
An opening <span> for a scope, with its attributes from a span_attrs/1 table.
An opening tag built from an attribute keyword list, with every value escaped.
Attributes for the <pre> tag used by inline and linked HTML.
The event stream rendered into HTML lines, one string per line.
A theme name with everything but letters, digits, - and _ replaced by -.
The CSS class for a scope, or "l-text" for one Lumis does not know.
Every scope's <span> attributes for one theme and language.
A <span> with a theme's colors written inline, with text escaped.
A scope's <span> attributes, read out of a span_attrs/1 table.
A <span> carrying a scope's CSS class, with text escaped.
The class attribute for a scope, for output styled by a theme stylesheet.
A <span> carrying every theme's style as CSS custom properties, with text escaped.
Every scope's <span> attributes for a set of themes, as CSS custom properties.
A Lumis.Theme.Style as inline CSS declarations.
The CSS text-decoration value for a Lumis.Theme.TextDecoration.
Whether a name is one HTML can carry on an attribute.
Wraps one rendered line in the <span> the built-in HTML formatters emit.
Functions
Every highlight scope mapped to the CSS class :html_linked gives it.
Read once and keep it: scope_to_class/1 looks a scope up in this, and calling
it per token is the whole reason the table exists.
@spec close_code_tag() :: String.t()
The closing </code> tag.
iex> Lumis.Formatter.HTML.close_code_tag()
"</code>"
@spec close_pre_tag() :: String.t()
The closing </pre> tag.
iex> Lumis.Formatter.HTML.close_pre_tag()
"</pre>"
@spec closing_tags() :: String.t()
Both closing tags, in the order open_code_tag/1 and open_pre_tag/1 opened them.
iex> Lumis.Formatter.HTML.closing_tags()
"</code></pre>"
@spec code_attrs(String.t() | nil, Lumis.html_attrs()) :: Lumis.html_attrs()
Attributes for the <code> tag for a language.
attrs is merged after the language class and the translate and tabindex
defaults. Classes are unioned; every other authored value wins.
Escapes &, <, >, " and '.
iex> Lumis.Formatter.HTML.escape(~s|a < b && c|)
"a < b && c"
Escapes a value going inside a double-quoted attribute.
The helpers here already escape every attribute they build; this is for a
formatter that assembles its own tags. The escape set is escape/1's, which is
safe for CSS in an attribute because the HTML parser decodes the entity before
the CSS parser sees it.
iex> Lumis.Formatter.HTML.escape_attr(~s|x"><script>|)
"x"><script>"
iex> Lumis.Formatter.HTML.escape_attr("font-family: 'Fira Code'")
"font-family: 'Fira Code'"
Escapes { and }, which a templating language such as HEEx would otherwise read.
iex> Lumis.Formatter.HTML.escape_braces("%{a: 1}")
"%{a: 1}"
@spec highlight_line_class([pos_integer() | Range.t()], pos_integer(), keyword()) :: String.t() | nil
The CSS class a highlighted line carries, or nil when the line is not highlighted.
Reads lines the way line_is_highlighted/2 does, including range steps and
direction.
Options
:class— the class to use, taking precedence over:default_class:default_class— what to fall back to, e.g."l-highlighted"
Example
iex> Lumis.Formatter.HTML.highlight_line_class([1, 3..5], 4, default_class: "l-highlighted")
"l-highlighted"
iex> Lumis.Formatter.HTML.highlight_line_class([1, 3..5], 2, default_class: "l-highlighted")
nil
@spec line_is_highlighted([pos_integer() | Range.t()], pos_integer()) :: boolean()
Whether a line falls inside a list of line numbers and ranges.
Lines are 1-based, matching the data-line wrap_line/3 writes and the
:highlight_lines option the built-in formatters take.
A range's step counts: 1..9//2 is five lines, not nine. Stepped ranges stay
compact across the native boundary, regardless of their declared span.
iex> Lumis.Formatter.HTML.line_is_highlighted([1, 3..5], 4)
true
iex> Lumis.Formatter.HTML.line_is_highlighted([1, 3..5], 2)
false
iex> Lumis.Formatter.HTML.line_is_highlighted([1..9//2], 4)
false
@spec multi_themes_pre_attrs(keyword()) :: Lumis.html_attrs()
Attributes for the <pre> tag used by a multi-theme block.
Carries lumis, lumis-themes and one class per theme name, so a stylesheet
can select the active theme, and the same normal colors
span_multi_themes_attrs/1 writes per scope.
Options
:themes(required) — the same keyword list or mapspan_multi_themes_attrs/1takes:default_theme— the theme written inline;"light-dark()"writes both thelightanddarkthemes into CSSlight-dark()calls:css_variable_prefix(default"--lumis") — the custom property prefix:class— appended to the classes above:attrs— additional attribute keyword list, merged after generated values
@spec open_code_tag(String.t() | nil, Lumis.html_attrs()) :: String.t()
The opening <code> tag for a language.
iex> Lumis.Formatter.HTML.open_code_tag("elixir")
~s|<code class="language-elixir" translate="no" tabindex="0">|A formatter reads its language from the :language option render/3 receives,
which Lumis has already resolved by detection when the caller named none.
An optional second argument supplies the attribute keyword list accepted by
code_attrs/2.
The opening <pre> tag for a multi-theme block.
Accepts the same options as multi_themes_pre_attrs/1.
Example
iex> Lumis.Formatter.HTML.open_multi_themes_pre_tag(themes: [light: "github_light"], default_theme: "light")
~s|<pre class="lumis lumis-themes light" style="color:#1f2328; background-color:#ffffff;">|
The opening <pre> tag, carrying a theme's own colors when one is given.
Accepts the same options as pre_attrs/1.
Example
iex> Lumis.Formatter.HTML.open_pre_tag(class: "my-block")
~s|<pre class="lumis my-block">|
An opening <span> for a scope, with its attributes from a span_attrs/1 table.
A scope the theme does not style opens a bare <span>, so every <span> still
pairs with the </span> an :end event writes.
@spec open_tag(String.t(), Lumis.html_attrs()) :: String.t()
An opening tag built from an attribute keyword list, with every value escaped.
This is what pre_attrs/1, multi_themes_pre_attrs/1 and code_attrs/2 are
built for: merge their result with your own attributes, then render the whole
thing here instead of assembling the string yourself.
iex> Lumis.Formatter.HTML.open_tag("pre", class: "lumis", hidden: true)
~s|<pre class="lumis" hidden>|Raises ArgumentError for a name HTML cannot carry, per valid_attr_name?/1.
@spec pre_attrs(keyword()) :: Lumis.html_attrs()
Attributes for the <pre> tag used by inline and linked HTML.
Options
:theme(Lumis.Theme.t/0or a theme name) — writes the theme'snormalcolors into astyleattribute, the way:html_inlinedoes:class— appended to thelumisclass every Lumis block carries:attrs— additional attribute keyword list. Classes are unioned, styles are appended, and every other value wins.
@spec render_lines_from_events(String.t(), [Lumis.Formatter.event(term())], %{ required(String.t()) => String.t() }) :: [String.t()]
The event stream rendered into HTML lines, one string per line.
A <span> that crosses a newline is closed at the end of one line and reopened
at the start of the next, so every line's tags nest on their own and can be
wrapped with wrap_line/3 independently. That closing and reopening is the
part worth not writing again.
Each returned line contains only its content, without LF or CRLF terminators.
A final newline ends the last line rather than adding an empty one.
Join wrap_line/3 results with "\n" to build an HTML block.
attrs maps a scope to the attributes its <span> carries, so the whole render
costs one call rather than one per token: a span_attrs/1 or
span_multi_themes_attrs/1 table, or for class-based output,
Map.new(classes(), fn {scope, _} -> {scope, span_linked_attrs(scope)} end).
A scope the table does not carry opens a bare <span>.
Event kinds this build does not render — annotations, and anything a newer Lumis adds — are skipped rather than raising.
Example
iex> events = [{:start, %{scope: "keyword", language: "elixir"}}, {:source, %{start: 0, end: 3}}, :end]
iex> Lumis.Formatter.HTML.render_lines_from_events("a\nb", events, %{"keyword" => ~s|class="l-keyword"|})
[~s|<span class="l-keyword">a</span>|, ~s|<span class="l-keyword">b</span>|]
A theme name with everything but letters, digits, - and _ replaced by -.
The form a theme name takes inside the CSS custom properties
span_multi_themes_attrs/1 writes.
iex> Lumis.Formatter.HTML.sanitize_theme_name("Catppuccin Mocha")
"Catppuccin-Mocha"
The CSS class for a scope, or "l-text" for one Lumis does not know.
iex> Lumis.Formatter.HTML.scope_to_class("keyword.function")
"l-keyword-function"
iex> Lumis.Formatter.HTML.scope_to_class("not.a.scope")
"l-text"
Every scope's <span> attributes for one theme and language.
The inline-style counterpart of classes/0, and the same advice applies: build
it once per document and read it per token with open_span/2 or
span_inline_attrs/2.
Options
:theme(Lumis.Theme.t/0or a theme name) — resolves each scope to its style. Without one every scope's attributes are"", which is a<span>with nothing on it.:language— the language whose specialized scopes to prefer, e.g.comment.elixirovercomment. Defaults to"plaintext".:italic(defaultfalse) — emitfont-style: italicfor a style that asks for it:include_highlights(defaultfalse) — adddata-highlight="<scope>"
@spec span_inline(String.t(), %{required(String.t()) => String.t()}, String.t() | nil) :: String.t()
A <span> with a theme's colors written inline, with text escaped.
Convenient for a one-off; in a loop, hoist span_attrs/1 out and use
open_span/2 so the table is not rebuilt per token.
A scope's <span> attributes, read out of a span_attrs/1 table.
Returns "" for a scope the theme styles in no way, which is a <span> with
no attributes rather than one with an empty style.
A <span> carrying a scope's CSS class, with text escaped.
iex> Lumis.Formatter.HTML.span_linked("defmodule", "keyword.function")
~s|<span class="l-keyword-function">defmodule</span>|
The class attribute for a scope, for output styled by a theme stylesheet.
iex> Lumis.Formatter.HTML.span_linked_attrs("keyword")
~s|class="l-keyword"|
@spec span_multi_themes( String.t(), %{required(String.t()) => String.t()}, String.t() | nil ) :: String.t()
A <span> carrying every theme's style as CSS custom properties, with text escaped.
attrs is a span_multi_themes_attrs/1 table. The inline counterpart is
span_inline/3, and the two differ only in which table they read.
Every scope's <span> attributes for a set of themes, as CSS custom properties.
The multi-theme counterpart of span_attrs/1, and the same advice applies:
build it once per document and read it per token with open_span/2. The result
is a table open_span/2 and span_multi_themes/3 both read.
Each theme's style becomes --<prefix>-<theme> custom properties, so one
document can be restyled by CSS alone. :default_theme names the one written
inline; the rest stay variables.
Options
:themes(required) — the same[light: "github_light", dark: dark_theme]keyword list:html_multi_themestakes, or a map. The names become the CSS variable suffixes and the<pre>classesopen_multi_themes_pre_tag/1writes.:default_theme— the theme written inline."light-dark()"writes the colors of the themes namedlightanddarkinto CSSlight-dark()calls; for font weight, font style and text decoration, a value the two share is an ordinary declaration, and a value they disagree on is one variable per theme with nothing inline, for a rule of your own to switch. Without one every theme is a variable and nothing is inline.:css_variable_prefix(default"--lumis") — the custom property prefix:language— the language whose specialized scopes to prefer, e.g.comment.elixirovercomment. Defaults to"plaintext".:italic(defaultfalse) — emitfont-style: italicfor a style that asks for it:include_highlights(defaultfalse) — adddata-highlight="<scope>"
@spec style_to_css(Lumis.Theme.Style.t(), keyword()) :: String.t()
A Lumis.Theme.Style as inline CSS declarations.
The same declarations span_attrs/1 puts in a style attribute, for a
formatter that styles something other than a <span>.
Options
:italic(defaultfalse) — emitfont-style: italicfor a style that asks for it:separator(default" ") — what goes between declarations
Example
iex> Lumis.Formatter.HTML.style_to_css(%Lumis.Theme.Style{fg: "#ff79c6", bold: true})
"color: #ff79c6; font-weight: bold;"
@spec text_decoration(Lumis.Theme.TextDecoration.t()) :: String.t()
The CSS text-decoration value for a Lumis.Theme.TextDecoration.
iex> Lumis.Formatter.HTML.text_decoration(%Lumis.Theme.TextDecoration{underline: :wavy, strikethrough: true})
"underline wavy line-through"
iex> Lumis.Formatter.HTML.text_decoration(%Lumis.Theme.TextDecoration{})
"none"
Whether a name is one HTML can carry on an attribute.
A name with a space, a quote, =, / or > in it cannot be escaped into
safety, because a space alone splits it into two attributes and the second one
can be an event handler. open_tag/2 rejects those rather than writing them.
iex> Lumis.Formatter.HTML.valid_attr_name?("data-copy")
true
iex> Lumis.Formatter.HTML.valid_attr_name?("x onclick=alert(1)")
false
@spec wrap_line(pos_integer(), iodata(), keyword()) :: String.t()
Wraps one rendered line in the <span> the built-in HTML formatters emit.
Lines are 1-based, and data-line is what a "highlight these lines" feature
and anchor links both key off.
content goes in verbatim. Pass content-only lines from
render_lines_from_events/3, then join the wrapped lines with "\n".
Options
:class_suffix— appended tol-lineverbatim, so pass a leading space, e.g." l-highlighted":style— astyleattribute for the line
Example
iex> Lumis.Formatter.HTML.wrap_line(2, "code")
~s|<span class="l-line" data-line="2">code</span>|