jssm 5.124.1 → 5.125.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,10 +18,10 @@ Please edit the file it's derived from, instead: `./src/md/readme_base.md`
18
18
 
19
19
 
20
20
 
21
- * Generated for version 5.124.1 at 5/19/2026, 1:07:52 PM
21
+ * Generated for version 5.125.1 at 5/20/2026, 3:45:22 PM
22
22
 
23
23
  -->
24
- # jssm 5.124.1
24
+ # jssm 5.125.1
25
25
 
26
26
  [**Try the live editor**](https://stonecypher.github.io/jssm-viz-demo/graph_explorer.html) ·
27
27
  [Documentation](https://stonecypher.github.io/jssm/docs/) ·
@@ -281,7 +281,7 @@ That decision shows up everywhere downstream:
281
281
  or run `npm run benny` against your own machine.
282
282
 
283
283
  - **More thoroughly tested than any other JavaScript state-machine
284
- library.** 6,078 tests at 100.0% line coverage
284
+ library.** 6,104 tests at 100.0% line coverage
285
285
  ([report](https://coveralls.io/github/StoneCypher/jssm)), plus
286
286
  fuzz testing via `fast-check`, with parser test data across ten natural
287
287
  languages and Emoji.
@@ -414,11 +414,11 @@ If your contribution is missing here, please open an issue.
414
414
 
415
415
  <br/>
416
416
 
417
- ***6,078 tests***, run 56,865 times.
417
+ ***6,104 tests***, run 56,891 times.
418
418
 
419
- - 5,565 specs with 100.0% coverage
419
+ - 5,591 specs with 100.0% coverage
420
420
  - 513 fuzz tests with 4.5% coverage
421
- - 4,331 TypeScript lines - 1.4 tests per line, 13.1 generated tests per line
421
+ - 4,355 TypeScript lines - 1.4 tests per line, 13.1 generated tests per line
422
422
 
423
423
  [![Actions Status](https://github.com/StoneCypher/jssm/workflows/Node%20CI/badge.svg)](https://github.com/StoneCypher/jssm/actions)
424
424
  [![NPM version](https://img.shields.io/npm/v/jssm.svg)](https://www.npmjs.com/package/jssm)
package/dist/cdn/viz.js CHANGED
@@ -21041,7 +21041,7 @@ var constants = /*#__PURE__*/Object.freeze({
21041
21041
  * Useful for runtime diagnostics and for embedding in serialized machine
21042
21042
  * snapshots so that deserializers can detect version-skew.
21043
21043
  */
21044
- const version = "5.124.1";
21044
+ const version = "5.125.1";
21045
21045
 
21046
21046
  // whargarbl lots of these return arrays could/should be sets
21047
21047
  const { state_name_chars, state_name_first_chars, action_label_chars } = constants;
@@ -22522,9 +22522,22 @@ class Machine {
22522
22522
  }
22523
22523
  */
22524
22524
  /** List all action names available as exits from a given state.
22525
+ *
22526
+ * Returns the empty array (does not throw) when `whichState` exists but has
22527
+ * no action-named exits — including terminal states, states whose only
22528
+ * exits are plain `->` transitions, and states in machines that use no
22529
+ * actions at all. Only nonexistent states cause a throw.
22530
+ *
22525
22531
  * @param whichState - The state to inspect. Defaults to the current state.
22526
- * @returns An array of action name strings.
22532
+ * @returns An array of action name strings, possibly empty.
22527
22533
  * @throws {JssmError} If the state does not exist.
22534
+ *
22535
+ * @example
22536
+ * const m = sm`a 'go' -> b; b -> c;`;
22537
+ * m.list_exit_actions('a'); // => ['go']
22538
+ * m.list_exit_actions('b'); // => []
22539
+ * m.list_exit_actions('c'); // => []
22540
+ * expect(() => m.list_exit_actions('z')).toThrow();
22528
22541
  */
22529
22542
  list_exit_actions(whichState = this.state()) {
22530
22543
  const ra_base = this._reverse_actions.get(whichState);
@@ -24357,18 +24370,113 @@ function vc(col) {
24357
24370
  return (_a = default_viz_colors[col]) !== null && _a !== void 0 ? _a : '';
24358
24371
  }
24359
24372
  /**
24360
- * Build a graphviz-safe node identifier for a state, by index. Accepts
24361
- * either a `string[]` (used historically; O(n) per call) or a
24362
- * precomputed `Map<state, index>` (used by rendering hot paths; O(1)
24363
- * per call). The map form is used during dot generation; the array
24364
- * form is retained for direct test access via `_test`.
24373
+ * Convert a state name into a URL-friendly slug suitable for use as the
24374
+ * body of a dot/SVG node identifier. The transformation is:
24375
+ *
24376
+ * 1. Lowercase
24377
+ * 2. Any run of characters outside `[a-z0-9]` (after lowercasing) becomes
24378
+ * a single `-`
24379
+ * 3. Leading and trailing `-` are trimmed
24380
+ *
24381
+ * If the result is empty (e.g. for a state named `"!!!"`), the empty
24382
+ * string is returned — callers are expected to fall back to an indexed
24383
+ * placeholder like `node-N`. See {@link slug_states} for the collision-
24384
+ * resolving wrapper that consumes this helper.
24385
+ *
24386
+ * ```typescript
24387
+ * slug_for('Green Light'); // 'green-light'
24388
+ * slug_for('!!!'); // ''
24389
+ * slug_for(' Foo Bar '); // 'foo-bar'
24390
+ * ```
24391
+ *
24392
+ * @param state The state name to slugify.
24393
+ * @returns The lowercase hyphen-separated slug, or empty string if none of
24394
+ * the characters were retainable.
24395
+ *
24396
+ * @internal
24397
+ */
24398
+ function slug_for(state) {
24399
+ return state
24400
+ .toLowerCase()
24401
+ .replace(/[^a-z0-9]+/g, '-')
24402
+ .replace(/^-+|-+$/g, '');
24403
+ }
24404
+ /**
24405
+ * Build a `Map<state, slug>` assigning every state in `states` a unique,
24406
+ * deterministic, URL-safe slug used as its dot/SVG node identifier.
24407
+ *
24408
+ * Algorithm:
24409
+ *
24410
+ * 1. Slug each state via {@link slug_for}. States whose slug comes out
24411
+ * empty fall back to `node-N`, where `N` is the state's declaration
24412
+ * index (1-based, to match user-visible numbering).
24413
+ * 2. Walk the state list in declaration order, tracking how many times
24414
+ * each base slug has already been used. The first occurrence keeps
24415
+ * the base slug; subsequent collisions get `-2`, `-3`, … suffixes.
24416
+ * If the proposed suffixed slug itself collides with a base slug
24417
+ * used later, the counter advances until a free slot is found.
24418
+ *
24419
+ * This yields a deterministic mapping given the state-declaration order,
24420
+ * so output is stable across runs.
24421
+ *
24422
+ * ```typescript
24423
+ * slug_states(['Red Light', 'red-light']);
24424
+ * // Map { 'Red Light' => 'red-light', 'red-light' => 'red-light-2' }
24425
+ *
24426
+ * slug_states(['!!!', '???']);
24427
+ * // Map { '!!!' => 'node-1', '???' => 'node-2' }
24428
+ * ```
24429
+ *
24430
+ * @param states States in declaration order.
24431
+ * @returns A `Map` from each state name to its unique slug.
24432
+ *
24433
+ * @internal
24434
+ */
24435
+ function slug_states(states) {
24436
+ const used = new Set();
24437
+ const out = new Map();
24438
+ states.forEach((s, i) => {
24439
+ const base = slug_for(s) || `node-${i + 1}`;
24440
+ let candidate = base;
24441
+ let n = 2;
24442
+ while (used.has(candidate)) {
24443
+ candidate = `${base}-${n}`;
24444
+ n += 1;
24445
+ }
24446
+ used.add(candidate);
24447
+ out.set(s, candidate);
24448
+ });
24449
+ return out;
24450
+ }
24451
+ /**
24452
+ * Build a graphviz-safe node identifier for a state. Accepts either a
24453
+ * `string[]` (legacy test-only path; returns an index-based `n0`/`n1`
24454
+ * identifier via `indexOf`), or a precomputed `Map<state, slug>` produced
24455
+ * by {@link slug_states} (used by all rendering hot paths).
24456
+ *
24457
+ * When a slug map is supplied, the identifier is the slug wrapped in
24458
+ * double quotes — dot allows quoted identifiers, and the slug alphabet
24459
+ * (lowercase alphanumerics + `-`) requires quoting because bare dot IDs
24460
+ * may not contain `-`. Graphviz round-trips the quoted form through to
24461
+ * the SVG `<title>` element and uses the slug as a stable basis for the
24462
+ * generated SVG element `id` attribute.
24463
+ *
24464
+ * ```typescript
24465
+ * node_of('Red Light', new Map([['Red Light', 'red-light']]));
24466
+ * // '"red-light"'
24467
+ * ```
24365
24468
  *
24366
24469
  * @internal
24367
24470
  */
24368
24471
  function node_of(state, state_index) {
24369
- return Array.isArray(state_index)
24370
- ? `n${state_index.indexOf(state)}`
24371
- : `n${state_index.get(state)}`;
24472
+ if (Array.isArray(state_index)) {
24473
+ return `n${state_index.indexOf(state)}`;
24474
+ }
24475
+ const v = state_index.get(state);
24476
+ if (typeof v === 'string') {
24477
+ return `"${v}"`;
24478
+ }
24479
+ return `n${v}`;
24372
24480
  }
24373
24481
  /**
24374
24482
  * Compose a graphviz `style` string from a pre-computed
@@ -24706,7 +24814,7 @@ function arranges_for(u_jssm, state_index) {
24706
24814
  function machine_to_dot(u_jssm, opts = {}) {
24707
24815
  var _a;
24708
24816
  const l_states = u_jssm.states();
24709
- const state_index = new Map(l_states.map((s, i) => [s, i]));
24817
+ const state_index = slug_states(l_states);
24710
24818
  const state_kinds = classify_states(u_jssm, l_states);
24711
24819
  const nodes = states_to_nodes_string(u_jssm, l_states, state_index, state_kinds, opts.hide_state_labels === true);
24712
24820
  const edges = states_to_edges_string(u_jssm, l_states, state_index, state_kinds);