jssm 5.162.6 → 5.162.8
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 +7 -7
- package/custom-elements.json +47 -47
- package/dist/cdn/instance.js +5 -3
- package/dist/cdn/viz.js +1 -1
- package/dist/cli/fsl-export-system-prompt.cjs +27 -24
- package/dist/cli/fsl-render.cjs +1 -1
- package/dist/cli/fsl.cjs +1 -1
- package/dist/cli/lib.cjs +1 -1
- package/dist/cli/lib.mjs +1 -1
- package/dist/cm6/fsl_language.js +10 -14
- package/dist/deno/README.md +7 -7
- package/dist/deno/fence.d.ts +0 -1
- package/dist/deno/fsl_fence_highlight.d.ts +0 -4
- package/dist/deno/fsl_fence_render.d.ts +0 -8
- package/dist/deno/fsl_gif.d.ts +0 -8
- package/dist/deno/fsl_markdown_fence.d.ts +0 -5
- package/dist/deno/fsl_svg_patch.d.ts +0 -6
- package/dist/deno/fsl_walk.d.ts +0 -2
- package/dist/deno/jssm.d.ts +214 -503
- package/dist/deno/jssm.js +1 -1
- package/dist/deno/jssm_compiler.d.ts +1 -1
- package/dist/deno/jssm_constants.d.ts +0 -3
- package/dist/deno/jssm_intern.d.ts +0 -15
- package/dist/deno/jssm_theme.d.ts +2 -2
- package/dist/deno/jssm_types.d.ts +13 -34
- package/dist/deno/jssm_util.d.ts +20 -6
- package/dist/deno/jssm_viz.d.ts +5 -51
- package/dist/es6/cm6/fsl_language.d.ts +0 -2
- package/dist/fence/fence.js +1664 -1830
- package/dist/jssm.es5.cjs +1 -1
- package/dist/jssm.es5.iife.js +1 -1
- package/dist/jssm.es6.mjs +1 -1
- package/dist/jssm_viz.cjs +1 -1
- package/dist/jssm_viz.iife.cjs +1 -1
- package/dist/jssm_viz.mjs +1 -1
- package/dist/wc/docs.define.js +0 -5
- package/dist/wc/docs.js +79 -43
- package/dist/wc/editor.define.js +0 -5
- package/dist/wc/editor.js +1 -7
- package/dist/wc/instance.define.js +0 -6
- package/dist/wc/instance.js +93 -115
- package/dist/wc/viz.define.js +0 -6
- package/dist/wc/viz.js +22 -36
- package/dist/wc/widgets.define.js +0 -5
- package/dist/wc/widgets.js +117 -95
- package/jssm.cli.d.cts +0 -8
- package/jssm.cli.d.ts +0 -8
- package/jssm.es5.d.cts +449 -808
- package/jssm.es6.d.ts +449 -808
- package/jssm.fence.d.ts +213 -508
- package/jssm_viz.es5.d.cts +216 -529
- package/jssm_viz.es6.d.ts +216 -529
- package/package.json +16 -10
package/dist/wc/widgets.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { css, html, LitElement } from 'lit';
|
|
2
2
|
import { state, property } from 'lit/decorators.js';
|
|
3
|
-
import {
|
|
3
|
+
import { machine_to_svg_string, machine_to_dot } from 'jssm/viz';
|
|
4
4
|
import { STOCHASTIC_DEFAULT_RUNS, STOCHASTIC_DEFAULT_MAX_STEPS, sm } from 'jssm';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -49,11 +49,9 @@ const fslTokens = css `
|
|
|
49
49
|
/**
|
|
50
50
|
* Returns true when `tag_name` is exactly `fsl-<suffix>` or `jssm-<suffix>`
|
|
51
51
|
* (case-insensitive).
|
|
52
|
-
*
|
|
53
52
|
* @param tag_name - The element tag name to test (e.g. `"FSL-VIZ"`, `"jssm-viz"`).
|
|
54
53
|
* @param suffix - The suffix to match after the prefix (e.g. `"viz"`).
|
|
55
54
|
* @returns `true` when `tag_name` is `fsl-<suffix>` or `jssm-<suffix>`.
|
|
56
|
-
*
|
|
57
55
|
* @example
|
|
58
56
|
* wc_suffix_matches('FSL-VIZ', 'viz'); // true
|
|
59
57
|
* wc_suffix_matches('jssm-viz', 'viz'); // true
|
|
@@ -63,15 +61,12 @@ const fslTokens = css `
|
|
|
63
61
|
/**
|
|
64
62
|
* Returns the nearest ancestor of `el` (or `el` itself) whose tag is
|
|
65
63
|
* `fsl-<suffix>` or `jssm-<suffix>`, or `null` if none exists.
|
|
66
|
-
*
|
|
67
64
|
* @param el - The element to start the search from.
|
|
68
65
|
* @param suffix - The suffix to match (e.g. `"instance"`).
|
|
69
66
|
* @returns The closest matching ancestor element, or `null`.
|
|
70
|
-
*
|
|
71
67
|
* @example
|
|
72
68
|
* // <fsl-instance><div id="k"></div></fsl-instance>
|
|
73
69
|
* closest_wc(document.getElementById('k'), 'instance'); // <fsl-instance>
|
|
74
|
-
*
|
|
75
70
|
* @see wc_suffix_matches
|
|
76
71
|
*/
|
|
77
72
|
function closest_wc(el, suffix) {
|
|
@@ -90,7 +85,6 @@ function closest_wc(el, suffix) {
|
|
|
90
85
|
*/
|
|
91
86
|
/**
|
|
92
87
|
* Default fragment key for the single-machine case (back-compat with 5.150).
|
|
93
|
-
*
|
|
94
88
|
* @example
|
|
95
89
|
* DEFAULT_PERMALINK_KEY; // 'm'
|
|
96
90
|
*/
|
|
@@ -98,7 +92,6 @@ const DEFAULT_PERMALINK_KEY = 'm';
|
|
|
98
92
|
/**
|
|
99
93
|
* URL-safe base64 (RFC 4648 §5) of raw bytes: standard base64 with `+`→`-`,
|
|
100
94
|
* `/`→`_`, and trailing `=` padding stripped.
|
|
101
|
-
*
|
|
102
95
|
* @example
|
|
103
96
|
* bytes_to_base64url(new TextEncoder().encode("a")); // "YQ"
|
|
104
97
|
*/
|
|
@@ -111,7 +104,6 @@ function bytes_to_base64url(bytes) {
|
|
|
111
104
|
}
|
|
112
105
|
/**
|
|
113
106
|
* DEFLATE `bytes` (raw, headerless) via the platform `CompressionStream`.
|
|
114
|
-
*
|
|
115
107
|
* @example
|
|
116
108
|
* await deflate_raw(new TextEncoder().encode("aaaaaaaa")); // shorter Uint8Array of raw DEFLATE bytes
|
|
117
109
|
*/
|
|
@@ -129,7 +121,6 @@ async function deflate_raw(bytes) {
|
|
|
129
121
|
* Encode FSL to a `<scheme><payload>` segment value (the part after `key=`).
|
|
130
122
|
* DEFLATE is used (scheme `1`) only when it is strictly shorter than the raw
|
|
131
123
|
* bytes (scheme `0`).
|
|
132
|
-
*
|
|
133
124
|
* @example
|
|
134
125
|
* await encode_machine("a -> b;"); // "0YSAtPiBiOw"
|
|
135
126
|
*/
|
|
@@ -139,8 +130,10 @@ async function encode_machine(fsl) {
|
|
|
139
130
|
const deflated = bytes_to_base64url(await deflate_raw(utf8));
|
|
140
131
|
return deflated.length < raw.length ? `1${deflated}` : `0${raw}`;
|
|
141
132
|
}
|
|
142
|
-
/**
|
|
143
|
-
*
|
|
133
|
+
/**
|
|
134
|
+
* `decodeURIComponent` that returns its input untouched on a malformed escape,
|
|
135
|
+
* so a hand-mangled fragment never throws out of {@link read_fragment_param}.
|
|
136
|
+
*/
|
|
144
137
|
function safe_decode(text) {
|
|
145
138
|
try {
|
|
146
139
|
return decodeURIComponent(text);
|
|
@@ -166,7 +159,6 @@ function fragment_pairs(hash) {
|
|
|
166
159
|
/**
|
|
167
160
|
* Return a new fragment body (no leading `#`) with `key`'s segment set to
|
|
168
161
|
* `value`, preserving every other segment and its order; appends if absent.
|
|
169
|
-
*
|
|
170
162
|
* @example
|
|
171
163
|
* set_fragment_param('#a=0AAA', 'b', '1BBB'); // "a=0AAA&b=1BBB"
|
|
172
164
|
*/
|
|
@@ -187,7 +179,6 @@ function set_fragment_param(hash, key, value) {
|
|
|
187
179
|
* The fragment key an element owns: its `uhash` attribute if set, else its
|
|
188
180
|
* `id`, else `null` (does not participate in URL sync). The single source of
|
|
189
181
|
* this rule, shared by the toolbar export and the sync controller.
|
|
190
|
-
*
|
|
191
182
|
* @example
|
|
192
183
|
* permalink_key_for(el); // "myId" (when <el id="myId">, no uhash)
|
|
193
184
|
*/
|
|
@@ -199,18 +190,15 @@ function permalink_key_for(host) {
|
|
|
199
190
|
* A shareable URL for `fsl` under `key`, merging into `currentHash` so sibling
|
|
200
191
|
* machines' segments survive. Browser-defaulted (`location`) but injectable for
|
|
201
192
|
* tests.
|
|
202
|
-
*
|
|
203
193
|
* @returns The absolute URL carrying the merged fragment.
|
|
204
|
-
*
|
|
205
194
|
* @example
|
|
206
195
|
* await permalink_for('a -> b;', 'm', 'https://h/p', ''); // "https://h/p#m=0YSAtPiBiOw"
|
|
207
|
-
*
|
|
208
196
|
* @see fsl_from_permalink
|
|
209
197
|
*/
|
|
210
198
|
async function permalink_for(fsl, key = DEFAULT_PERMALINK_KEY, href = location.href, currentHash = location.hash) {
|
|
211
199
|
const segment = await encode_machine(fsl);
|
|
212
200
|
const fragment = set_fragment_param(currentHash, key, segment);
|
|
213
|
-
return `${href.split('#')[0]}#${fragment}`;
|
|
201
|
+
return `${href.split('#', 1)[0]}#${fragment}`;
|
|
214
202
|
}
|
|
215
203
|
|
|
216
204
|
var __decorate$8 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
@@ -234,11 +222,12 @@ const EXPORT_FORMATS = [
|
|
|
234
222
|
{ value: 'permalink', label: 'Permalink (URL)' },
|
|
235
223
|
{ value: 'embed', label: 'Embed snippet' },
|
|
236
224
|
];
|
|
225
|
+
// The permalink codec lives in its own module (`fsl_permalink.ts`); re-export so
|
|
226
|
+
// existing importers — and the 5.151 toolbar tests — keep resolving these here.
|
|
237
227
|
/**
|
|
238
228
|
* A paste-able HTML snippet that renders the given FSL from the CDN builds: an
|
|
239
229
|
* `<fsl-instance>` reading its source from a `<script type="text/fsl">` child,
|
|
240
230
|
* with a slotted `<fsl-viz>` for the graph.
|
|
241
|
-
*
|
|
242
231
|
* @example
|
|
243
232
|
* embed_snippet_for("a -> b;"); // "<script …instance.js …><fsl-instance>…</fsl-instance>"
|
|
244
233
|
*/
|
|
@@ -313,7 +302,6 @@ const LAYOUTS = [
|
|
|
313
302
|
* Layout / Export / Theme pulldowns (the Layout button shows the current
|
|
314
303
|
* layout's icon). Standalone (no host) the host-dependent controls disappear.
|
|
315
304
|
* A trailing slot carries extra buttons.
|
|
316
|
-
*
|
|
317
305
|
* @element fsl-toolbar
|
|
318
306
|
* @csspart toolbar - The bar container.
|
|
319
307
|
* @slot - Trailing custom controls.
|
|
@@ -342,18 +330,23 @@ class FslToolbar extends LitElement {
|
|
|
342
330
|
super.connectedCallback();
|
|
343
331
|
const host = closest_wc(this, 'instance');
|
|
344
332
|
this._host = host;
|
|
333
|
+
// eslint-disable-next-line unicorn/require-css-escape -- jsdom provides no CSS global at all and this component must run there; PANELS slot names are static identifiers needing no escaping
|
|
345
334
|
this._present = host === null ? [] : PANELS.filter(p => host.querySelector(`[slot="${p.slot}"]`) !== null);
|
|
346
335
|
}
|
|
347
|
-
/**
|
|
348
|
-
*
|
|
336
|
+
/**
|
|
337
|
+
* Set the theme mode (System/Light/Dark). The host applies the palette + drives
|
|
338
|
+
* the editor; the menu stays open so a theme can be picked in the same trip.
|
|
339
|
+
*/
|
|
349
340
|
_setMode(mode) {
|
|
350
341
|
if (this._host !== null) {
|
|
351
342
|
this._host.theme = mode;
|
|
352
343
|
}
|
|
353
344
|
this.requestUpdate();
|
|
354
345
|
}
|
|
355
|
-
/**
|
|
356
|
-
*
|
|
346
|
+
/**
|
|
347
|
+
* Select a named theme from the host's registry. The theme-name buttons only
|
|
348
|
+
* render when a host exists, so `_host` is non-null here.
|
|
349
|
+
*/
|
|
357
350
|
_setThemeName(name) {
|
|
358
351
|
this._host.themeName = name;
|
|
359
352
|
this.requestUpdate();
|
|
@@ -364,11 +357,15 @@ class FslToolbar extends LitElement {
|
|
|
364
357
|
}
|
|
365
358
|
this._openMenu = '';
|
|
366
359
|
}
|
|
367
|
-
/**
|
|
368
|
-
*
|
|
360
|
+
/**
|
|
361
|
+
* Set the active export destination; the menu stays open so a format can be
|
|
362
|
+
* chosen next.
|
|
363
|
+
*/
|
|
369
364
|
_setDest(dest) { this._dest = dest; }
|
|
370
|
-
/**
|
|
371
|
-
*
|
|
365
|
+
/**
|
|
366
|
+
* Emit `fsl-export` with the chosen format's content + the active destination.
|
|
367
|
+
* The embedder performs the actual clipboard / file save.
|
|
368
|
+
*/
|
|
372
369
|
async _export(format) {
|
|
373
370
|
var _a;
|
|
374
371
|
const host = this._host;
|
|
@@ -378,30 +375,39 @@ class FslToolbar extends LitElement {
|
|
|
378
375
|
return;
|
|
379
376
|
}
|
|
380
377
|
let content;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
378
|
+
switch (format) {
|
|
379
|
+
case 'dot': {
|
|
380
|
+
content = machine_to_dot(host.machine);
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
case 'json': {
|
|
384
|
+
content = JSON.stringify(host.machine.serialize(), null, 2);
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
case 'svg': {
|
|
388
|
+
content = await machine_to_svg_string(host.machine);
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
case 'permalink': {
|
|
392
|
+
content = await permalink_for(host.fsl, (_a = permalink_key_for(host)) !== null && _a !== void 0 ? _a : undefined);
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
case 'embed': {
|
|
396
|
+
content = embed_snippet_for(host.fsl);
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
default: {
|
|
400
|
+
content = host.fsl;
|
|
401
|
+
}
|
|
398
402
|
}
|
|
399
403
|
this.dispatchEvent(new CustomEvent('fsl-export', { detail: { format, content, destination }, bubbles: true, composed: true }));
|
|
400
404
|
}
|
|
401
|
-
/**
|
|
405
|
+
/**
|
|
406
|
+
* Fire a workspace-action intent (validate / lint) for the consumer to
|
|
402
407
|
* fulfill — the toolbar presents the action; the embedder runs it. The
|
|
403
408
|
* current machine source rides along in the detail as a convenience. The
|
|
404
|
-
* buttons only render with a host, so `_host` is non-null here.
|
|
409
|
+
* buttons only render with a host, so `_host` is non-null here.
|
|
410
|
+
*/
|
|
405
411
|
_fireAction(type) {
|
|
406
412
|
this.dispatchEvent(new CustomEvent(type, { detail: { fsl: this._host.fsl }, bubbles: true, composed: true }));
|
|
407
413
|
}
|
|
@@ -420,24 +426,30 @@ class FslToolbar extends LitElement {
|
|
|
420
426
|
<div class="grp">
|
|
421
427
|
${host
|
|
422
428
|
? this._present.map(p => html `
|
|
423
|
-
|
|
424
|
-
|
|
429
|
+
<button class="tb icon" aria-pressed=${!host.isPanelHidden(p.slot)} aria-label=${p.label} title=${p.label}
|
|
430
|
+
@click=${() => { host.togglePanel(p.slot); this.requestUpdate(); }}>${p.icon}</button>
|
|
431
|
+
`)
|
|
425
432
|
: ''}
|
|
426
433
|
</div>
|
|
427
434
|
${host ? html `
|
|
428
435
|
<div class="grp">
|
|
429
436
|
${this.noValidate ? '' : html `
|
|
430
|
-
<button class="tb icon" aria-label="Validate" title="Validate" @click=${() => this._fireAction('fsl-validate')}>${ICON_VALIDATE}</button
|
|
437
|
+
<button class="tb icon" aria-label="Validate" title="Validate" @click=${() => this._fireAction('fsl-validate')}>${ICON_VALIDATE}</button>
|
|
438
|
+
`}
|
|
431
439
|
${this.noLint ? '' : html `
|
|
432
|
-
<button class="tb icon" aria-label="Lint" title="Lint" @click=${() => this._fireAction('fsl-lint')}>${ICON_LINT}</button
|
|
433
|
-
|
|
440
|
+
<button class="tb icon" aria-label="Lint" title="Lint" @click=${() => this._fireAction('fsl-lint')}>${ICON_LINT}</button>
|
|
441
|
+
`}
|
|
442
|
+
</div>
|
|
443
|
+
` : ''}
|
|
434
444
|
<div class="grp">
|
|
435
445
|
<button class="tb layout" aria-haspopup="true" aria-expanded=${this._openMenu === 'layout'} aria-label="Layout" title="Layout" @click=${() => this._toggleMenu('layout')}>${layoutIcon}<span class="caret">▾</span></button>
|
|
436
446
|
${this._openMenu === 'layout' ? html `
|
|
437
447
|
<div class="menu" role="menu">
|
|
438
448
|
${LAYOUTS.map(o => html `
|
|
439
|
-
<button role="menuitemradio" aria-checked=${layout === o.value} @click=${() => this._setLayout(o.value)}>${o.icon}${o.label}</button
|
|
440
|
-
|
|
449
|
+
<button role="menuitemradio" aria-checked=${layout === o.value} @click=${() => this._setLayout(o.value)}>${o.icon}${o.label}</button>
|
|
450
|
+
`)}
|
|
451
|
+
</div>
|
|
452
|
+
` : ''}
|
|
441
453
|
</div>
|
|
442
454
|
<div class="grp">
|
|
443
455
|
<button class="tb icon" aria-haspopup="true" aria-expanded=${this._openMenu === 'export'} aria-label="Export" title="Export" @click=${() => this._toggleMenu('export')}>${ICON_EXPORT}</button>
|
|
@@ -445,26 +457,33 @@ class FslToolbar extends LitElement {
|
|
|
445
457
|
<div class="menu" role="menu">
|
|
446
458
|
<button role="menuitemradio" aria-checked=${this._dest === 'clipboard'} @click=${() => this._setDest('clipboard')}>to clipboard</button>
|
|
447
459
|
${this.lastDirectory ? html `
|
|
448
|
-
<button role="menuitemradio" aria-checked=${this._dest === 'lastdir'} @click=${() => this._setDest('lastdir')}>to ${this.lastDirectory}</button
|
|
460
|
+
<button role="menuitemradio" aria-checked=${this._dest === 'lastdir'} @click=${() => this._setDest('lastdir')}>to ${this.lastDirectory}</button>
|
|
461
|
+
` : ''}
|
|
449
462
|
<button role="menuitemradio" aria-checked=${this._dest === 'pick'} @click=${() => this._setDest('pick')}>choose directory…</button>
|
|
450
463
|
<div class="divider" role="separator"></div>
|
|
451
464
|
${EXPORT_FORMATS.map(f => html `
|
|
452
|
-
<button role="menuitem" @click=${() => this._export(f.value)}>${f.label}</button
|
|
453
|
-
|
|
465
|
+
<button role="menuitem" @click=${() => this._export(f.value)}>${f.label}</button>
|
|
466
|
+
`)}
|
|
467
|
+
</div>
|
|
468
|
+
` : ''}
|
|
454
469
|
</div>
|
|
455
470
|
<div class="grp">
|
|
456
471
|
<button class="tb icon" aria-haspopup="true" aria-expanded=${this._openMenu === 'theme'} aria-label="Theme" title="Theme" @click=${() => this._toggleMenu('theme')}>${ICON_THEME}</button>
|
|
457
472
|
${this._openMenu === 'theme' ? html `
|
|
458
473
|
<div class="menu" role="menu">
|
|
459
474
|
${THEME_MODES.map(m => html `
|
|
460
|
-
<button role="menuitemradio" aria-checked=${mode === m.value} @click=${() => this._setMode(m.value)}>${m.label}</button
|
|
475
|
+
<button role="menuitemradio" aria-checked=${mode === m.value} @click=${() => this._setMode(m.value)}>${m.label}</button>
|
|
476
|
+
`)}
|
|
461
477
|
<div class="divider" role="separator"></div>
|
|
462
478
|
${themeNames.map(n => html `
|
|
463
|
-
<button role="menuitemradio" aria-checked=${themeName === n} @click=${() => this._setThemeName(n)}>${n}</button
|
|
464
|
-
|
|
479
|
+
<button role="menuitemradio" aria-checked=${themeName === n} @click=${() => this._setThemeName(n)}>${n}</button>
|
|
480
|
+
`)}
|
|
481
|
+
</div>
|
|
482
|
+
` : ''}
|
|
465
483
|
</div>
|
|
466
484
|
<slot></slot>
|
|
467
|
-
</div
|
|
485
|
+
</div>
|
|
486
|
+
`;
|
|
468
487
|
}
|
|
469
488
|
}
|
|
470
489
|
FslToolbar.styles = css `
|
|
@@ -550,10 +569,8 @@ const ACTION_EVENTS = ['fsl-transition', 'fsl-machine-rebuilt'];
|
|
|
550
569
|
* appear, and each group is omitted when empty, so a self-loop-only state shows
|
|
551
570
|
* just its actions and a terminal state shows `no actions available`. Standalone
|
|
552
571
|
* (no host ancestor) renders empty.
|
|
553
|
-
*
|
|
554
572
|
* @element fsl-actions
|
|
555
573
|
* @csspart actions - The container.
|
|
556
|
-
*
|
|
557
574
|
* @example
|
|
558
575
|
* // For `A 'x' -> B; A 'y' => C; A ~> D;` while in A:
|
|
559
576
|
* // Actions: [ x ] [ y ]
|
|
@@ -603,7 +620,7 @@ class FslActions extends LitElement {
|
|
|
603
620
|
if (e.from !== current || e.to === current) {
|
|
604
621
|
continue;
|
|
605
622
|
} // only non-self exits from here
|
|
606
|
-
const to =
|
|
623
|
+
const to = e.to;
|
|
607
624
|
if (e.main_path) {
|
|
608
625
|
main.add(to);
|
|
609
626
|
}
|
|
@@ -632,8 +649,10 @@ class FslActions extends LitElement {
|
|
|
632
649
|
}
|
|
633
650
|
else {
|
|
634
651
|
host.transition(to);
|
|
635
|
-
} }}>→ ${to}</button
|
|
636
|
-
|
|
652
|
+
} }}>→ ${to}</button>
|
|
653
|
+
`)}
|
|
654
|
+
</div>
|
|
655
|
+
`;
|
|
637
656
|
}
|
|
638
657
|
render() {
|
|
639
658
|
const host = this._host;
|
|
@@ -650,12 +669,15 @@ class FslActions extends LitElement {
|
|
|
650
669
|
<div class="group">
|
|
651
670
|
<div class="label">Actions</div>
|
|
652
671
|
${this._actions.map(a => html `
|
|
653
|
-
<button class="act" @click=${() => host.do(a)}>${a}</button
|
|
654
|
-
|
|
672
|
+
<button class="act" @click=${() => host.do(a)}>${a}</button>
|
|
673
|
+
`)}
|
|
674
|
+
</div>
|
|
675
|
+
`}
|
|
655
676
|
${this._group(host, 'Main', this._main, false)}
|
|
656
677
|
${this._group(host, 'Transitions', this._regular, false)}
|
|
657
678
|
${this._group(host, 'Forced', this._forced, true)}
|
|
658
|
-
</div
|
|
679
|
+
</div>
|
|
680
|
+
`;
|
|
659
681
|
}
|
|
660
682
|
}
|
|
661
683
|
FslActions.styles = css `
|
|
@@ -718,7 +740,6 @@ function plural(n, word) {
|
|
|
718
740
|
* `fsl-machine-rebuilt`, so the footer survives a live rebuild (#1387). A
|
|
719
741
|
* default slot carries embedder status. Standalone (no `<fsl-instance>`
|
|
720
742
|
* ancestor) it renders just the slot.
|
|
721
|
-
*
|
|
722
743
|
* @element fsl-footer
|
|
723
744
|
* @csspart bar - The footer bar container.
|
|
724
745
|
* @slot - Trailing custom status, right-aligned.
|
|
@@ -773,15 +794,17 @@ class FslFooter extends LitElement {
|
|
|
773
794
|
this._host = null;
|
|
774
795
|
}
|
|
775
796
|
}
|
|
776
|
-
/**
|
|
797
|
+
/**
|
|
798
|
+
* Recompute the machine-derived counts: local transitions out of the current
|
|
777
799
|
* state, and the global action / action-start / transition totals. Only
|
|
778
|
-
* called while a host is attached, so `_host` is non-null here.
|
|
800
|
+
* called while a host is attached, so `_host` is non-null here.
|
|
801
|
+
*/
|
|
779
802
|
_syncMachine() {
|
|
780
803
|
var _a;
|
|
781
804
|
const host = this._host;
|
|
782
805
|
const current = (_a = host.getAttribute('current-state')) !== null && _a !== void 0 ? _a : '';
|
|
783
806
|
const edges = host.machine.list_edges();
|
|
784
|
-
this._transitions = edges.filter(e =>
|
|
807
|
+
this._transitions = edges.filter(e => e.from === current).length;
|
|
785
808
|
const actionEdges = edges.filter(e => typeof e.action === 'string');
|
|
786
809
|
this._gStarts = actionEdges.length;
|
|
787
810
|
this._gActions = new Set(actionEdges.map(e => e.action)).size;
|
|
@@ -795,7 +818,8 @@ class FslFooter extends LitElement {
|
|
|
795
818
|
${this._complete ? html `<span class="badge">complete</span>` : ''}
|
|
796
819
|
<span class="spacer"></span>
|
|
797
820
|
<slot></slot>
|
|
798
|
-
</div
|
|
821
|
+
</div>
|
|
822
|
+
`;
|
|
799
823
|
}
|
|
800
824
|
}
|
|
801
825
|
FslFooter.styles = css `
|
|
@@ -848,7 +872,6 @@ var __decorate$5 = (undefined && undefined.__decorate) || function (decorators,
|
|
|
848
872
|
* `<details>` sections). Presentational and self-contained — it owns no machine
|
|
849
873
|
* binding. The reflected `open` attribute drives visibility, so embedders can
|
|
850
874
|
* animate it (e.g. a width transition on the host) purely from CSS.
|
|
851
|
-
*
|
|
852
875
|
* @element fsl-help
|
|
853
876
|
* @csspart drawer - The drawer container.
|
|
854
877
|
* @csspart head - The header bar.
|
|
@@ -878,7 +901,8 @@ class FslHelp extends LitElement {
|
|
|
878
901
|
<button class="close" part="close" @click=${this._onClose} aria-label="Close documentation" title="Close">×</button>
|
|
879
902
|
</div>
|
|
880
903
|
<div class="body" part="body"><slot></slot></div>
|
|
881
|
-
</aside
|
|
904
|
+
</aside>
|
|
905
|
+
`;
|
|
882
906
|
}
|
|
883
907
|
}
|
|
884
908
|
FslHelp.styles = css `
|
|
@@ -925,7 +949,6 @@ var __decorate$4 = (undefined && undefined.__decorate) || function (decorators,
|
|
|
925
949
|
* transition, #639) and records the host's reflected `current-state`, so it
|
|
926
950
|
* captures every transition and survives a live machine rebuild without a
|
|
927
951
|
* machine subscription. Standalone (no host ancestor) renders empty.
|
|
928
|
-
*
|
|
929
952
|
* @element fsl-history
|
|
930
953
|
* @csspart timeline - The timeline container.
|
|
931
954
|
*/
|
|
@@ -966,7 +989,8 @@ class FslHistory extends LitElement {
|
|
|
966
989
|
return html `
|
|
967
990
|
<div class="timeline" part="timeline">
|
|
968
991
|
${this._history.map((s, i) => html `${i > 0 ? html `<span class="arrow">→</span>` : ''}<span class="state ${i === last ? 'current' : ''}">${s}</span>`)}
|
|
969
|
-
</div
|
|
992
|
+
</div>
|
|
993
|
+
`;
|
|
970
994
|
}
|
|
971
995
|
}
|
|
972
996
|
FslHistory.styles = css `
|
|
@@ -1004,10 +1028,8 @@ const JSON_TOKEN_RE = /"(?:\\.|[^"\\])*"|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\
|
|
|
1004
1028
|
* `:`, otherwise a `string`; `true`/`false` are `bool`, `null` is `null`,
|
|
1005
1029
|
* numbers are `number`, and everything between (braces, commas, whitespace) is
|
|
1006
1030
|
* `plain`. Driven by the text, not a JSON parse, so it never throws.
|
|
1007
|
-
*
|
|
1008
1031
|
* @param json - A JSON string (typically `JSON.stringify(value, null, 2)`).
|
|
1009
1032
|
* @returns The tokens in source order; concatenating their `text` reproduces `json`.
|
|
1010
|
-
*
|
|
1011
1033
|
* @example
|
|
1012
1034
|
* tokenizeJson('{"a": 1}');
|
|
1013
1035
|
* // [{text:'{',kind:'plain'}, {text:'"a"',kind:'key'}, {text:': ',kind:'plain'},
|
|
@@ -1050,7 +1072,6 @@ function tokenizeJson(json) {
|
|
|
1050
1072
|
* host's transition / data-change / rebuild DOM events. The panel is bounded and
|
|
1051
1073
|
* scrolls internally (a self-contained vertical column). Renders `no data` when
|
|
1052
1074
|
* the machine carries none; standalone (no host) renders empty.
|
|
1053
|
-
*
|
|
1054
1075
|
* @element fsl-data-inspector
|
|
1055
1076
|
* @csspart inspector - The scrollable container.
|
|
1056
1077
|
*/
|
|
@@ -1088,7 +1109,8 @@ class FslDataInspector extends LitElement {
|
|
|
1088
1109
|
${this._data === undefined
|
|
1089
1110
|
? html `<span class="empty">no data</span>`
|
|
1090
1111
|
: html `<pre class="json">${tokenizeJson(JSON.stringify(this._data, null, 2)).map(t => t.kind === 'plain' ? t.text : html `<span class="tok-${t.kind}">${t.text}</span>`)}</pre>`}
|
|
1091
|
-
</div
|
|
1112
|
+
</div>
|
|
1113
|
+
`;
|
|
1092
1114
|
}
|
|
1093
1115
|
}
|
|
1094
1116
|
FslDataInspector.styles = css `
|
|
@@ -1128,7 +1150,6 @@ const LOGGED_EVENTS = [
|
|
|
1128
1150
|
* `<fsl-hook-log>` — a running log of a parent `<fsl-instance>`'s machine
|
|
1129
1151
|
* events, listening to the host's re-emitted `fsl-*` DOM events (#639). Keeps
|
|
1130
1152
|
* the most recent {@link MAX_ENTRIES}. Standalone (no host ancestor) is empty.
|
|
1131
|
-
*
|
|
1132
1153
|
* @element fsl-hook-log
|
|
1133
1154
|
* @csspart log - The log container.
|
|
1134
1155
|
*/
|
|
@@ -1166,7 +1187,8 @@ class FslHookLog extends LitElement {
|
|
|
1166
1187
|
${this._log.length === 0
|
|
1167
1188
|
? html `<span class="empty">no events</span>`
|
|
1168
1189
|
: this._log.map(name => html `<div class="entry">${name}</div>`)}
|
|
1169
|
-
</div
|
|
1190
|
+
</div>
|
|
1191
|
+
`;
|
|
1170
1192
|
}
|
|
1171
1193
|
}
|
|
1172
1194
|
FslHookLog.styles = css `
|
|
@@ -1200,7 +1222,6 @@ var __decorate$1 = (undefined && undefined.__decorate) || function (decorators,
|
|
|
1200
1222
|
* {@link FslSimulation.interval} ms and stops automatically when the machine
|
|
1201
1223
|
* reaches a terminal state (no legal actions). Standalone (no host ancestor)
|
|
1202
1224
|
* the controls are disabled.
|
|
1203
|
-
*
|
|
1204
1225
|
* @element fsl-simulation
|
|
1205
1226
|
* @csspart sim - The control row.
|
|
1206
1227
|
* @attr {number} interval - Auto-step period in milliseconds (default 600).
|
|
@@ -1261,7 +1282,8 @@ class FslSimulation extends LitElement {
|
|
|
1261
1282
|
<button class="btn" @click=${this._step}>Step</button>
|
|
1262
1283
|
<button class="btn" @click=${this._toggle}>${this._running ? 'Pause' : 'Play'}</button>
|
|
1263
1284
|
<span class="count ${this._host === null ? 'idle' : ''}">${this._steps} step${this._steps === 1 ? '' : 's'}</span>
|
|
1264
|
-
</div
|
|
1285
|
+
</div>
|
|
1286
|
+
`;
|
|
1265
1287
|
}
|
|
1266
1288
|
}
|
|
1267
1289
|
FslSimulation.styles = css `
|
|
@@ -1297,7 +1319,6 @@ __decorate$1([
|
|
|
1297
1319
|
* content }`; the embedder decides what to do with it (copy, download, show).
|
|
1298
1320
|
* Formats: Graphviz `dot` (via `machine_to_dot`), `json` (the machine's
|
|
1299
1321
|
* `serialize()`), and `fsl` (the source). Standalone is inert.
|
|
1300
|
-
*
|
|
1301
1322
|
* @element fsl-export
|
|
1302
1323
|
* @csspart export - The button row.
|
|
1303
1324
|
* @fires {CustomEvent<{format: FslExportFormat, content: string}>} fsl-export
|
|
@@ -1334,7 +1355,8 @@ class FslExport extends LitElement {
|
|
|
1334
1355
|
<button class="btn" @click=${() => this._emit('dot')}>DOT</button>
|
|
1335
1356
|
<button class="btn" @click=${() => this._emit('json')}>JSON</button>
|
|
1336
1357
|
<button class="btn" @click=${() => this._emit('fsl')}>FSL</button>
|
|
1337
|
-
</div
|
|
1358
|
+
</div>
|
|
1359
|
+
`;
|
|
1338
1360
|
}
|
|
1339
1361
|
}
|
|
1340
1362
|
FslExport.styles = css `
|
|
@@ -1366,7 +1388,6 @@ var __decorate = (undefined && undefined.__decorate) || function (decorators, ta
|
|
|
1366
1388
|
* host's `.fsl` source (never touching the live machine) and renders
|
|
1367
1389
|
* aggregate run statistics in-panel. Standalone (no host) the controls are
|
|
1368
1390
|
* disabled.
|
|
1369
|
-
*
|
|
1370
1391
|
* @element fsl-stochastic
|
|
1371
1392
|
* @csspart controls - The control row.
|
|
1372
1393
|
* @fires fsl-stochastic-complete - CustomEvent<JssmStochasticSummary> after a run.
|
|
@@ -1415,7 +1436,6 @@ class FslStochastic extends LitElement {
|
|
|
1415
1436
|
*
|
|
1416
1437
|
* Falls back to immediate (synchronous chunk) scheduling under jsdom where
|
|
1417
1438
|
* `requestAnimationFrame` is undefined.
|
|
1418
|
-
*
|
|
1419
1439
|
* @example
|
|
1420
1440
|
* panel.runs = 100;
|
|
1421
1441
|
* await panel.play(); // resolves when all 100 runs are done
|
|
@@ -1534,7 +1554,6 @@ class FslStochastic extends LitElement {
|
|
|
1534
1554
|
/**
|
|
1535
1555
|
* Fold accumulated counters into a rendered summary. Shared by {@link play}
|
|
1536
1556
|
* for incremental rendering during animation.
|
|
1537
|
-
*
|
|
1538
1557
|
* @param state_visits - Accumulated visit counts per state name.
|
|
1539
1558
|
* @param edge_traversals - Accumulated traversal counts per edge key.
|
|
1540
1559
|
* @param path_lengths - Lengths of completed (terminated) paths.
|
|
@@ -1562,13 +1581,14 @@ class FslStochastic extends LitElement {
|
|
|
1562
1581
|
}
|
|
1563
1582
|
_bars() {
|
|
1564
1583
|
const frac = this._summary.state_visit_fraction;
|
|
1565
|
-
const rows = [...frac
|
|
1584
|
+
const rows = [...frac].sort((a, b) => b[1] - a[1]);
|
|
1566
1585
|
return html `${rows.map(([name, f]) => html `
|
|
1567
1586
|
<div class="bar-row">
|
|
1568
1587
|
<span>${name}</span>
|
|
1569
1588
|
<span class="track"><span class="bar" style="width:${(f * 100).toFixed(1)}%"></span></span>
|
|
1570
1589
|
<span>${(f * 100).toFixed(1)}%</span>
|
|
1571
|
-
</div
|
|
1590
|
+
</div>
|
|
1591
|
+
`)}`;
|
|
1572
1592
|
}
|
|
1573
1593
|
_panes() {
|
|
1574
1594
|
const s = this._summary;
|
|
@@ -1580,7 +1600,8 @@ class FslStochastic extends LitElement {
|
|
|
1580
1600
|
<div><strong>State visits</strong></div>
|
|
1581
1601
|
${this._bars()}
|
|
1582
1602
|
${mc ? html `<div>Reached terminal: ${reached}% · Hit cap: ${capped}%</div>` : html `<div class="muted">steady-state distribution</div>`}
|
|
1583
|
-
</div
|
|
1603
|
+
</div>
|
|
1604
|
+
`;
|
|
1584
1605
|
}
|
|
1585
1606
|
render() {
|
|
1586
1607
|
const disabled = this._host === null;
|
|
@@ -1599,7 +1620,8 @@ class FslStochastic extends LitElement {
|
|
|
1599
1620
|
<button class="btn" ?disabled=${disabled} @click=${this._togglePlay}>${this._playing ? 'Pause' : 'Play'}</button>
|
|
1600
1621
|
</div>
|
|
1601
1622
|
${this._error ? html `<div class="error">${this._error}</div>` : ''}
|
|
1602
|
-
${this._summary && !this._error ? this._panes() : html `<div class="panes muted">No run yet.</div>`}
|
|
1623
|
+
${this._summary && !this._error ? this._panes() : html `<div class="panes muted">No run yet.</div>`}
|
|
1624
|
+
`;
|
|
1603
1625
|
}
|
|
1604
1626
|
}
|
|
1605
1627
|
FslStochastic.styles = css `
|
package/jssm.cli.d.cts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* `--target` enum derive from it, so a new target is declared in exactly one
|
|
6
6
|
* place. Future targets (mermaid, plantuml, scxml, ascii, fsl) land here in
|
|
7
7
|
* v0.2+.
|
|
8
|
-
*
|
|
9
8
|
* @example
|
|
10
9
|
* RENDER_TARGETS.includes('gif' as RenderTarget); // true
|
|
11
10
|
*/
|
|
@@ -81,7 +80,6 @@ declare class RasterizationUnsupportedError extends RenderError {
|
|
|
81
80
|
* Returns a discriminated union: `kind: 'text'` for SVG / DOT / HTML, and
|
|
82
81
|
* `kind: 'raster'` for PNG / JPEG / GIF. Use `kind` to narrow before
|
|
83
82
|
* accessing `content` or `buffer`.
|
|
84
|
-
*
|
|
85
83
|
* @param fsl - FSL source text
|
|
86
84
|
* @param opts.target - Output format ('svg' | 'dot' | 'png' | 'jpeg' | 'html' | 'gif')
|
|
87
85
|
* @param opts.width - Fit raster output to this pixel width (raster only)
|
|
@@ -93,7 +91,6 @@ declare class RasterizationUnsupportedError extends RenderError {
|
|
|
93
91
|
* @returns RenderResult, discriminated by `kind`
|
|
94
92
|
* @throws RenderError on parse, render, or target-dispatch failures
|
|
95
93
|
* @throws RasterizationUnsupportedError on raster targets where no backend exists
|
|
96
|
-
*
|
|
97
94
|
* @example
|
|
98
95
|
* const r = await render(fslText, { target: 'svg' });
|
|
99
96
|
* if (r.kind === 'text') console.log(r.content);
|
|
@@ -115,11 +112,9 @@ type RenderSetItem = RenderSetItemOk | RenderSetItemErr;
|
|
|
115
112
|
* Render multiple FSL source strings in parallel, returning one result
|
|
116
113
|
* per input. Errors are captured per-input rather than aborting the whole
|
|
117
114
|
* batch: callers can inspect which inputs succeeded and which failed.
|
|
118
|
-
*
|
|
119
115
|
* @param inputs - Array of FSL source strings
|
|
120
116
|
* @param opts - Render options applied to every input
|
|
121
117
|
* @returns Array of per-input results, same length and order as `inputs`
|
|
122
|
-
*
|
|
123
118
|
* @example
|
|
124
119
|
* const results = await renderSet([fsl1, fsl2], { target: 'svg' });
|
|
125
120
|
* for (const item of results) {
|
|
@@ -158,14 +153,11 @@ interface ParseResult<S extends ParseSpec> {
|
|
|
158
153
|
* -b boolean short flag
|
|
159
154
|
* -- terminate flag parsing; remaining args are positional
|
|
160
155
|
* - positional (stdin sentinel)
|
|
161
|
-
*
|
|
162
156
|
* @param argv - The argument array to parse (e.g. process.argv.slice(2))
|
|
163
157
|
* @param spec - The flag specification describing accepted flags, their types, and defaults
|
|
164
158
|
* @returns A ParseResult containing positional args, parsed flag values, and a helpText() generator
|
|
165
|
-
*
|
|
166
159
|
* @throws Error if an unknown flag is seen, an enum value mismatches,
|
|
167
160
|
* or a numeric flag receives a non-numeric value.
|
|
168
|
-
*
|
|
169
161
|
* @example
|
|
170
162
|
* ```ts
|
|
171
163
|
* const spec = {
|