# `Lumis.Formatter.HTML`
[🔗](https://github.com/leandrocp/lumis/blob/hex-lumis/v0.10.0/packages/elixir/lumis/lib/lumis/formatter/html.ex#L1)

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

# `classes`

```elixir
@spec classes() :: %{required(String.t()) =&gt; String.t()}
```

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.

# `close_code_tag`

```elixir
@spec close_code_tag() :: String.t()
```

The closing `</code>` tag.

    iex> Lumis.Formatter.HTML.close_code_tag()
    "</code>"

# `close_pre_tag`

```elixir
@spec close_pre_tag() :: String.t()
```

The closing `</pre>` tag.

    iex> Lumis.Formatter.HTML.close_pre_tag()
    "</pre>"

# `closing_tags`

```elixir
@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>"

# `code_attrs`

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

# `escape`

```elixir
@spec escape(String.t()) :: String.t()
```

Escapes `&`, `<`, `>`, `"` and `'`.

    iex> Lumis.Formatter.HTML.escape(~s|a < b && c|)
    "a &lt; b &amp;&amp; c"

# `escape_attr`

```elixir
@spec escape_attr(String.t()) :: String.t()
```

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&quot;&gt;&lt;script&gt;"

    iex> Lumis.Formatter.HTML.escape_attr("font-family: 'Fira Code'")
    "font-family: &#39;Fira Code&#39;"

# `escape_braces`

```elixir
@spec escape_braces(String.t()) :: String.t()
```

Escapes `{` and `}`, which a templating language such as HEEx would otherwise read.

    iex> Lumis.Formatter.HTML.escape_braces("%{a: 1}")
    "%&lbrace;a: 1&rbrace;"

# `highlight_line_class`

```elixir
@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

# `line_is_highlighted`

```elixir
@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

# `multi_themes_pre_attrs`

```elixir
@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 map `span_multi_themes_attrs/1` takes
  * `:default_theme` — the theme written inline; `"light-dark()"` writes both
    the `light` and `dark` themes into CSS `light-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

# `open_code_tag`

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

# `open_multi_themes_pre_tag`

```elixir
@spec open_multi_themes_pre_tag(keyword()) :: String.t()
```

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;">|

# `open_pre_tag`

```elixir
@spec open_pre_tag(keyword()) :: String.t()
```

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">|

# `open_span`

```elixir
@spec open_span(%{required(String.t()) =&gt; String.t()}, String.t() | nil) :: String.t()
```

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.

# `open_tag`

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

# `pre_attrs`

```elixir
@spec pre_attrs(keyword()) :: Lumis.html_attrs()
```

Attributes for the `<pre>` tag used by inline and linked HTML.

## Options

  * `:theme` (`t:Lumis.Theme.t/0` or a theme name) — writes the theme's
    `normal` colors into a `style` attribute, the way `:html_inline` does
  * `:class` — appended to the `lumis` class every Lumis block carries
  * `:attrs` — additional attribute keyword list. Classes are unioned, styles
    are appended, and every other value wins.

# `render_lines_from_events`

```elixir
@spec render_lines_from_events(String.t(), [Lumis.Formatter.event(term())], %{
  required(String.t()) =&gt; 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>|]

# `sanitize_theme_name`

```elixir
@spec sanitize_theme_name(String.t()) :: String.t()
```

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"

# `scope_to_class`

```elixir
@spec scope_to_class(String.t() | nil) :: String.t()
```

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"

# `span_attrs`

```elixir
@spec span_attrs(keyword()) :: %{required(String.t()) =&gt; String.t()}
```

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` (`t:Lumis.Theme.t/0` or 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.elixir` over `comment`. Defaults to `"plaintext"`.
  * `:italic` (default `false`) — emit `font-style: italic` for a style that
    asks for it
  * `:include_highlights` (default `false`) — add `data-highlight="<scope>"`

# `span_inline`

```elixir
@spec span_inline(String.t(), %{required(String.t()) =&gt; 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.

# `span_inline_attrs`

```elixir
@spec span_inline_attrs(%{required(String.t()) =&gt; String.t()}, String.t() | nil) ::
  String.t()
```

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

# `span_linked`

```elixir
@spec span_linked(String.t(), String.t() | nil) :: String.t()
```

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

# `span_linked_attrs`

```elixir
@spec span_linked_attrs(String.t() | nil) :: String.t()
```

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"|

# `span_multi_themes`

```elixir
@spec span_multi_themes(
  String.t(),
  %{required(String.t()) =&gt; 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.

# `span_multi_themes_attrs`

```elixir
@spec span_multi_themes_attrs(keyword()) :: %{required(String.t()) =&gt; String.t()}
```

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_themes` takes, or a map. The names become the CSS
    variable suffixes and the `<pre>` classes `open_multi_themes_pre_tag/1`
    writes.
  * `:default_theme` — the theme written inline. `"light-dark()"` writes the
    colors of the themes named `light` and `dark` into CSS `light-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.elixir` over `comment`. Defaults to `"plaintext"`.
  * `:italic` (default `false`) — emit `font-style: italic` for a style that
    asks for it
  * `:include_highlights` (default `false`) — add `data-highlight="<scope>"`

# `style_to_css`

```elixir
@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` (default `false`) — emit `font-style: italic` for 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;"

# `text_decoration`

```elixir
@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"

# `valid_attr_name?`

```elixir
@spec valid_attr_name?(String.t()) :: boolean()
```

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

# `wrap_line`

```elixir
@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 to `l-line` verbatim, so pass a leading space,
    e.g. `" l-highlighted"`
  * `:style` — a `style` attribute for the line

## Example

    iex> Lumis.Formatter.HTML.wrap_line(2, "code")
    ~s|<span class="l-line" data-line="2">code</span>|

---

*Consult [api-reference.md](api-reference.md) for complete listing*
