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
:scxmlroot is declared with a stables<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
[*] --> sNmarker per resolved initial state, including one written without aninitialattribute (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'sexternaldefault 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 inState.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
activeMermaid 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>anddonedatahave 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
@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