# `MishkaCmsCore.Runtime.PageBuilder.Containment`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/page_builder/containment.ex#L5)

The one authority on "may this node go inside that one" — the HTML content model plus the
component/slot rules, in Elixir, on the server.

The React editor spread this over four competing lists in three files and only ever consulted them
in the browser, so a tampered payload (or the structure tree, which used a different list than the
canvas) could build markup no parser would accept. Here the client gets a **hint** — an `accepts`
map precomputed per node for instant drag feedback — and the server re-checks every insert and
every move before it mutates anything. Client hints, server decides (ADR-6).

## The rules

  * **Void elements** (`br`, `img`, `input`, …) take no children at all.
  * **Phrasing-only elements** (`span`, `a`, `p`, `h1`–`h6`, `button`, `pre`, …) take text and
    other phrasing content, never a block element. This is deliberately stricter than the spec —
    `<p>` may legally hold some things this rejects — because the stricter rule is the one that
    matches what an author means to build. Ported as-is from `containment.js`.
  * **Structural pairs** the spec fixes exactly: a `table` section takes only `tr`, a `tr` takes
    only `td`/`th`, a list takes only `li`, a `dl` takes `dt`/`dd`/`div`, a `select` takes
    `option`/`optgroup`.
  * **Anchors** may not contain another interactive element — nested interactive content is
    invalid and unusable with a keyboard.
  * **Components and slots** accept children when their metadata declares a slot (or when they
    already hold content, which means a previous author put something there legitimately). A
    dotted tag the kit catalog has never heard of is asked of the compiled module instead, so the
    CMS's own `<.form>` and `<.input>` are containers here the same way they are on the page.
  * **A form may not go inside a form** — the one ancestor rule in this module, and the only
    nesting HTML both forbids and fails at silently. What counts as a form is
    `MishkaCmsCore.Runtime.PageBuilder.Fields.form_node?/2`, the same answer the insert funnel and
    the audit rail take, so a kit's own form wrapper is one here too. The thing being dragged is
    asked about with everything under it, because a Block opens with a band tag and carries its
    form as a child.

Every refusal carries a sentence written for the person dragging, which the canvas shows as the
demo's danger toast near the drop point.

A field that lands with no form around it is not refused: see
`MishkaCmsCore.Runtime.PageBuilder.PageForms.FormPlacement`, which wraps it instead. Building a
form top-down is an ordinary gesture, so only the nesting HTML itself forbids is an error here.

# `accepts_children?`

```elixir
@spec accepts_children?(String.t() | nil) :: boolean()
```

Whether a bare HTML tag can hold children **at all** — false only for void elements.

This is deliberately narrower than the React `canAcceptChild` it replaces. That one also answered
false for every phrasing-only tag, and its single caller used the answer as "accepts any child",
so the editor refused to drop even a word of text into a `<span>` or an `<a>`. Whether a *block*
child is allowed is a separate question, asked per-pair by `can_drop?/4`.

Components and slots answer `true`; whether they really have somewhere to put a child depends on
their slot metadata, which `can_drop?/4` checks.

# `accepts_map`

```elixir
@spec accepts_map(
  MishkaCmsCore.Runtime.PageBuilder.Tree.t(),
  MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | map(),
  keyword()
) :: %{required(String.t()) =&gt; boolean()}
```

Precomputes, for every node in the tree, whether `dragged` may land inside it — the hint map the
drag hook uses to paint drop targets without a round trip per hover. The server still re-checks
the drop it is actually asked to perform.

Reached through `MishkaCmsCore.Runtime.PageBuilder.EditorSession.drop_hints/2`, which is what the
editor calls at `dragstart` and which also folds in the session's locks. A refusal here is not the
end of the gesture: `EditorSession.apply_drop/2` places the node beside the target instead.

# `can_drop?`

```elixir
@spec can_drop?(
  MishkaCmsCore.Runtime.PageBuilder.Tree.t(),
  MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | map(),
  MishkaCmsCore.Runtime.PageBuilder.Tree.parent(),
  keyword()
) :: :ok | {:error, String.t()}
```

Whether `dragged` may be dropped into `target`.

`dragged` is a `Tree.Node`, or a palette entry — any map carrying `:tag` (what it will become) and
optionally `:slots`. `target` is a node id or `:root`.

Options:

  * `:components` — the palette metadata list (maps with `name` and `slots`), used to decide
    whether a component declares somewhere to put a child and what part it plays in a form.
  * `:dragged_tree` — the tree `dragged` belongs to, which for an insert is the parsed template
    rather than the document. Without it a Block's own `<.form>` is invisible here, because a
    block opens with a band tag and only its head node arrives.
  * `:roles` — the memo `MishkaCmsCore.Runtime.PageBuilder.Fields.roles/2` builds, computed once
    per drag by `accepts_map/3` and rebuilt here when a caller asks about a single drop.

Returns `:ok` or `{:error, message}` with a sentence meant for the person dragging.

# `can_hold?`

```elixir
@spec can_hold?(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | map(), String.t()) ::
  :ok | {:error, String.t()}
```

Whether `child` may sit inside a tag that does not exist yet.

A wrap mints its wrapper on the spot, so there is no node to point `can_drop?/4` at and no place in
the tree to ask about — but the pairing still has to hold: a `<span>` will not take a `<section>`,
and an `<img>` will not take anything at all. Only the rules that are a property of the PAIR are
asked here; the ones that depend on where the wrapper lands (cycles, form nesting, an interactive
ancestor) are asked of the wrapper's destination instead, which is a real position in a real tree.

Returns `:ok` or `{:error, message}`, in the same words a refused drop uses.

# `holds_children?`

```elixir
@spec holds_children?(
  MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | term(),
  keyword()
) :: boolean()
```

Whether this node may hold children at all, asked without a node being dragged.

The two rules `can_drop?/4` applies to the TARGET alone — what HTML lets an element contain, and a
component needing somewhere to put content — and nothing about what is arriving. The layers rail
has to answer this while it renders a row, long before anything has been picked up, and answering
it a second way is how the rail came to refuse drops the server allows: `drop_target?/2` required a
palette row, so the `<.form>` this editor wraps every loose field in — a component Phoenix declares
a slot for, and one no kit ships — was drawn as a row nothing could be dropped inside, and a field
aimed at the middle of it landed beside it instead.

A guess, not a verdict: it knows nothing about the node being moved, so `can_drop?/4` still decides.

# `parent_restricted?`

```elixir
@spec parent_restricted?(String.t() | nil) :: boolean()
```

Whether a tag may only appear under a specific parent — see `parent_restricted_tags/0`.

# `parent_restricted_tags`

```elixir
@spec parent_restricted_tags() :: [String.t()]
```

Tags the content model only permits under a specific parent — `<td>` outside a `<tr>` is not a
cell, it is foster-parented out of the table by the parser.

The page builder asks this before it wraps a node in its own element: an editor wrapper around a
`<td>` lands where only a `<td>` may go, so the parser moves it out and the wrapper becomes an
orphan sitting above the table, taking the node's markers with it.

# `phrasing_only?`

```elixir
@spec phrasing_only?(String.t() | nil) :: boolean()
```

Whether a tag holds phrasing content only, so no block element may be dropped into it.

# `phrasing_only_tags`

```elixir
@spec phrasing_only_tags() :: [String.t()]
```

Tags that hold phrasing content only — no block child may be dropped into them.

# `void_tags`

```elixir
@spec void_tags() :: [String.t()]
```

Tags that can never hold children.

---

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