# `Coelho.Render`
[🔗](https://github.com/nseaSeb/coelho/blob/main/lib/coelho/render.ex#L1)

Turns a validated document into HTML.

Rendering is driven by the `:render` field of each node and mark spec,
which takes one of three forms:

  * `nil` — the node contributes nothing but its children
  * `{tag, attrs}` — an element, where `attrs` is either a static list of
    `{name, value}` pairs or a function of the node returning such a list
  * `fun/2` — full control, receiving the node and its already rendered
    children as iodata
  * `fun/3` — the same, plus the `:context` given to this render call

A spec's `:class` is merged into the `{tag, attrs}` form, after whatever
class the attributes already carry. A render function is not touched: it
builds the whole element itself, so it applies its own class.

Callers can override any of them per call through the `:nodes` and
`:marks` options, which is how a Phoenix application injects its own
markup — mentions, embeds, syntax highlighted code — without changing what
is stored.

The `:context` option carries whatever the render functions need from the
application and cannot know on their own. Attachments use it to turn a
stored key into a URL at render time, which is what lets signed and
expiring URLs work at all — see `Coelho.Attachments`.

## What is escaped

Two things, and between them they are the safety guarantee of the package:

  * the text of every text node, through `escape/1`
  * every attribute value, through `escape/1` before it is quoted

`&`, `<`, `>`, `"` and `'` all become entities. Nothing a writer typed is
ever emitted as markup, because a document holds no markup to begin with —
it holds a tree, and this builds the tags.

What escaping cannot cover is a value that is perfectly quoted and still
dangerous: `javascript:` in an `href` executes however well it is escaped.
`safe_url/1` is for those, and the shipped renderers put every URL through
it.

Two things are *not* escaped, and both are the schema author's to get
right: tag names, and attribute *names*. Both come from the schema, which
is code.

`nil` renders as nothing rather than raising: it is what a nullable column
holds and what both stored types cast an absent document to, so a template
writing `to_safe_html(@post.body)` should not have to guard it.

Only validated documents should be rendered. Rendering does not re-check
the document against the schema; it trusts `Coelho.Document.validate/2`
to have run, and raises on anything it does not recognise. For a document
read back out of storage, where that is not a safe assumption, put it
through `Coelho.Document.sanitize/2` first.

# `callbacks`

```elixir
@type callbacks() :: %{
  :node =&gt; (map(), [term()] -&gt; term()) | (map(), [term()], term() -&gt; term()),
  optional(:text) =&gt;
    (String.t(), [map()] -&gt; term()) | (String.t(), [map()], term() -&gt; term())
}
```

# `opts`

```elixir
@type opts() :: [
  nodes: %{optional(atom()) =&gt; term()},
  marks: %{optional(atom()) =&gt; term()},
  context: term()
]
```

# `attr`

```elixir
@spec attr(map(), String.t(), term()) :: term()
```

Reads an attribute out of a node or a mark, falling back to a default.

An attribute sitting at its schema default is not stored — see
`Coelho.Document.canonical/1` for why — so a renderer must supply the
default rather than read the key and hope. This is the one place that
knows the shape of `"attrs"`.

The default to pass is **the schema's**, not one chosen here: a heading
stored with no `"level"` is a heading at whatever `:level` declares as its
default, and a renderer answering something else prints a document the
editor drew differently. Where the schema is at hand, read it from there
rather than writing the number twice:

    level = Render.attr(node, "level", Schema.node_spec(schema, :heading).attrs.level.default)

# `escape`

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

Escapes text for inclusion in HTML, attribute values included.

Both halves of what an element carries go through this: the text of a text
node, and every attribute value, escaped in `attributes/1` before it is
quoted. `&`, `<`, `>`, `"` and `'` all become entities, which is what makes
an attribute value safe inside either quoting style and text safe outside
a tag.

What escaping does not cover, and what `safe_url/1` exists for, is a value
that is correctly quoted and still dangerous — a `javascript:` URL in an
`href`.

# `reduce`

```elixir
@spec reduce(map(), Coelho.Schema.t(), callbacks(), opts()) :: term()
```

Folds a document into any term at all.

`to_html/3` and `to_iodata/3` answer one question — what does this look
like on a web page — and answer it in iodata, which is the wrong shape for
every other target. An invoice rendered through a typesetter, a search
index, a word count per heading, a summary of the links a document
contains: each of those is a fold over the same tree, and reimplementing
the traversal per target is how a consumer ends up quietly disagreeing
with the schema about what a document may hold.

Two callbacks, and the accumulator is whatever they return:

  * `:node` — `fn node, children -> term end`, where `children` is the
    list of what this node's children folded to, in order
  * `:text` — `fn text, marks -> term end`, where `marks` is the node's
    marks, resolved against the schema, in the schema's declaration order.
    Leave it out and text nodes go through `:node` with no children.

Either callback may take a third argument, which receives the `:context`
option, exactly as the render functions do.

The point of returning a term rather than iodata is that a target with its
own escaping rules — a typesetting language, a template engine — can hand
back a list of maps and let its own encoder do the quoting. Nothing the
writer typed is ever concatenated into a string that something downstream
will interpret as code.

    Coelho.Render.reduce(document, schema, %{
      text: fn text, marks -> %{"text" => text, "marks" => Enum.map(marks, & &1["type"])} end,
      node: fn node, children -> %{"block" => node["type"], "children" => children} end
    })

Like `to_iodata/3`, this trusts the document: an unknown node or mark type
raises rather than being skipped. Fold a validated document, or one that
has been through `Coelho.Document.sanitize/2`.

That strictness is why the library's own walks are not written on top of
this one, which is otherwise the obvious thing to ask. Validation has to
*report* on a node it does not know, sanitisation has to remove it, and
`Coelho.Attachments.keys/2` is handed rows written under whatever schema
was in force years ago — none of the three may raise, and a fold that
cannot raise cannot promise the caller that what it was handed is a
document this schema admits. The two contracts are different on purpose;
this one is for a consumer with a document it has already validated.

# `safe_url`

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

Returns a URL fit to be emitted, or `nil` for one that is not.

Validation already rejects unsafe URLs on the way in, but stored documents
are not re-validated on the way out — `Coelho.Ecto.Type` deliberately trusts
what is in the column. A row written before the schema tightened, by
a direct database write, or under a looser custom schema, would otherwise
put `javascript:` straight into an `href`. Escaping does not help there:
the value is quoted correctly and still executes.

Attributes built with this return `nil` and are dropped, so a suspect link
renders as an `<a>` without an `href` rather than as a live one.

# `tag`

```elixir
@spec tag(String.t(), [{String.t(), term()}], iodata()) :: iolist()
```

Builds an element from a tag, an attribute list and rendered children.

# `to_html`

```elixir
@spec to_html(map(), Coelho.Schema.t(), opts()) :: String.t()
```

Renders a document to an HTML string.

# `to_inline_html`

```elixir
@spec to_inline_html(map(), Coelho.Schema.t(), opts()) :: String.t()
```

Renders a document where only inline elements are legal.

## Why this exists

A `<p>` inside a `<p>` is not nested by the browser, it is **closed** by
it. Put `to_html/3` inside a paragraph or a span — a news banner, the
detail panel of a map marker, a truncated card excerpt — and four things
happen, none of them reported:

  * the enclosing paragraph ends where the document's first one begins, so
    every class on it stops applying from there
  * an empty paragraph appears where the enclosing one was reopened
  * a block element inside a `<span>` breaks the line, whatever the span's
    layout was for
  * and the words of two paragraphs run together, because the tags that
    separated them are gone

The last is the one nobody sees, because it looks like text.

## What it guarantees

**The output holds nothing that is illegal in an inline context.** That is
the whole contract, and everything else follows from it without a judgement
call:

  * marks stay — `<strong>`, `<em>`, `<code>`, `<a>` are inline
  * `<img>` and `<br>` stay
  * every block is unwrapped to its children: a heading contributes its
    words, a list its items, a code block its text. A block that had a tag
    loses the tag, which is what "inline" means
  * a node whose inline form is not simply its children says so, with
    `:render_inline` in its spec — an attachment renders its image and its
    caption's text rather than the `<figure>` it is on a page of its own
  * a node with no inline equivalent at all contributes nothing. A
    horizontal rule is the example, and it is mechanical rather than a
    policy

Empty contributions are dropped rather than separated, which is not a mode:
it is what joining correctly means.

## The separator is yours

Only the caller knows whether its container can take a line break. A map
bubble wants `:br`; a fixed-height banner wants `:space`, and so does a
truncated excerpt.

    Coelho.Render.to_inline_html(document, schema, separator: :br)

`:space` is the default because the two mistakes do not cost the same. A
space where a break was wanted puts two sentences on one line, which reads.
A break where a space was wanted grows the caller's box and breaks their
layout.

A separator of your own is escaped, because a value that reached this from
data would otherwise be markup. Pass `{:safe, iodata}` to say it is not.

It governs the boundaries between blocks and nothing else. A hard break the
writer typed is content, and still renders as `<br>` under `:space` — it is
not a seam this put there, and dropping it would be dropping something
someone wrote.

# `to_inline_iodata`

```elixir
@spec to_inline_iodata(map(), Coelho.Schema.t(), opts()) :: iodata()
```

`to_inline_html/3` as iodata.

# `to_iodata`

```elixir
@spec to_iodata(map(), Coelho.Schema.t(), opts()) :: iodata()
```

Renders a document to iodata.

# `to_safe_html`

```elixir
@spec to_safe_html(map(), Coelho.Schema.t(), opts()) :: {:safe, iodata()}
```

Renders a validated document as `{:safe, iodata}`.

What `to_html/3` renders, in the shape a template will not escape again.
`to_html/3` answers a `String.t()`, which HEEx and `Phoenix.HTML` treat as
text — correctly, since they cannot know it is markup — so the caller has
to remember `raw/1`, and has two ways to get it wrong: forget it, and the
document is shown as its own source; reach for it somewhere else, and
something that should have been escaped no longer is.

For a package whose argument is that rendering is safe by construction,
that is the last link left to the caller. This closes it:

    <div class="prose">{Coelho.to_safe_html(@post.body)}</div>

The `{:safe, iodata}` pair is what `Phoenix.HTML.Engine` unwraps directly,
so this costs no dependency — Coelho has none for HTML, and does not
acquire one here. There is no `Phoenix.HTML.Safe` implementation to go with
it because there is nothing to implement it *for*: a document is a bare
map, deliberately, and an implementation for `Map` would apply to every map
in the application.

It renders the same iodata `to_iodata/3` builds, so the escaping guarantees
above hold unchanged.

# `to_safe_inline_html`

```elixir
@spec to_safe_inline_html(map(), Coelho.Schema.t(), opts()) :: {:safe, iodata()}
```

`to_inline_html/3` in the shape a template will not escape again.

The same reasoning as `to_safe_html/3`: having removed the need to remember
`raw/1` in one place, this does not reintroduce it in the other.

# `void_tag`

```elixir
@spec void_tag(String.t(), [{String.t(), term()}]) :: iolist()
```

Builds a childless element, self-closing only if HTML says it is.

---

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