# `MishkaCmsCore.Builder.Command`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/builder/command.ex#L5)

A `Phoenix.LiveView.JS` command as a term, printed to source and read back from it.

## Nothing is spelled by hand

An action says what it wants as a triple — the command's name, its positional arguments, and its
options — and every step after that belongs to somebody else's code:

  * `Phoenix.LiveView.JS` decides whether the options are legal. The triple is applied to it and
    the `%Phoenix.LiveView.JS{}` it answers with is thrown away; what is kept is that it answered
    at all. So `transition:`, `time:`, `display:` and `blocking:` are supported by this module
    knowing nothing about them, an option a later LiveView adds arrives supported, and the
    message an author sees for a wrong one is LiveView's own.
  * `Macro.escape/1` turns the terms into quoted Elixir, which is what gets a class name
    containing a double quote, a backslash or `#{` right — the third being the one that is not a
    broken string but interpolation running in somebody's page.
  * The code formatter prints it, at infinite width so that one command is one line.
  * `Code.string_to_quoted/1` parses it back, and `MishkaCmsCore.Builder.Literal` unwraps the
    literals into the terms an action matches on.

This is what replaced a private table of `~s|JS.#{verb}("#{class}", to: "#{target}")|`. A string
built that way has to be re-checked after the fact, because building it is where the mistake goes;
a term printed by the formatter cannot have the mistake in it.

## Where the application happens

In the editor, once, when an author presses Apply — `MishkaCmsCore.Runtime.PageBuilder.Interactions`
calls this from the admin LiveView. What reaches a document is the printed source and only that,
so nothing here is in a visitor's page, in the compiled module, or on the render path.

## What a page is allowed to contain

A command names `JS` and carries data. That is the whole rule, and it is kept by construction
rather than by inspection: an action hands over terms, `data?/1` refuses anything that is not data
before it is escaped, and the source is built here. There is no path by which a package's text
reaches a document.

# `t`

```elixir
@type t() :: {atom(), [term()], keyword()}
```

One command: the `Phoenix.LiveView.JS` function name, its positional arguments, and its options.

    {:add_class, ["is-open"], [to: ".pb-7f3a91b2"]}
    {:push, ["pb:say"], [value: %{"kind" => "info", "message" => "Saved"}]}
    {:show, [], [to: ".pb-7f3a91b2", time: 300]}

# `read`

```elixir
@spec read(String.t()) :: {:ok, [t()]} | :no
```

The commands a source string holds, in the order they run, or `:no`.

A pipe reads as a list; anything that is not `JS` calls carrying literal data reads as `:no` and
belongs to whoever hand-wrote it.

    iex> MishkaCmsCore.Builder.Command.read(~s|JS.toggle_class("on", to: ".pb-1")|)
    {:ok, [{:toggle_class, ["on"], [to: ".pb-1"]}]}

    iex> MishkaCmsCore.Builder.Command.read(~s|JS.show(to: some_variable)|)
    :no

# `write`

```elixir
@spec write(t() | [t()]) :: {:ok, String.t()} | {:error, String.t()}
```

The source for one command, or for a pipe of them.

    iex> MishkaCmsCore.Builder.Command.write({:add_class, ["is-open"], [to: ".pb-1"]})
    {:ok, ~s|JS.add_class("is-open", to: ".pb-1")|}

    iex> MishkaCmsCore.Builder.Command.write({:show, [], [to: ".pb-1", speed: "fast"]})
    {:error, "invalid option for show\nExpected keys to be one of [:to, :transition, :display, :time, :blocking], got: :speed\n"}

---

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