MishkaCmsCore.Runtime.PageBuilder.Op (Mishka CMS Core v0.0.1)

Copy Markdown View Source

One validated edit request — the only thing allowed to reach a MishkaCmsCore.Runtime.PageBuilder.Tree combinator.

Every handle_event in the editor turns its raw params into an Op with build/1 before the session touches the tree (ADR-13). That is what makes the server the authority rather than a transcription service: a tampered payload fails validation here, and a plugin that wants to veto or rewrite an edit gets a typed struct on the builder:node:before_* hook instead of a bag of strings.

Why one struct and not twelve

ADR-13 sketches a struct per combinator. One struct with a validated kind and a per-kind required-field check gives the same guarantee — nothing unvalidated reaches a combinator — with one module to read and one hook payload shape for plugins to match on. It follows the house precedent (MishkaCmsCore.Dashboard.Widget is likewise one struct with a type), and pattern matching on %Op{kind: :move} reads the same as matching a Move struct would.

Insertion is always "parse this template"

Dropping a palette component, inserting an example variant and pasting the clipboard are the same operation: take HEEx, parse it, splice the subtree in. :insert therefore carries a template string rather than a pre-built node, which means an example keeps its attributes and children (the React editor used only ast[0][0] and dropped both) and a pasted subtree is validated by the same parser as everything else.

Summary

Functions

Applies a validated op to a tree.

The style buckets an override may be recorded in — every width crossed with every state.

Builds an op from raw client params.

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

Every op kind the editor can issue.

A short label for the history sheet and the plugin payload — "Moved element", "Set class", …

Whether an op changes the document (and so belongs in the undo history) — every kind currently does, but the editor also routes selection and viewport changes through the same funnel, and those must not consume a history slot.

Types

t()

@type t() :: %MishkaCmsCore.Runtime.PageBuilder.Op{
  bucket: atom() | nil,
  index: integer() | nil,
  kind: atom(),
  name: String.t() | nil,
  node_id: String.t() | nil,
  node_ids: list() | nil,
  parent_id: String.t() | nil,
  prop: String.t() | nil,
  tag: String.t() | nil,
  template: String.t() | nil,
  text: String.t() | nil,
  value: String.t() | nil,
  value_kind: atom() | nil
}

Functions

apply_to(op, tree)

Applies a validated op to a tree.

Returns {:ok, tree}, {:ok, tree, node_id} for the ops that produce a node the caller wants to select afterwards, or {:error, reason}. Containment is not checked here — the session does that, so a plugin rewriting an op on the before_* hook cannot accidentally bypass it.

buckets()

@spec buckets() :: [atom()]

The style buckets an override may be recorded in — every width crossed with every state.

build(params)

@spec build(map()) :: {:ok, t()} | {:error, term()}

Builds an op from raw client params.

Accepts string or atom keys, coerces the handful of fields the wire sends as strings (index, kind, bucket, value_kind) and then enforces the fields that kind actually needs — a :move without a node_id, or a :set_style naming a bucket that does not exist, is refused here rather than halfway through a combinator.

builder(attrs_or_input, error \\ false)

enforce_keys()

enforce_keys(field)

example()

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

keys()

keys(field)

kinds()

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

Every op kind the editor can issue.

label(map)

@spec label(t()) :: String.t()

A short label for the history sheet and the plugin payload — "Moved element", "Set class", …

mutating?(map)

@spec mutating?(t()) :: boolean()

Whether an op changes the document (and so belongs in the undo history) — every kind currently does, but the editor also routes selection and viewport changes through the same funnel, and those must not consume a history slot.