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

One node of an editable template — **one shape for everything the editor can address**.

This is the model the React editor never had. It carried two incompatible record shapes in one
array (a palette-dragged element with `component`/`renderedHtml` and no `tag`, and a loaded element
with `tag`/`attrs` and no `component`), so every consumer had to branch and the save serializer
only understood one of them. Here a `<div>`, a `<.card>`, a `:slot` entry, a text run and an
`<%= for %>` block are all the same struct, distinguished by `kind`.

## Fields

  * `id` — `"n"` plus eight lowercase base32 characters. Minted at parse, stable across moves, and
    what every `phx-value-id` in the canvas carries. It is **not** persisted: serializing drops it,
    and re-parsing mints fresh ones (see `MishkaCmsCore.Runtime.PageBuilder.Tree` on identity).
  * `kind` — one of [:tag, :component, :slot, :text, :eex, :eex_block, :eex_comment, :html_comment].
  * `tag` — `"div"`, `".card"`, `"MyApp.Components.hero"`, `":item"`; `nil` for text and comments.
  * `attrs` — an **ordered** list of `{name, value}`. `name` is a binary, or `:root` for a
    `{@rest}`-style root attribute. `value` is `{:string, binary}`, `{:expr, binary}` or
    `:boolean` (a bare attribute such as `disabled`). Preserving both the order and the
    string/expression distinction is what lets a template round-trip; the old JSON encoding
    flattened `{:expr, v}` to the string `"{v}"` and guessed it back with a `binary_part` check.
  * `children` — child **ids**, not structs. The tree is flat and id-keyed.
  * `parent_id` — `nil` for a root node.
  * `text` — the content of `:text`, `:eex`, `:eex_comment` and `:html_comment` nodes, and the
    block argument (`"for post <- @posts"`) used as an `:eex_block` chip's label.
  * `meta` — parser and editor metadata: `:mode` (`:normal` | `:preserve` for `pre`/`textarea`/
    `phx-no-format`), `:form` (`:body` for `{expr}` versus `:eex` for `<%= expr %>`), `:opt` (the
    EEx marker), `:source` (an `:eex_block`'s verbatim source — see ADR-9) and `:styles` (the
    responsive/pseudo cascade the inspector records).

## Construction

Internal callers — the parser and the combinators — build the struct literally, because a 2,000
node page would otherwise pay validation on every keystroke. `builder/1` is the boundary check and
is required for anything arriving from outside: a client-sent op, a plugin-contributed subtree, a
pasted clipboard payload, an inserted example.

See also `MishkaCmsCore.Runtime.PageBuilder.Tree`, `MishkaCmsCore.Runtime.PageBuilder.Tree.Serializer`
and `MishkaCmsCore.Runtime.PageBuilder.Containment`.

# `t`

```elixir
@type t() :: %MishkaCmsCore.Runtime.PageBuilder.Tree.Node{
  attrs: list() | nil,
  children: list() | nil,
  id: String.t(),
  kind: atom(),
  meta: map() | nil,
  parent_id: String.t() | nil,
  tag: String.t() | nil,
  text: String.t() | nil
}
```

# `attr`

```elixir
@spec attr(t(), String.t()) ::
  {:string, String.t()} | {:expr, String.t()} | :boolean | nil
```

Reads one attribute's value, or `nil` when the node does not carry it. Bare attributes read as
`:boolean`, so an absent attribute and a valueless one stay distinguishable.

# `attr_text`

```elixir
@spec attr_text(t(), String.t()) :: String.t() | nil
```

An attribute's plain text value — the literal for a `{:string, _}`, the expression source for an
`{:expr, _}`, `""` for a bare attribute and `nil` when unset. This is what property widgets and
audit checks read; use `attr/2` when the string/expression distinction matters.

# `builder`

# `comment?`

```elixir
@spec comment?(t()) :: boolean()
```

Whether this node is a comment, in either syntax.

# `comment_kinds`

```elixir
@spec comment_kinds() :: [atom()]
```

The kinds that are a note to the author rather than anything the published page renders.

# `component_name`

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

The name a `:component` node resolves to at render time.

Runtime components are written as `<.component component_name="chelekom-card" site="Global">`,
because a database-defined component's name is data (and often hyphenated, which is not a valid
function-call tag). That wrapper form is the one the compilers emit and the one every stored
template uses, so its `component_name` attribute — not the literal tag — is the answer here.
A plain `<.card>` still resolves to `"card"`, and anything else gives `nil`.

