# `StatifierUI.Diagram`
[🔗](https://github.com/riddler/statifier-ui/blob/v0.11.0/lib/statifier_ui/diagram.ex#L1)

Renders a compiled `Statifier.Machine` and an active configuration as
Mermaid `stateDiagram-v2` source, for display via `Kino.Mermaid` (or any
other Mermaid consumer - this module is pure and depends on neither Kino
nor LiveView).

`render/2` is a pure function: machine and configuration in, diagram
source out. That interface is the stable part. The Mermaid backend is the
Livebook inspector's first rendering (statifier-ui ADR-0008 fixes the
destination stack as client-side elkjs producing SVG, and explicitly
leaves this first cut to the inspector epic); when the elkjs renderer
arrives it replaces the body of this module, not its callers.

## The accepted Mermaid compromise

Mermaid cannot draw a transition between internal states of two
*different* composite states - the cross-hierarchy edges SCXML's LCCA
semantics make routine, and the limitation that disqualified Mermaid as
the destination renderer (ADR-0008). Rather than drop those transitions,
`render/2` lifts each one to the pair of composite siblings under the
endpoints' least common ancestor and draws the edge there, with a
`[lifted: <source> -> <target>]` marker appended to the label naming the
real endpoints. The same lifting applies to a composite's `[*]` initial
marker when its resolved initial state is a deep descendant rather than a
direct child - the marker there is `[deep: <target>]`.

## What the source contains

- Every state except the synthesized `:scxml` root is declared with a
  stable `s<index>` alias (the engine's document-order state index, the
  identity vocabulary of statifier ADR-0012), so tests and callers can
  address nodes without depending on how labels are escaped.
- Compound states are composite blocks; parallel states are composite
  blocks whose children are separated by Mermaid's `--` region divider.
- `<final>` states carry a `(final)` label suffix; history states carry
  `(H)` (shallow) or `(H*)` (deep) - Mermaid has no native pseudo-state
  notation for either.
- Every compound state carries a `[*] --> sN` marker per resolved initial
  state, including one written without an `initial` attribute (the
  compiler resolves it to the first child). A parallel state carries
  none, because entering it enters every region at once.
- Transitions are labeled with their event descriptors; a guarded
  transition carries a `[cond]` marker (the Machine retains the compiled
  expression, not its source text).
- A transition label is written with `:` escaped as Mermaid's `#58;`
  entity code (and `#` as `#35;` ahead of it). Mermaid's grammar ends
  the label at the next `:` and offers no quoted form for it, so a
  prefixed event descriptor - `myapp:authorize` - would otherwise make
  the whole source unparseable. The escape renders as a colon.
- A **targetless** transition - spec-legal, and the way a chart runs
  executable content without changing configuration - is drawn as a
  self-edge marked `[internal]`. UML puts one inside the state's box;
  Mermaid has no in-box notation, and dropping the transition entirely
  is worse: it renders a state that handles an event as one that
  ignores it.
- A transition written `type="internal"` also carries `[internal]`.
  SCXML's `external` default is left unmarked, so the marker means "this
  edge does not exit and re-enter its source".
- A **history** state's default transition (`State.history_default`) is
  drawn with a `[default]` marker. It is not selectable, so it is not in
  `State.transitions`; without this the `(H)` / `(H*)` label would name a
  pseudo-state whose fallback target is invisible.
- Active states - every index in the configuration, ancestors included,
  per the full-configuration convention of statifier-ui ADR-0005 - are
  assigned the `active` Mermaid class. Out-of-range indexes and the root
  are ignored, so a stale or empty configuration degrades to an
  unhighlighted chart rather than an error.

## Theming the active highlight

The `active` class is styled by a `classDef` line the source carries, and
by default that line is the shipped light palette. A host whose chrome is
dark passes `active_style: :none` to `render/3`, which drops the
`classDef` and leaves the `class sN active` assignment: the rendered nodes
still carry the class, so the host's own stylesheet or Mermaid theme
reaches them. A host that would rather keep the styling inside the source
passes its own `classDef` body as a binary instead. Neither path asks a
host to post-process the source it was handed.

