# `Raxol.Core.Boundary.TermText`
[🔗](https://github.com/DROOdotFOO/raxol/blob/v2.8.0/lib/raxol/core/boundary/term_text.ex#L1)

Terminal-injection confinement: neutralize ESC/ANSI/control bytes in
untrusted text *before* it reaches the terminal renderer.

One of the three centralized boundary confinements (PR #569 thread 2); the
others are `Raxol.Core.Boundary.Path` (path-traversal) and
`Raxol.Core.Boundary.Evaluation` (code-evaluation exposure). This one has
byte-stream semantics — a total function over binaries, no filesystem, never
errors, never passes a dangerous byte through. It enforces the repo rule
"never embed raw ANSI codes in strings passed to `text()`" *at the untrusted
boundary* instead of trusting upstream.

## What `sanitize/2` strips

In a single left-to-right pass:

  * **ESC (`0x1B`) and every sequence it introduces** — CSI (`ESC [` … final
    `0x40..0x7E`), OSC (`ESC ]` … `BEL`/`ST`; kills title-set and OSC-8
    hyperlink smuggling), DCS/APC/PM/SOS (`ESC P`/`ESC _`/`ESC ^`/`ESC X` …
    `ST`), two-byte `ESC <c>` forms, and a truncated trailing `ESC`.
  * **C0 controls (`0x00..0x1F`)** except the `:allow` list (default `[?\n]`).
  * **DEL (`0x7F`)** and **raw C1 controls (`0x80..0x9F`)**.
  * **Format characters that misrepresent a line** — the bidi
    embedding/override block and isolates (`U+202A..U+202E`,
    `U+2066..U+2069`), which reorder rendered text (Trojan Source,
    CWE-451) so `"disabled in a hosted session"` can be made to read as
    its opposite with nothing an ESC filter would catch; `U+200B`,
    `U+200E`, `U+200F`, `U+00AD`, `U+2028`, `U+2029` and `U+FEFF`, which
    hide or re-break the content a reader is asked to trust; and the tag
    characters `U+E0000..U+E007F`, which carry an invisible ASCII payload
    inside ordinary-looking text. ZWJ (`U+200D`) and ZWNJ (`U+200C`) are
    deliberately KEPT: they join emoji sequences, carry meaning in Indic
    and Perso-Arabic scripts, and reorder nothing.
  * **Invalid UTF-8 bytes** → replaced with `U+FFFD` (never emit a broken
    sequence downstream). Raw C1 bytes are stripped (not replaced); other
    invalid bytes become `U+FFFD`.

Valid printable text (including CJK, emoji, and other multi-byte UTF-8) passes
through unchanged. The function never raises and its output contains no `ESC`
and no disallowed control byte for ANY input binary.

## Where confinement happens (authoritative)

Untrusted content is confined at the OUTPUT SINKS, not by walking and
rebuilding a component tree on every frame. Component modules keep their
view data ordinary and do no terminal confinement of their own; they point
here instead of restating this list. The sinks are:

  * **Cell writes** — `Raxol.Core.Runtime.Rendering.Backends.sanitize_char/1`
    delegates to `sanitize_cell/1` as a cell enters the screen buffer.
  * **Terminal emitters** — `Raxol.Terminal.Renderer` and
    `Raxol.Core.Renderer` pass every cell through `sanitize_cell/1` and
    every OSC 8 URL through `sanitize_url/1`, immediately before the bytes
    are assembled.
  * **Layout link propagation** — `Raxol.UI.Layout.Engine` confines a
    `:link` attribute with `sanitize_url/1` as it propagates.
  * **Append/paint authority** — `Raxol.Harness.Surface.ViewText`
    sanitizes per line while flattening the view tree, over this module's
    `strip_codepoint?/2` deny set.

A cell sink and a text sink differ in exactly ONE respect: a cell owns a
column, so a disallowed cell is BLANKED (`sanitize_cell/1`), while
disallowed text is DELETED (`sanitize/2`). Every sink shares one deny set,
`strip_codepoint?/2`.

# `control_byte`

```elixir
@type control_byte() :: 0..31
```

A C0/allowed control byte value, e.g. `?\t` or `?\n`.

# `sanitize`

```elixir
@spec sanitize(binary(), keyword()) :: String.t()
```

Sanitize `binary` for safe delivery to a terminal renderer.

Total: always returns a `String.t()`, never raises.

## Options

  * `:allow` — a list of C0 control byte values (`0..31`) to pass through.
    Defaults to `[?\n]`. `ESC` (`0x1B`) is never allowed regardless of this
    list.

# `sanitize_cell`

```elixir
@spec sanitize_cell(term()) :: String.t()
```

Confine one screen CELL's text.

A cell is POSITIONAL: it owns a column. Deleting it (what `sanitize/2` does
to text) pulls every cell to its right one column left, so box borders,
table columns and the approval line come apart around a single hostile
byte. A disallowed cell is therefore BLANKED to `" "`, never deleted, and
an invalid UTF-8 byte becomes the one-column `U+FFFD` the text path already
substitutes.

A non-binary cell fails closed as `""`, and an empty cell stays empty:
neither occupies a column to preserve.

# `sanitize_url`

```elixir
@spec sanitize_url(term()) :: String.t()
```

Confine a URL for OSC 8 emission: `sanitize/2` with `allow: []`, then a
scheme allowlist (`http`, `https`, `mailto`).

Returns `""` for a non-binary, for a URL whose bytes all sanitize away, and
for any other scheme — including a URL with no scheme at all, which a
terminal resolves against a base this process does not control. Callers
read `""` as "no link": the label text still renders, it is simply not
clickable.

# `strip_codepoint?`

```elixir
@spec strip_codepoint?(integer(), [control_byte()]) :: boolean()
```

The deny set, as a predicate over one codepoint.

`true` when `codepoint` must never reach a terminal: a C0 control outside
`allow`, `DEL`, a C1 control, or one of the format characters listed in the
moduledoc. Public so that every sink — text, cell, and the
`Raxol.Harness.Surface.ViewText` line scanner — decides with the SAME set
rather than keeping a second list that drifts from this one.

---

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