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 everyphx-value-idin the canvas carries. It is not persisted: serializing drops it, and re-parsing mints fresh ones (seeMishkaCmsCore.Runtime.PageBuilder.Treeon identity).kind— one of [:tag, :component, :slot, :text, :eex, :eex_block, :eex_comment, :html_comment].tag—"div",".card","MyApp.Components.hero",":item";nilfor text and comments.attrs— an ordered list of{name, value}.nameis a binary, or:rootfor a{@rest}-style root attribute.valueis{:string, binary},{:expr, binary}or:boolean(a bare attribute such asdisabled). 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 abinary_partcheck.children— child ids, not structs. The tree is flat and id-keyed.parent_id—nilfor a root node.text— the content of:text,:eex,:eex_commentand:html_commentnodes, and the block argument ("for post <- @posts") used as an:eex_blockchip's label.meta— parser and editor metadata::mode(:normal|:preserveforpre/textarea/phx-no-format),:form(:bodyfor{expr}versus:eexfor<%= 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.
Summary
Functions
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.
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.
Whether this node is a comment, in either syntax.
The kinds that are a note to the author rather than anything the published page renders.
The name a :component node resolves to at render time.
Whether this node may hold children at all.
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.
Whether this node is one of the document's own elements — <html>, <head>, <body>.
The tags that are the document itself rather than content in it.
An attribute value as it has to be written between double quotes.
A sample %MishkaCmsCore.Runtime.PageBuilder.Tree.Node{} populated from defaults + type-based fallbacks.
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.
Whether this node is folded away on the canvas, given the session's eye set.
Every kind a node may declare.
What to call this node in front of a reader — "h1", "text", a component's name.
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.
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.
The slot name a :slot node fills — ":item" gives "item". Returns nil for any other kind.
An attribute value read back from source, as the author typed it.
Types
Functions
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.
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.
Whether this node is a comment, in either syntax.
@spec comment_kinds() :: [atom()]
The kinds that are a note to the author rather than anything the published page renders.
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.
Whether this node may hold children at all.
@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.
Whether this node is one of the document's own elements — <html>, <head>, <body>.
@spec document_tags() :: [String.t()]
The tags that are the document itself rather than content in it.
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.
A sample %MishkaCmsCore.Runtime.PageBuilder.Tree.Node{} populated from defaults + type-based fallbacks.
@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.
@spec kinds() :: [atom()]
Every kind a node may declare.
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.
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.
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.
The slot name a :slot node fills — ":item" gives "item". Returns nil for any other kind.
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.