# `Kotoba.Content`
[🔗](https://github.com/hattoriai/kotoba/blob/v0.5.0/lib/kotoba/content.ex#L1)

An `Ecto.Type` that stores a rich text document with a cached rendering.

A `Kotoba.Content` struct has:

  * `:doc` - the document envelope, as a map with string keys (see
    `Kotoba.Document`).
  * `:html` - the safe HTML of the document, rendered with the
    `:default` policy, as a string.
  * `:text` - the plain text of the document, as a string.
  * `:version` - the content cache version, currently `1`.

`cast/1` reads a JSON string (as a form posts it through the hidden
input), a document envelope, a bare Lexical root node (wrapped in an
envelope), a `Kotoba.Content` struct, `nil` or `""` (both cast to
`empty/0`). It parses the input through `Kotoba.Document.parse/2`, with
the node registry from the application configuration, and renders
`:html` and `:text` right away, so a row always carries a fresh cache.
Invalid input casts to `:error`, which `Ecto.Changeset` turns into an
`"is invalid"` error.

`dump/1` and `load/1` store and read `%{"doc", "html", "text",
"version"}`. When a loaded map has no `"html"` or no `"text"` (an older
row, or one written outside Kotoba), `load/1` re-renders them from
`"doc"`. When `"doc"` itself no longer parses (for example a custom
node lost a field it once allowed), `load/1` still succeeds, with an
empty cache, rather than failing the whole query; a row where `"doc"`
is not even a map is refused.

`equal?/2` compares the `:doc` field of two `Kotoba.Content` structs,
so an `Ecto.Changeset` does not mark the field changed when only the
cache was recomputed; it falls back to `==` for anything else, so `nil`
equals `nil`.

Use `rerender/2` after a node registry or a render policy changes, to
bring the cache of a stored document up to date. Use `from_markdown/2`
to build a `Kotoba.Content` from a Markdown string, on a best-effort
basis.

## Forms and changesets

The Kotoba editor hook always posts a JSON document through the hidden
input, on every change: an empty editor posts the empty envelope (the
`doc` of `empty/0`), never `""` and never no param at all. So in the
ordinary form-submit path, `cast/1` only ever sees a JSON string, and
the `nil`/`""` handling above does not come into play.

`Ecto.Changeset.cast/4` has its own, separate handling of `""`: by
default, it treats a param of `""` (or all white space) as absent and
keeps the field's current value (`nil`, unless the schema gives it a
default) instead of calling this type's `cast/1` at all. A blank param
never reaches `empty/0` this way. A host that wants `""` to cast to
`empty/0` — a manual API call, say, rather than the editor — opts in
with `empty_values: []` on that field's own `cast/4` call. This turns
off the `""` → current-value behaviour for every field named in that
same `cast/4` call, not only `:body`, so give the field its own
`cast/4` call when the form has other string fields that should keep
the default. A host can instead give the field a starting document with
`field :body, Kotoba.Content, default: Kotoba.Content.empty()`.

## Examples

    iex> content = Kotoba.Content.empty()
    iex> content.html
    ""
    iex> content.text
    ""

# `t`

```elixir
@type t() :: %Kotoba.Content{
  doc: map(),
  html: String.t(),
  text: String.t(),
  version: pos_integer()
}
```

# `cache_version`

```elixir
@spec cache_version() :: pos_integer()
```

Returns the current content cache version.

A `Kotoba.Content` with another `:version` has a cache made by another
version of Kotoba; `Kotoba.Components.kotoba_content/1` renders such a
document again instead of using its `:html`.

# `cast`

Casts a JSON string, a document envelope, a bare Lexical root node, a
`Kotoba.Content` struct, `nil` or `""` to a `Kotoba.Content`.

Renders `:html` and `:text` at cast time, also for a `Kotoba.Content`
struct: its `:doc` is parsed again, and its `:html` and `:text` are not
kept. Returns `:error` when the
input is not valid JSON, is not an object, or does not parse as a
`Kotoba.Document` (see `Kotoba.Document.parse/2`).

