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

One seed per form, and the five names derived from it.

| derived      | from seed `contact`            | with token `k7m2q`       |
|--------------|--------------------------------|--------------------------|
| DOM id       | `contact-form`                 | `contact-form`           |
| param name   | `as={:contact}`                | `as={:contact}`          |
| page assign  | `for={assigns[:contact_form]}` | the same                 |
| change event | `contact_validate`             | `contact-k7m2q-validate` |
| submit event | `contact_submit`               | `contact-k7m2q-submit`   |

## Tokens

A form carries a random 5-character token in both event names. A rename keeps it, so a handler
stored under the old event name is still found by token and kind (`event_key/1`). A form arriving
in the tree gets a token when it has none or shares one, and any form whose token an earlier form
already holds gets a fresh one (`dedupe/2`). A form without a token keeps the untagged names until
it is renamed or placed again; a markup edit of an ancestor is neither (`dedupe/3`).

There is no control anywhere in the editor that edits one of the five alone, so they cannot
disagree: the Inspector's Form name control writes `attrs/2` in one batch, and every other route
into the tree goes through `dedupe/1`. Two forms on one page are therefore impossible to collide
rather than merely reported as colliding.

## Everything here is a string

A seed is a binary, a field key is a binary, and nothing in this module calls `String.to_atom/1`.
What the editor is producing is SOURCE TEXT — `as={:contact}` and `field={f[:email]}` are markup,
and the atom in them is a literal the page compiler interns once, not a value this module needs.
Minting an atom per label typed into a text box would leak the atom table for no gain, so a caller
writing a key into markup interpolates it as `":" <> key`.

The param name is written as `as={:contact}` rather than `as="contact"` because that is what a
hand-written LiveView form carries, and it is what `Phoenix.Component.form/1` documents;
`MishkaCmsCore.Runtime.PageBuilder.PageForms.param_name/1` reads both forms back.

## Why a seed is re-minted on the way out

`attrs/1` slugs whatever it is handed. A seed that reached the tree some other way — hand-typed
into the code drawer, pasted from another page — could otherwise produce `as={:2 items}`, which is
not an atom literal and does not compile, or `for={assigns[:my form_form]}`, which is not an
assign. Slugging is idempotent, so a seed that is already legal survives untouched.

A seed already in the page is minted the same way before it is compared, so what a rename writes
is measured against what every other form would be called — `as={:"my form"}` cannot be renumbered
into a name a second form is already using.

## Why renumbering runs at the op layer

`dedupe/2` is applied to the whole tree after a mutating op rather than at one insert route,
because paste (`kind: :insert`, built by `MishkaCmsCore.Runtime.PageBuilder.EditorSession.paste/2`)
and duplicate (`kind: :duplicate`) never pass through the insert funnel — and "duplicate this form"
is how an author actually makes the second one. `MishkaCmsCore.Runtime.PageBuilder.Tree.duplicate/2`
re-mints NODE ids and copies every attribute verbatim, so the copy arrives carrying the original's
seed in all five places.

`rekey/2` is the same rule one level down: a duplicated FIELD arrives carrying the key its
original owns, and several inputs bound to one key overwrite each other on submit. Both take the
ids the op just created, and in both the arrival is what gets renamed — see `dedupe/2` for what
renaming the wrong one costs.

## Gaps are filled, not skipped

A page whose forms are `contact` and `contact_3` mints `contact_2` next. Numbering is a search for
the lowest free member of a family, not a counter, so deleting the second of three forms does not
push the next one to `contact_4`.

See also `MishkaCmsCore.Runtime.PageBuilder.PageForms` for what declares a form in the first place.

# `attrs`

```elixir
@spec attrs(String.t() | nil) :: [{String.t(), {:string | :expr, String.t()}}]
```

The five attributes a form node carries with the untagged event names, in the order a reader
expects them, ready for `MishkaCmsCore.Runtime.PageBuilder.Tree.set_attr/4`.

# `attrs`