# `container?`

```elixir
@spec container?(t()) :: boolean()
```

Whether this node may hold children at all.

# `container_kinds`

```elixir
@spec container_kinds() :: [atom()]
```

The kinds that may hold children. Text, both comment kinds and `:eex` are leaves; `:eex_block` is
a leaf too, deliberately — ADR-9 makes it atomic so its source survives byte-for-byte.

# `document?`

```elixir
@spec document?(t() | nil) :: boolean()
```

Whether this node is one of the document's own elements — `<html>`, `<head>`, `<body>`.

# `document_tags`

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

The tags that are the document itself rather than content in it.

# `enforce_keys`

# `enforce_keys`

# `escape_attr`

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

An attribute value as it has to be written between double quotes.

A node holds what the author typed. Written back as source, a double quote in it closes the
attribute early and the template stops parsing — and because the editor's save path is
tree, serialize, re-parse, that does not surface as a rejected keystroke. The op is recorded, the
history entry is written, the re-parse fails, and the draft simply stops being saved while the
editor goes on saying it saved. A regex in a `pattern`, an apostrophe-and-quote in a success
message: ordinary things to type.

Pairs with `unescape_attr/1`, and the pair is stable — a value survives any number of round trips.

# `example`

A sample %MishkaCmsCore.Runtime.PageBuilder.Tree.Node{} populated from defaults + type-based fallbacks.

# `generate_id`

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

A fresh node id: `"n"` + eight lowercase base32 characters, which is exactly five random bytes.
The alphabet (`a-z2-7`) is a subset of the `id` derive's `[a-z0-9]`, so a generated id always
passes `builder/1`.

# `hidden?`

```elixir
@spec hidden?(t(), MapSet.t()) :: boolean()
```

Whether this node is folded away on the canvas, given the session's eye set.

The set holds the ids whose eye has been FLIPPED from the default for their kind, not the ids that
are hidden — because a comment starts folded and everything else starts drawn. A comment renders
nothing on the published page, so the chip standing in for one is scaffolding: worth having while
you are hunting for a note, noise every other minute of the day.

Folding is a view preference and nothing else. `MishkaCmsCore.Runtime.PageBuilder.Tree.Serializer`
never reads it, so a note nobody can see is still a note the save writes back out.

# `keys`

# `keys`

# `kinds`

```elixir
@spec kinds() :: [atom()]
```

Every kind a node may declare.

# `label`

```elixir
@spec label(map()) :: String.t()
```

What to call this node in front of a reader — `"h1"`, `"text"`, a component's name.

Here rather than in the panel because it is not only a label any more: a review note captures it
at write time, and `MishkaCmsCore.Runtime.PageBuilder.Review` compares it against the element a
stored address resolves to. A positional anchor with nothing to check it against selects whatever
slid into that slot after an edit; this is the check.

# `literal`

```elixir
@spec literal(t(), String.t()) :: String.t() | nil
```

A word the author actually wrote in this attribute: the literal of a `{:string, _}`, trimmed, and
`nil` when the attribute is unset, holds an expression, or holds nothing but whitespace.

`attr_text/2` is the other question and answers an expression's source too, which is what a
property widget wants. Everything that MINES an author's own word out of markup wants this one:
a palette card is seeded `name=""` so it renders standing alone, and reading that blank as a word
is how a wrapped field came out `f[:field]` inside `form-form`; `name={@input_name}` is source, not
a name. It was written out four times — once each in
`MishkaCmsCore.Runtime.PageBuilder.PageForms`, its `FormChecks` and its `FormPlacement`, and again
as the value test in `MishkaCmsCore.Runtime.PageBuilder.Fields` — and one of the four trimmed
differently.

# `new`

```elixir
@spec new(atom(), keyword()) :: t()
```

Builds a node without validation — the internal constructor used by the parser and the
combinators. Anything crossing a trust boundary goes through `builder/1` instead.

# `slot_name`

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

The slot name a `:slot` node fills — `":item"` gives `"item"`. Returns `nil` for any other kind.

# `unescape_attr`

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

An attribute value read back from source, as the author typed it.

Only the double quote is decoded, because it is the only character `escape_attr/1` encodes. Every
other entity in a template is the author's own text and is left exactly as they wrote it.

---

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