# `PhoenixExRatatui.Renderer.Html`
[🔗](https://github.com/mcass19/phoenix_ex_ratatui/blob/v0.2.0/lib/phoenix_ex_ratatui/renderer/html.ex#L1)

Encodes `ExRatatui.CellSession.Diff` payloads for transmission to the
browser via `Phoenix.LiveView.push_event/3`.

LiveView's `push_event/3` only accepts JSON-serializable terms — atoms
encode as strings (via `Jason`), but tuples like `{:rgb, r, g, b}` do
not encode cleanly. This module bridges the gap: convert each
`%CellSession.Cell{}` into a fixed-shape array that the JS hook on
the other side can read positionally without allocating an object
per cell.

## Wire shape

A diff payload becomes a map ready for `push_event/3`:

    %{
      "width" => 80,
      "height" => 24,
      "ops" => [
        [row, col, symbol, fg, bg, modifiers, skip],
        ...
      ]
    }

Where each value follows these encodings:

  * `row`, `col` — integers, zero-indexed
  * `symbol` — UTF-8 string (single character or grapheme cluster)
  * `fg`, `bg` — color encoding:
      - `:reset` → `"reset"`
      - named atoms (`:red`, `:dark_gray`, ...) → matching string (`"red"`)
      - `{:rgb, r, g, b}` → `["rgb", r, g, b]`
      - `{:indexed, n}` → `["indexed", n]`
  * `modifiers` — list of strings in canonical bitflag order
    (`["bold", "italic"]`); empty list when no modifiers are set
  * `skip` — boolean; `true` means "leave whatever was here"

## Why arrays not objects per op

A 200×60 full diff is 12_000 cells. Encoded as objects with named
keys (`{"row": 0, "col": 0, "symbol": " ", "fg": "reset", ...}`) each
cell is roughly 80 bytes; encoded as a 7-element array each cell is
roughly 30 bytes. At websocket frame scale that's the difference
between ~1MB and ~360KB on a single full-paint, before gzip. Since
every browser implements `Array#0` access in a single CPU instruction,
the JS side pays nothing for the positional read.

## Defaults are not omitted

Every cell in the diff carries a full 7-element op even when most of
its fields are at their default values (`"reset"`, `[]`, `false`).
Omitting defaults would shrink the payload further but would push
schema knowledge into the JS hook, and the diff path already filters
cells aggressively — frames that "shouldn't" carry a cell at all
don't appear in `:ops` to begin with. We can revisit if profiling
flags it.

# `encoded_cell`

```elixir
@type encoded_cell() :: [
  non_neg_integer() | String.t() | encoded_color() | [String.t()] | boolean(),
  ...
]
```

JSON-friendly encoded cell: a 7-element list in `[row, col, symbol,
fg, bg, modifiers, skip]` order.

# `encoded_color`

```elixir
@type encoded_color() :: String.t() | [String.t() | non_neg_integer(), ...]
```

JSON-friendly encoded color: a string for named/reset colors, or a
tagged 4- or 2-element array for RGB and indexed colors.

# `encoded_diff`

```elixir
@type encoded_diff() :: %{
  required(String.t()) =&gt; non_neg_integer() | [encoded_cell()]
}
```

Full diff payload as it appears on the LiveView socket. String map
keys (not atoms) so it round-trips cleanly through `Jason`.

# `encode_cell`

```elixir
@spec encode_cell(ExRatatui.CellSession.Cell.t()) :: encoded_cell()
```

Encodes a single cell into the 7-element list shape. Exposed for
callers that need finer-grained control (e.g. encoding cells from
a `Snapshot` rather than a `Diff`, or building a diff op by hand).

## Examples

    iex> alias ExRatatui.CellSession.Cell
    iex> cell = %Cell{row: 3, col: 7, symbol: "A", fg: :green, bg: :reset, modifiers: [:bold], skip: false}
    iex> PhoenixExRatatui.Renderer.Html.encode_cell(cell)
    [3, 7, "A", "green", "reset", ["bold"], false]

# `encode_color`

```elixir
@spec encode_color(ExRatatui.Style.color()) :: encoded_color()
```

Encodes a single color value. Named atoms become strings, RGB and
indexed colors become tagged arrays.

## Examples

    iex> PhoenixExRatatui.Renderer.Html.encode_color(:reset)
    "reset"

    iex> PhoenixExRatatui.Renderer.Html.encode_color(:light_cyan)
    "light_cyan"

    iex> PhoenixExRatatui.Renderer.Html.encode_color({:rgb, 200, 100, 50})
    ["rgb", 200, 100, 50]

    iex> PhoenixExRatatui.Renderer.Html.encode_color({:indexed, 42})
    ["indexed", 42]

# `encode_diff`

```elixir
@spec encode_diff(ExRatatui.CellSession.Diff.t()) :: encoded_diff()
```

Encodes an `ExRatatui.CellSession.Diff` into the JSON-friendly map
shape `Phoenix.LiveView.push_event/3` ships to the client.

See the moduledoc for the full wire shape.

## Examples

    iex> alias ExRatatui.CellSession.{Cell, Diff}
    iex> diff = %Diff{
    ...>   width: 2, height: 1,
    ...>   ops: [%Cell{row: 0, col: 0, symbol: "X", fg: :red, bg: :reset, modifiers: [:bold], skip: false}]
    ...> }
    iex> PhoenixExRatatui.Renderer.Html.encode_diff(diff)
    %{
      "width" => 2,
      "height" => 1,
      "ops" => [[0, 0, "X", "red", "reset", ["bold"], false]]
    }

# `encode_modifiers`

```elixir
@spec encode_modifiers([ExRatatui.Style.modifier()]) :: [String.t()]
```

Encodes a modifier list. Each atom becomes its string name; the list
preserves canonical bitflag order (set by `ExRatatui.CellSession`'s
encoder).

## Examples

    iex> PhoenixExRatatui.Renderer.Html.encode_modifiers([])
    []

    iex> PhoenixExRatatui.Renderer.Html.encode_modifiers([:bold, :italic])
    ["bold", "italic"]

---

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