```elixir
@spec attrs(String.t() | nil, String.t() | nil) :: [
  {String.t(), {:string | :expr, String.t()}}
]
```

The five attributes with `token` inside both event names: `contact-k7m2q-validate` and
`contact-k7m2q-submit`. A `nil` token writes the untagged names, as `attrs/1` does.

# `dedupe`

```elixir
@spec dedupe(
  MishkaCmsCore.Runtime.PageBuilder.Tree.t(),
  Enumerable.t(),
  Enumerable.t()
) ::
  MishkaCmsCore.Runtime.PageBuilder.Tree.t()
```

Renumbers every form that shares a seed with one that claimed it first, rewriting all five derived
names in one pass.

`arrived` is the node ids the op just created, and THEY ARE THE ONES THAT YIELD. Claiming in
document order renamed whichever form came first, so dropping a second Contact form ABOVE the
first rewrote the form already on the page — its param name, its page assign and both event names
— and the `contact_submit` clause its author had written on the Events tab was left bound to
nothing while the page went on compiling. A form nobody has typed a handler for yet is the only
one that can be renamed for free.

With no arrivals named, document order decides, which is what a whole document arriving at once
(`MishkaCmsCore.Runtime.PageBuilder.EditorSession.put_tree/3`) wants: there is no incumbent there.

Tokens follow the same order. A renumbered form keeps its token unless an earlier form holds it,
and gets a fresh one otherwise. A form whose token an earlier form holds gets a fresh one. An
arriving form with no token gets one (`retokened/2`). An incumbent form with no token is left
alone.

`carried` is the event names the page's forms carried before the change. An arriving form with no
token that answers one of them is the same form parsed again (a markup edit of an ancestor), so it
keeps its untagged names unless its seed is taken.

Answers the tree it was given when nothing needs a new name or token.

# `derived`

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

Which attributes a rename touches — read by the Inspector so it can batch them as one op.

# `event`

```elixir
@spec event(String.t(), String.t() | nil, :validate | :submit) :: String.t()
```

One event name: `contact-k7m2q-submit` with a token, `contact_submit` with `nil`.

# `event_key`

```elixir
@spec event_key(term()) :: {String.t(), :validate | :submit} | nil
```

`{token, kind}` of a tagged event name, or `nil` for any other name.

# `field_key`

```elixir
@spec field_key(String.t() | nil, [atom() | String.t()]) :: String.t()
```

A field key minted from a label, unique within its form.

`taken` is the keys already bound in that form, as atoms or as binaries — the parsed side of the
editor reads them out of `f[:email]` expressions as atoms, and this compares them as text.

# `form_token`

```elixir
@spec form_token(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t() | nil) ::
  String.t() | nil
```

The token a form carries in its `phx-submit`, else in its `phx-change`, or `nil`.

# `kept_token`

```elixir
@spec kept_token(
  MishkaCmsCore.Runtime.PageBuilder.Tree.t(),
  MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t()
) :: String.t()
```

The form's own token when no other form in `tree` carries it; otherwise, or when it has none, a
fresh one. What a renumbered form is written with.

# `label`

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

`people_name/1` for a name an author typed rather than a seed: `"Contáct Us"` is "Contact us", and
a name that leaves no seed is "Form 1".

# `mint_token`

```elixir
@spec mint_token(MishkaCmsCore.Runtime.PageBuilder.Tree.t()) :: String.t()
```

A fresh token no event name in `tree` carries: 5 characters of `a-z2-7`.

# `names_for`

```elixir
@spec names_for(
  MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(),
  String.t() | nil,
  String.t() | nil
) ::
  [{String.t(), {:string | :expr, String.t()}}]
```

`attrs/2` for the form `node`, and its `phx-trigger-action` under the new seed when it sends to
another address (`PageForms.sends_elsewhere?/1`), so a rename or a renumber keeps that working.
What every writer of the five names writes.

# `numbered_name`

```elixir
@spec numbered_name(pos_integer() | String.t()) :: String.t()
```

The name of form number `number`, as a person reads it: "Form 3".