`StatifierUI.Inspector.diagram/3` takes the same option and passes it
through, and `StatifierUI.Live.diagram/1` takes it as an attribute. The
Livebook inspector (`StatifierUI.Kino`) builds its own fold options and
forwards none, so it always draws the default palette.

## Known limits of this projection

These are accepted, not defects to file. Each is a thing the Mermaid
backend cannot express; the destination elkjs renderer (ADR-0008) is
where they get fixed, and none of them makes the source *silently*
wrong - every one is either marked in the output or listed here.

- **Lifted edges lose their real endpoints in the picture.** A lifted
  edge is drawn composite-to-composite and the true endpoints survive
  only in the `[lifted: ...]` label text, not in the geometry. This also
  covers an edge between two regions of the same parallel state: it is
  lifted to the two regions, which reads as leaving one lane for
  another when the semantics are an exit and re-entry within them.
- **Internal transitions are drawn as self-edges.** The arrow implies a
  round trip through exit and entry that an internal transition does not
  make; only the `[internal]` marker says otherwise.
- **Pseudo-states are ordinary nodes.** History and final states are
  drawn as boxes with a label suffix, so a chart with many of them reads
  as having more real states than it has.
- **Shallow and deep history differ only in the label.** `(H)` versus
  `(H*)` is the whole distinction; what each restores is not drawable.
- **Executable content is not drawn at all.** `onentry`, `onexit`,
  transition content, `<invoke>` and `donedata` have no notation here.
  An edge label says which event fires a transition, never what it does.
- **Nothing is laid out by this module.** Mermaid decides geometry, so
  region order within a parallel is document order and nothing keeps a
  deeply nested chart from rendering wider than it is readable.

# `active_style`

```elixir
@type active_style() :: :default | :none | String.t()
```

How the `active` class is styled in the emitted source.

  * `:default` - the shipped light palette, emitted as a `classDef`.
  * `:none` - emit no `classDef` at all. The `class sN active` assignment
    stays, so the rendered nodes still carry the class and the host's own
    stylesheet or Mermaid theme decides how they look.
  * a binary - the body of the `classDef`, verbatim: Mermaid style
    declarations separated by commas, as in
    `"fill:#0c4a6e,stroke:#38bdf8,color:#e0f2fe"`.

# `opt`

```elixir
@type opt() :: {:active_style, active_style()}
```

Options accepted by `render/3`.

# `render`

```elixir
@spec render(Statifier.Machine.t(), Enumerable.t(), [opt()]) :: String.t()
```

Renders `machine` with `configuration` highlighted, as Mermaid
`stateDiagram-v2` source.

`configuration` is any enumerable of state indexes - typically the full
active configuration (`MapSet.t(non_neg_integer())`) a trace message or
`Statifier.MachineState` carries. Pass `[]` (or an empty set) for a chart
that is not running.

## Options

  * `:active_style` - how the active-configuration highlight is styled;
    `t:active_style/0`. Defaults to `:default`, which emits the shipped
    light palette unchanged.

A host under a dark theme passes `:none` and styles `.active` from its own
stylesheet, or passes its own `classDef` body as a binary. Neither
requires post-processing the returned source.

## Examples

    iex> {:ok, machine} =
    ...>   Statifier.compile("""
    ...>       <scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="pending">
    ...>         <state id="pending">
    ...>           <transition event="authorize.approved" target="authorized"/>
    ...>         </state>
    ...>         <state id="authorized"/>
    ...>       </scxml>
    ...>   """)
    iex> source = StatifierUI.Diagram.render(machine, [1])
    iex> String.starts_with?(source, "stateDiagram-v2")
    true
    iex> source =~ "class s1 active"
    true
    iex> source =~ "classDef active"
    true

    iex> {:ok, machine} =
    ...>   Statifier.compile("""
    ...>       <scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initial="pending">
    ...>         <state id="pending"/>
    ...>       </scxml>
    ...>   """)
    iex> source = StatifierUI.Diagram.render(machine, [1], active_style: :none)
    iex> source =~ "class s1 active"
    true
    iex> source =~ "classDef"
    false

---

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