StatifierUI.Diagram (StatifierUI v0.3.0)

Copy Markdown View Source

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 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.

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.

Summary

Functions

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

Functions

render(machine, configuration)

@spec render(Statifier.Machine.t(), Enumerable.t()) :: 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.

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