## Examples

    iex> {:ok, content} = Kotoba.Content.cast(%{"type" => "root", "children" => []})
    iex> content.text
    ""

    iex> Kotoba.Content.cast("not json")
    :error

# `empty`

```elixir
@spec empty() :: t()
```

Returns an empty `Kotoba.Content`, for a new form or a blank field.

# `from_markdown`

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

Builds a `Kotoba.Content` from a Markdown string.

MDEx (comrak, a port of GitHub's cmark-gfm) reads the Markdown:
CommonMark and GitHub Flavored Markdown, with tables, task lists,
strikethrough, autolinks and footnotes, and `__underline__` and
`==highlight==`, as `Kotoba.Renderer.to_markdown/2` writes them (bold is
`**`). Each construct becomes its
node: quotes hold blocks, an image is a `Kotoba.Nodes.Image`, and a
paragraph of `&nbsp;` is a blank line (see `Kotoba.Nodes.Paragraph`), so
the Markdown of `Kotoba.Renderer.to_markdown/2` reads back to the same
document.

Raw HTML is text: Kotoba never passes HTML through. A list item holds
inline content and one nested list; the paragraphs of an item with
several are joined by line breaks, and a code block, a quote, a table or
a heading in an item comes after the list. When the built document
somehow fails to parse, returns `empty/0`.

## Options

  * `:features` - the features of the editor (see `Kotoba.Features`),
    every feature by default. A construct whose feature is off becomes
    its closest form, with no text lost: a heading a paragraph, a quote
    its blocks, a list its items, a table a line for each row, an image
    its alternative text, a footnote its `[^label]` text, and a format is
    dropped.
  * `:attachments` - a map of names to attachment nodes
    (`Kotoba.Nodes.Attachment`, as `Kotoba.Attachments.store/2` returns
    them). A paragraph of only `![alt](attachment:name)` images becomes
    the attachment of that name, or a gallery of several, as the editor
    makes them for a file someone drops in. The image's alternative
    text is the attachment's `caption` (its `alt` and `figcaption` in
    HTML), unless it is empty or the file name; a `caption` the node has
    already wins. A name that is not in the map raises `ArgumentError`.
  * `:nodes` and `:policy` - as in `rerender/2`.

## Examples

    iex> content = Kotoba.Content.from_markdown("# Title\n\nHello **world**.")
    iex> content.text
    "Title\nHello world."

# `rerender`

```elixir
@spec rerender(t(), keyword()) :: t()
```

Re-renders `:html` and `:text` from `:doc`, with a node registry and a
render policy.

When `:doc` no longer parses (for example a required field that a
removed custom node used to allow), the content is returned unchanged.

## Options

  * `:nodes` - extra node modules for the registry, as in
    `Kotoba.Document.parse/2`.
  * `:policy` - the render policy for `:html`. The default is `:default`.

# `to_doc`

```elixir
@spec to_doc(term()) :: {:ok, map()} | :error
```

Returns the document envelope of anything that `cast/1` accepts, with the
same checks, but without rendering `:html` and `:text`.

`Kotoba.Components.kotoba/1` uses it for the hidden input on every
render.

## Examples

    iex> {:ok, doc} = Kotoba.Content.to_doc(~s({"type": "root", "children": []}))
    iex> doc["kotoba"]
    1

    iex> Kotoba.Content.to_doc(42)
    :error

# `validate_features`

```elixir
@spec validate_features(Ecto.Changeset.t(), atom(), [atom() | String.t()]) ::
  Ecto.Changeset.t()
```

Validates that the `Kotoba.Content` of a changeset field uses only the
given features (see `Kotoba.Features`), the ones of the field's editor.
The editor keeps out what it does not have, but a request can post any
document; this refuses one with a table in a comment field, say.

    changeset
    |> cast(params, [:body])
    |> Kotoba.Content.validate_features(:body, ~w(bold italic links lists)a)

The error is `"has content that is not allowed: %{names}"`, with
`names: "tables"` (the features, joined with ", ") and
`validation: :features` in its keys. A field with no content, or with a
document that no longer parses, passes. The features are checked with
`Kotoba.Features.names!/1`, which raises for an unknown name.

---

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