# `people_name`

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

The name a site admin reads for a form's seed: `form` is "Form 1", `form_3` is "Form 3", and any
other seed is its words with a capital ("contact_us" is "Contact us"). `nil` for `nil`.

# `rekey`

```elixir
@spec rekey(MishkaCmsCore.Runtime.PageBuilder.Tree.t(), Enumerable.t()) ::
  MishkaCmsCore.Runtime.PageBuilder.Tree.t()
```

Re-keys a field that arrived carrying a key the form it landed in already uses.

The same rule forms get, applied to the keys inside one. `MishkaCmsCore.Runtime.PageBuilder.Tree`
copies every attribute verbatim on a duplicate or a paste, so duplicating a field gave several
inputs one key: all of them render `name="contact[email]"`, typing in one shows that value in all
of them, only the last one submitted survives, and the spec carries a single field — with nothing
in the audit rail to say so. Duplicating a field is how a non-programmer builds the second one.

Every node bound to the colliding key moves together, because a label and its input are one field
and re-keying the input alone would point the label at a field that no longer exists.

A whole form arriving is not a collision. Each form scopes its own params, so the copy keeps every
key it had and only `dedupe/2` renames it.

# `retokened`

```elixir
@spec retokened(MishkaCmsCore.Runtime.PageBuilder.Tree.Node.t(), String.t()) :: [
  {String.t(), {:string, String.t()}}
]
```

The event attributes of `node` rewritten to carry `token`, ready for
`MishkaCmsCore.Runtime.PageBuilder.Tree.set_attr/4`.

Only names this module writes change: a tagged name keeps its own seed and kind, and an untagged
`<seed>_validate`/`<seed>_submit` of the form's own seed gets the token. Any other name is a
developer's and is left out.

# `same_event?`

```elixir
@spec same_event?(term(), term()) :: boolean()
```

Whether two event names carry the same token and the same kind, whatever their seeds.

# `same_form?`

```elixir
@spec same_form?(term(), term()) :: boolean()
```

Whether two event names carry the same token, so they belong to the same form.

# `seed`

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

The seed a label mints: lowercased, accents folded away, every other run of characters turned into
a single `_`, and never starting with a digit.

A label that leaves nothing usable — blank, punctuation only, or written in a script that has no
ASCII form — falls back to `form`, because every one of the five derived names has to
exist.

# `seeds`

```elixir
@spec seeds(MishkaCmsCore.Runtime.PageBuilder.Tree.t()) :: [{String.t(), String.t()}]
```

Every form already in this tree, as `{node id, seed}` in document order.

The seeds are PARSED off the nodes rather than counted, so a page whose only form is `contact_2`
answers `contact_2` and the next one minted is not the same name again.

A node is a form when `MishkaCmsCore.Runtime.PageBuilder.PageForms.form?/1` says so — it holds the
page assign its values live in. Asking only for an `as` would take `as` at its HTML word, and `as`
is an ordinary attribute of an ordinary tag:

    <link rel="preload" as="font" href="/inter.woff2" />

Two of those on a page read as two forms both seeded `font`, and `dedupe/1` renames the second by
writing five form attributes onto a `<link>` — breaking the preload, minting a form nobody placed,
and raising a duplicate-seed blocker about it. It runs on every insert, duplicate, wrap AND on
every save, draft restore and history revert, so the wreckage is what gets stored.

# `token`

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

The token inside a tagged event name, or `nil`.

# `trigger`

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

The `phx-trigger-action` a form sending to another address carries: the page assign
`MishkaCmsCore.Runtime.PageForm.submit/5` sets once its answers are checked, `assigns[:contact_send]`
for the seed `contact`.

# `unique`

```elixir
@spec unique(MishkaCmsCore.Runtime.PageBuilder.Tree.t(), String.t() | nil) ::
  String.t()
```

A seed based on `base` that no form in `tree` is using.

`base` may be an author's label; it is minted through `seed/1` first, so the answer is always a
legal name.

---

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