Options for the dot/SVG render entry points.
hide_state_labels (default false) — when true, the rendered dot
output omits the label= attribute on every state's node line.
Graphviz then draws the box without any text inside. Useful for
diagrams where shape, color, or layout alone carry the meaning
(icon-only diagrams, tutorial graphics, presentation slides).footer — verbatim dot source inserted just before the closing }
of the generated dot source (e.g. labelloc="b"; label="caption";).engine — graphviz layout engine for the SVG render path (e.g.
dot, neato, circo); honored by fsl_to_svg_string.render_groups (default 'cluster') — how FSL state groups are drawn;
see RenderGroups. 'off' reproduces the historical, group-blind
output byte-for-byte. Build the per-state chip map for 'chips' mode: every group a state
belongs to, in declaration order, becomes a chip on its label, and no
clusters are emitted at all. This is the all-chips counterpart to the
overflow-only chips produced by plan_cluster_groups.
// for `&inner:[a]; &outer:[&inner b]; a -> b;`
// chips_for_all_groups(m, ['a','b']) === Map { 'a' => ['inner','outer'], 'b' => ['outer'] }
Slugify a group name into the body of a Graphviz cluster_… subgraph
identifier. Graphviz treats any subgraph whose name begins with the
literal cluster as a visually-boxed cluster, so the emitted name is
cluster_<slug>_<index>. The slug alphabet is lowercase alphanumerics
joined by _; a name that slugs to empty (e.g. &"!!!") falls back to
g<index> (the index alone, no slug component). The _<index> suffix
guarantees every group gets a unique cluster id even when two distinct
names happen to slugify identically (e.g. "Active Players" and
"active-players" both slug to active_players).
cluster_id_for('Active Players', 0); // 'cluster_active_players_0'
cluster_id_for('!!!', 3); // 'cluster_g3'
The FSL group name.
The group's stable declaration-order index (0-based); included in the emitted id to prevent slug collisions.
A valid Graphviz subgraph identifier starting with cluster_.
Convert an 8-channel hex color (#RRGGBBAA) to a 6-channel hex color
(#RRGGBB), discarding the alpha channel. Throws if the input is not
a 9-character #-prefixed string.
Graphviz dot does not support alpha; this is a lossy projection.
Escape a string for safe interpolation inside a DOT double-quoted
attribute value. Replaces every " with \" so that group names,
state labels, and chip labels containing literal double-quotes produce
valid, parseable DOT source.
doublequote('a"b'); // 'a\\"b'
doublequote('safe'); // 'safe'
Any string that will be placed inside "…" in a DOT attribute.
The string with every " replaced by \".
Map a single transition: {} config item ({ key, value }) to a Graphviz
edge-scope attribute name="value" pair, or undefined when the key has
no edge-meaningful projection. Mirrors the per-node mapping in
{@link state_node_line}, but targets the attribute names Graphviz uses on
edges:
color and the legacy graph_default_edge_color both set the edge line
color.text-color → edge label fontcolor.line-style → edge style (dashed/dotted/solid pass through). Node-only keys (background-color, shape, corners, image, url,
state-label, border-color) have no edge meaning and yield undefined,
so they are dropped from the edge [ … ] default statement.
Project a JssmTransitionConfig (the compiled transition: {} block)
onto the body of a Graphviz default-edge statement — the attribute list that
belongs inside edge [ … ];. Per-key last-wins is already applied by the
compiler, so the list is walked in order and each edge-meaningful key
(see edge_attr_for) contributes one attribute. Returns the empty
string when the config is absent or contributes nothing, so a machine with
no transition: {} block produces byte-identical output to before.
edge_defaults_body([{ key: 'color', value: '#0000ffff' }]);
// 'color="#0000ffff"'
Map a single graph: {} config item ({ key, value }) to a Graphviz
graph-scope attribute name="value" pair, or undefined when the key is
either not graph-meaningful or already handled by another machine path
(graph_layout → SVG engine, flow → rankdir, theme → style cascade,
dot_preamble → preamble). background-color is handled separately — it
feeds the existing single bgcolor slot in {@link dot_template} so it is
never double-emitted — and so is excluded here.
color → graph color (cluster/graph border).text-color → graph fontcolor. Project the graph-scope attributes of a JssmGraphConfig that are NOT
the background color (handled via graph_bg_color_from_config) onto
one Graphviz graph attribute statement per key (e.g. color="…";). Returns
the empty string when nothing applies, so machines without graph-scope color
attributes are byte-identical to before.
Read the effective graph background color from a JssmGraphConfig,
honouring the background-color item the compiler folded graph_bg_color
into (and into which an explicit graph: { background-color: … } block
already won, last-wins). Falls back to the supplied palette default when
the config carries no background color, so output is unchanged for machines
without a graph: {} background.
This is the single reconciliation point for the graph background: the value
it returns flows into {@link dot_template}'s one bgcolor="…" slot, so the
graph: {} value wins over the legacy alias and is never emitted twice.
Walk the primary-parent chain of a group up to its root, returning the ancestor set including the group itself. Used both to nest clusters and to decide which of a state's memberships its primary cluster already represents (so the rest become chips).
Build the group→parent-group map used to lay groups out as a properly
nested cluster tree. Graphviz clusters must nest strictly (a node lives in
exactly one innermost cluster, and clusters may not partially overlap), but
the FSL group registry is a DAG — a sub-group may be referenced by several
parents. We therefore pick, for each group that is referenced as a
group-kind member, a single primary parent: the earliest-declared group
that lists it. Groups with no parent are roots.
// for `&inner:[a]; &outer:[&inner b];`
// group_parent_map(reg, ['inner','outer']) === Map { 'inner' => 'outer' }
Emit the nested-cluster DOT for a machine's groups, weaving each state's
node statement into its primary cluster's subgraph cluster_<group> { … }
block, deepest group innermost. Roots (groups with no primary parent) are
emitted at top level; ungrouped states are returned separately so the
caller can place them outside every cluster.
The cluster tree follows group_parent_map — the strict-nesting subset of the group DAG — so output is always valid Graphviz even when the source groups genuinely overlap; the unrepresentable memberships travel as chips on the node label instead (see plan_cluster_groups).
// for `&inner:[a]; &outer:[&inner b]; a -> b;` the result contains
// subgraph cluster_outer { label="outer"; … subgraph cluster_inner { … } }
{ clusters, ungrouped_nodes } — the cluster DOT block and the
node statements for states in no group.
Read the image filename for a state through jssm.Machine.style_for,
so theme-supplied images are honoured along with per-state declarations.
Returns undefined if neither a theme nor a state declaration supplies an
image.
Append group-membership chips to a node label. Each extra group becomes a
bracketed suffix (e.g. Foo [overlap] [extra]), so a node that the cluster
tree can only place in its primary group still surfaces its other
memberships visually. With no chips the label is returned verbatim, so
chip-free output is byte-identical to the historical label.
label_with_chips('Foo', []); // 'Foo'
label_with_chips('Foo', ['a', 'b']); // 'Foo [a] [b]'
Assemble the node-declaration block for a machine, honouring the
render_groups mode:
'cluster' — emit nested subgraph cluster_<group> { … } boxes via
groups_to_subgraph_string, with each member node statement woven
into its primary cluster and overlap memberships chipped onto the label.'chips' — emit a flat node list (no clusters) with every group
membership rendered as a label chip (see chips_for_all_groups).'off' — emit the historical flat node list, ignoring groups
entirely; output is byte-identical to the pre-groups renderer. A machine that declares no groups produces the same flat node list in every
mode, so 'cluster'/'chips' are no-ops there.
Build a graphviz-safe node identifier for a state. Accepts either a
string[] (legacy test-only path; returns an index-based n0/n1
identifier via indexOf), or a precomputed Map<state, slug> produced
by slug_states (used by all rendering hot paths).
When a slug map is supplied, the identifier is the slug wrapped in
double quotes — dot allows quoted identifiers, and the slug alphabet
(lowercase alphanumerics + -) requires quoting because bare dot IDs
may not contain -. Graphviz round-trips the quoted form through to
the SVG <title> element and uses the slug as a stable basis for the
generated SVG element id attribute.
node_of('Red Light', new Map([['Red Light', 'red-light']]));
// '"red-light"'
Plan how each state's groups are rendered in 'cluster' mode. Produces,
per state, the primary cluster it is placed in (or undefined for an
ungrouped state) plus the chip groups — memberships the primary cluster's
ancestry does not already represent, i.e. genuine overlap that nesting
cannot show.
{ placement, chips } where placement maps state → primary
group, and chips maps state → the overflow group names (declaration
order).
Choose the primary cluster for a state: the innermost (smallest
{@link membership_distance}) group containing it, ties broken by latest
declaration order — the same precedence the config cascade uses, so a
state's cluster placement agrees with the group whose style won. Returns
undefined for a state in no group.
Read the graphviz shape for a state through jssm.Machine.style_for,
so theme-supplied shapes are honoured along with per-state declarations.
Returns undefined if neither a theme nor a state declaration supplies a
shape.
Convert a state name into a URL-friendly slug suitable for use as the body of a dot/SVG node identifier. The transformation is:
[a-z0-9] (after lowercasing) becomes
a single -- are trimmed If the result is empty (e.g. for a state named "!!!"), the empty
string is returned — callers are expected to fall back to an indexed
placeholder like node-N. See slug_states for the collision-
resolving wrapper that consumes this helper.
slug_for('Green Light'); // 'green-light'
slug_for('!!!'); // ''
slug_for(' Foo Bar '); // 'foo-bar'
The state name to slugify.
The lowercase hyphen-separated slug, or empty string if none of the characters were retainable.
Build a Map<state, slug> assigning every state in states a unique,
deterministic, URL-safe slug used as its dot/SVG node identifier.
Algorithm:
node-N, where N is the state's declaration
index (1-based, to match user-visible numbering).-2, -3, … suffixes.
If the proposed suffixed slug itself collides with a base slug
used later, the counter advances until a free slot is found.This yields a deterministic mapping given the state-declaration order, so output is stable across runs.
slug_states(['Red Light', 'red-light']);
// Map { 'Red Light' => 'red-light', 'red-light' => 'red-light-2' }
slug_states(['!!!', '???']);
// Map { '!!!' => 'node-1', '???' => 'node-2' }
States in declaration order.
A Map from each state name to its unique slug.
Compose a graphviz style string for a state by looking up its merged
style via jssm.Machine.style_for, then delegating to
{@link compose_style_string}. Theme-supplied corners and lineStyle
are honoured along with per-state declarations.
Variant of color8to6 that passes undefined through.
Look up a color from the default viz palette by key, returning empty string if the key is unknown (so it disappears in feature concatenation).
Inject runtime configuration for jssm/viz. Currently only accepts a
custom DOMParser constructor for use by *_svg_element functions in
environments that do not provide one globally (e.g. Node + jsdom).
Idempotent — last call wins. No-op if called with no recognized keys.
// Node, with jsdom:
import { JSDOM } from 'jsdom';
import { configure, fsl_to_svg_element } from 'jssm/viz';
configure({ DOMParser: new JSDOM().window.DOMParser });
const el = await fsl_to_svg_element('a -> b;');
Configuration overrides.
Constructor compatible with the WHATWG DOMParser
interface. Used as a fallback when globalThis.DOMParser is undefined.
Compatibility wrapper for machine_to_dot, retained from jssm-viz. Will be removed in the next major.
Render a graphviz dot source string to SVG using @viz-js/viz. The
underlying viz instance is lazy-initialized on first call and cached for
the lifetime of the module.
const svg = await dot_to_svg('digraph G { a -> b }');
const svg_neato = await dot_to_svg('digraph G { a -> b }', { engine: 'neato' });
Graphviz dot source.
Optional renderer overrides.
Graphviz layout engine to use (e.g. 'dot',
'neato', 'circo'). Unrecognized engine names cause @viz-js/viz
to throw at render time.
A promise resolving to an SVG XML string.
Render an FSL string directly to graphviz dot source.
import { fsl_to_dot } from 'jssm/viz';
const dot = fsl_to_dot('a -> b;');
// suppress state-name labels
const dot2 = fsl_to_dot('a -> b;', { hide_state_labels: true });
const dot_with_footer = fsl_to_dot('a -> b;', { footer: 'label="caption";' });
// 'digraph G { ... label="caption"; }'
The FSL source.
Optional render flags. See VizRenderOpts.
A complete graphviz dot source string.
Render an FSL string directly to a parsed SVGSVGElement.
The FSL source.
Optional render flags. See VizRenderOpts.
A promise resolving to a parsed SVGSVGElement.
Render an FSL string directly to SVG.
const svg = await fsl_to_svg_string('a -> b;');
const svg_neato = await fsl_to_svg_string('a -> b;', { engine: 'neato' });
The FSL source.
Optional render flags. See VizRenderOpts.
A promise resolving to an SVG XML string.
Render a jssm.Machine as a graphviz dot string.
An optional footer may be supplied via opts.footer; it is emitted
verbatim just before the closing } of the dot source, after all
arrange declarations. This is a function-argument-only feature for
the moment — a machine-attribute equivalent is planned as a follow-up.
import { sm } from 'jssm';
import { machine_to_dot } from 'jssm/viz';
const dot = machine_to_dot(sm`a -> b;`);
// 'digraph G { ... }'
// suppress state-name labels (boxes only, no text inside)
const dot2 = machine_to_dot(sm`a -> b;`, { hide_state_labels: true });
const dot_with_footer = machine_to_dot(sm`a -> b;`, { footer: 'labelloc="b"; label="caption";' });
// 'digraph G { ... labelloc="b"; label="caption"; }'
// render FSL state groups as nested clusters (the default)
const grouped = machine_to_dot(sm`&g : [a b]; a -> b;`);
// 'digraph G { ... subgraph cluster_g { label="g"; ... } ... }'
// or as label chips, with no cluster boxes
const chipped = machine_to_dot(sm`&g : [a b]; a -> b;`, { render_groups: 'chips' });
The machine to render.
Optional render flags. See VizRenderOpts.
A complete graphviz dot source string.
Render a jssm.Machine to a parsed SVGSVGElement.
The machine to render.
Optional render flags. See VizRenderOpts.
A promise resolving to a parsed SVGSVGElement.
Render a jssm.Machine to SVG.
The machine to render.
Optional render flags. See VizRenderOpts.
A promise resolving to an SVG XML string.
Generated using TypeDoc
How machine_to_dot renders FSL state groups (
&group : [ … ];).'cluster'(default) — render the subset of groups that form a clean nesting tree as nested Graphvizsubgraph cluster_<group> { … }boxes, deepest group innermost. A state that genuinely overlaps two groups (member of two groups where neither nests inside the other) can only be drawn inside one cluster, so its remaining memberships are shown as bracketed chips appended to the node label.'chips'— render every group membership as a chip on the node label and emit no clusters at all. Useful when cluster boxes clutter the diagram or when overlap is pervasive.'off'— ignore groups entirely; byte-for-byte the historical output.