jssm 5.155.0 → 5.156.0
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 +53 -1
- package/dist/cdn/instance.js +65 -7
- package/dist/cdn/viz.js +135 -1
- package/dist/cli/fsl-export-system-prompt.cjs +1 -1
- 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/deno/README.md +7 -7
- package/dist/deno/jssm.js +1 -1
- 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/instance.js +64 -6
- package/dist/wc/viz.js +134 -0
- package/dist/wc/widgets.js +20 -4
- package/package.json +1 -1
package/dist/wc/instance.js
CHANGED
|
@@ -521,7 +521,19 @@ async function deflate_raw(bytes) {
|
|
|
521
521
|
return new Uint8Array(await new Response(stream.readable).arrayBuffer());
|
|
522
522
|
}
|
|
523
523
|
/**
|
|
524
|
-
*
|
|
524
|
+
* Hard ceiling on the inflated size of a permalink, in bytes. A permalink rides
|
|
525
|
+
* in a URL an attacker can hand a victim, and {@link inflate_raw} runs on it
|
|
526
|
+
* automatically on page load, so an uncapped inflate is a decompression-bomb
|
|
527
|
+
* vector (a tiny `#m=…` could expand to hundreds of MB and OOM the tab). This is
|
|
528
|
+
* generous for real FSL (text — even a vast machine is well under a megabyte).
|
|
529
|
+
*/
|
|
530
|
+
const MAX_PERMALINK_INFLATE_BYTES = 5 * 1024 * 1024;
|
|
531
|
+
/**
|
|
532
|
+
* Inverse of {@link deflate_raw}, reading the stream in chunks and aborting once
|
|
533
|
+
* the inflated output would exceed {@link MAX_PERMALINK_INFLATE_BYTES} (a
|
|
534
|
+
* decompression-bomb guard — see that constant).
|
|
535
|
+
*
|
|
536
|
+
* @throws RangeError when the inflated output exceeds the cap.
|
|
525
537
|
*
|
|
526
538
|
* @example
|
|
527
539
|
* new TextDecoder().decode(await inflate_raw(await deflate_raw(new TextEncoder().encode("hi")))); // "hi"
|
|
@@ -531,7 +543,30 @@ async function inflate_raw(bytes) {
|
|
|
531
543
|
const writer = stream.writable.getWriter();
|
|
532
544
|
void writer.write(bytes);
|
|
533
545
|
void writer.close();
|
|
534
|
-
|
|
546
|
+
// Read incrementally; stopping past the cap leaves the stream half-drained, so
|
|
547
|
+
// backpressure halts further inflation and the abandoned stream is GC'd. We do
|
|
548
|
+
// not cancel (which would abort the writable and leak an unhandled rejection).
|
|
549
|
+
const reader = stream.readable.getReader();
|
|
550
|
+
const chunks = [];
|
|
551
|
+
let total = 0;
|
|
552
|
+
for (;;) {
|
|
553
|
+
const { done, value } = await reader.read();
|
|
554
|
+
if (done) {
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
total += value.length;
|
|
558
|
+
if (total > MAX_PERMALINK_INFLATE_BYTES) {
|
|
559
|
+
throw new RangeError(`permalink inflate exceeded ${MAX_PERMALINK_INFLATE_BYTES} bytes`);
|
|
560
|
+
}
|
|
561
|
+
chunks.push(value);
|
|
562
|
+
}
|
|
563
|
+
const out = new Uint8Array(total);
|
|
564
|
+
let offset = 0;
|
|
565
|
+
for (const chunk of chunks) {
|
|
566
|
+
out.set(chunk, offset);
|
|
567
|
+
offset += chunk.length;
|
|
568
|
+
}
|
|
569
|
+
return out;
|
|
535
570
|
}
|
|
536
571
|
/**
|
|
537
572
|
* Encode FSL to a `<scheme><payload>` segment value (the part after `key=`).
|
|
@@ -560,14 +595,28 @@ async function decode_machine(segment) {
|
|
|
560
595
|
const plain = scheme === '1' ? await inflate_raw(bytes) : bytes;
|
|
561
596
|
return new TextDecoder().decode(plain);
|
|
562
597
|
}
|
|
563
|
-
/**
|
|
598
|
+
/** `decodeURIComponent` that returns its input untouched on a malformed escape,
|
|
599
|
+
* so a hand-mangled fragment never throws out of {@link read_fragment_param}. */
|
|
600
|
+
function safe_decode(text) {
|
|
601
|
+
try {
|
|
602
|
+
return decodeURIComponent(text);
|
|
603
|
+
}
|
|
604
|
+
catch (_a) {
|
|
605
|
+
return text;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Split a fragment (leading `#` optional) into `[key, value]` pairs, dropping
|
|
610
|
+
* empties. Keys are percent-decoded (they are percent-encoded on write by
|
|
611
|
+
* {@link set_fragment_param}); values are the URL-safe base64 payload as-is.
|
|
612
|
+
*/
|
|
564
613
|
function fragment_pairs(hash) {
|
|
565
614
|
const body = hash.startsWith('#') ? hash.slice(1) : hash;
|
|
566
615
|
return body.split('&').filter(Boolean).map(seg => {
|
|
567
616
|
const eq = seg.indexOf('=');
|
|
568
617
|
return eq === -1
|
|
569
|
-
? [seg, '']
|
|
570
|
-
: [seg.slice(0, eq), seg.slice(eq + 1)];
|
|
618
|
+
? [safe_decode(seg), '']
|
|
619
|
+
: [safe_decode(seg.slice(0, eq)), seg.slice(eq + 1)];
|
|
571
620
|
});
|
|
572
621
|
}
|
|
573
622
|
/**
|
|
@@ -598,7 +647,9 @@ function set_fragment_param(hash, key, value) {
|
|
|
598
647
|
else {
|
|
599
648
|
pairs[at] = [key, value];
|
|
600
649
|
}
|
|
601
|
-
|
|
650
|
+
// Percent-encode the key so an `id`/`uhash` containing `=`, `&`, or `#` cannot
|
|
651
|
+
// break segmentation or collide with a sibling. Values are URL-safe base64.
|
|
652
|
+
return pairs.map(([k, v]) => `${encodeURIComponent(k)}=${v}`).join('&');
|
|
602
653
|
}
|
|
603
654
|
/**
|
|
604
655
|
* The fragment key an element owns: its `uhash` attribute if set, else its
|
|
@@ -666,6 +717,13 @@ class FslPermalinkSync {
|
|
|
666
717
|
}
|
|
667
718
|
try {
|
|
668
719
|
const fsl = await decode_machine(segment);
|
|
720
|
+
// The decode is async; if the host was disconnected while it ran, drop the
|
|
721
|
+
// result rather than mutating a detached element (and triggering a stray
|
|
722
|
+
// rebuild on a later reconnect). A reconnect runs hostConnected → _restore
|
|
723
|
+
// afresh.
|
|
724
|
+
if (!this.host.isConnected) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
669
727
|
this._last = segment;
|
|
670
728
|
this.host.fsl = fsl;
|
|
671
729
|
}
|
package/dist/wc/viz.js
CHANGED
|
@@ -351,6 +351,125 @@ class FslViz extends LitElement {
|
|
|
351
351
|
render() {
|
|
352
352
|
return html `<div class="container">${unsafeHTML(this._svg)}</div>`;
|
|
353
353
|
}
|
|
354
|
+
/**
|
|
355
|
+
* Clears any active programmatic highlights from the rendered SVG,
|
|
356
|
+
* restoring every node and edge to its default Graphviz presentation.
|
|
357
|
+
*
|
|
358
|
+
* Removes only the inline `fill` / `stroke` / `opacity` overrides that
|
|
359
|
+
* {@link highlightTrace} installs; the SVG's own presentation attributes
|
|
360
|
+
* are untouched. Safe to call when nothing is highlighted, before the
|
|
361
|
+
* first render, or while detached — it no-ops if there is no rendered
|
|
362
|
+
* SVG to clear.
|
|
363
|
+
*
|
|
364
|
+
* ```typescript
|
|
365
|
+
* const viz = document.querySelector('fsl-viz');
|
|
366
|
+
* viz.highlightTrace(['a', 'b']);
|
|
367
|
+
* viz.clearHighlights(); // back to the default rendering
|
|
368
|
+
* ```
|
|
369
|
+
*
|
|
370
|
+
* @see highlightTrace
|
|
371
|
+
*/
|
|
372
|
+
clearHighlights() {
|
|
373
|
+
if (!this.shadowRoot) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const container = this.shadowRoot.querySelector('.container');
|
|
377
|
+
if (!container) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
// Remove the inline styles that override the SVG presentation attributes.
|
|
381
|
+
const elements = container.querySelectorAll('.node, .edge, .node *, .edge *');
|
|
382
|
+
elements.forEach(el => {
|
|
383
|
+
el.style.removeProperty('fill');
|
|
384
|
+
el.style.removeProperty('stroke');
|
|
385
|
+
el.style.removeProperty('opacity');
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Programmatically highlights one execution trace (a path of state names)
|
|
390
|
+
* through the rendered graph, optionally fading everything off the path.
|
|
391
|
+
*
|
|
392
|
+
* Matches nodes by their Graphviz `<title>` (the state name) and edges by
|
|
393
|
+
* the `from->to` title Graphviz emits, applying inline style overrides so
|
|
394
|
+
* the highlight composes over — and is reversible against — the default
|
|
395
|
+
* rendering (see {@link clearHighlights}, which this calls first). No-ops
|
|
396
|
+
* when detached, before the first render, or given an empty trace.
|
|
397
|
+
*
|
|
398
|
+
* ```typescript
|
|
399
|
+
* // Highlight a -> b -> c in green, without dimming the rest:
|
|
400
|
+
* viz.highlightTrace(['a', 'b', 'c'], { color: '#2e7d32', fadeOthers: false });
|
|
401
|
+
* ```
|
|
402
|
+
*
|
|
403
|
+
* @param trace Ordered state names describing the path (e.g.
|
|
404
|
+
* `['A', 'B', 'C']`). Consecutive pairs select the edges
|
|
405
|
+
* `A->B` and `B->C`. An empty array is a no-op.
|
|
406
|
+
* @param options Highlight styling; see {@link HighlightOptions}. Defaults
|
|
407
|
+
* to crimson with off-trace fading enabled.
|
|
408
|
+
*
|
|
409
|
+
* @see clearHighlights
|
|
410
|
+
* @see HighlightOptions
|
|
411
|
+
*/
|
|
412
|
+
highlightTrace(trace, options = {}) {
|
|
413
|
+
if (!this.shadowRoot) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const container = this.shadowRoot.querySelector('.container');
|
|
417
|
+
if (!container || trace.length === 0) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
this.clearHighlights();
|
|
421
|
+
const color = options.color || '#b71c1c'; // default: a distinct crimson
|
|
422
|
+
const fadeOthers = options.fadeOthers !== false; // default: true
|
|
423
|
+
const targetNodes = new Set();
|
|
424
|
+
const targetEdges = new Set();
|
|
425
|
+
for (let i = 0; i < trace.length; i++) {
|
|
426
|
+
targetNodes.add(trace[i]);
|
|
427
|
+
if (i < trace.length - 1) {
|
|
428
|
+
targetEdges.add(`${trace[i]}->${trace[i + 1]}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const allNodes = container.querySelectorAll('.node');
|
|
432
|
+
const allEdges = container.querySelectorAll('.edge');
|
|
433
|
+
// Graphviz escapes '->' inside <title> as '->'; DOM textContent
|
|
434
|
+
// usually decodes it, but normalize defensively (and drop quotes).
|
|
435
|
+
const unescapeTitle = (title) => title.replace(/-/g, '-').replace(/>/g, '>').replace(/"/g, '');
|
|
436
|
+
allNodes.forEach(node => {
|
|
437
|
+
const titleEl = node.querySelector('title');
|
|
438
|
+
const title = titleEl ? unescapeTitle(titleEl.textContent || '') : '';
|
|
439
|
+
if (targetNodes.has(title)) {
|
|
440
|
+
node.querySelectorAll('polygon, ellipse, path').forEach(shape => {
|
|
441
|
+
shape.style.stroke = color;
|
|
442
|
+
shape.style.strokeWidth = '2px';
|
|
443
|
+
});
|
|
444
|
+
node.querySelectorAll('text').forEach(text => {
|
|
445
|
+
text.style.fill = color;
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
else if (fadeOthers) {
|
|
449
|
+
node.style.opacity = '0.2';
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
allEdges.forEach(edge => {
|
|
453
|
+
const titleEl = edge.querySelector('title');
|
|
454
|
+
const title = titleEl ? unescapeTitle(titleEl.textContent || '') : '';
|
|
455
|
+
if (targetEdges.has(title)) {
|
|
456
|
+
// Recolour the edge line and its arrowhead. The edge's action label
|
|
457
|
+
// is intentionally left alone: reorder_svg_layers() hoists every edge
|
|
458
|
+
// <text> out of its group onto a top paint layer (so labels can't be
|
|
459
|
+
// overdrawn), where it carries no <title> to correlate back to this
|
|
460
|
+
// edge — so a state-name trace cannot reliably re-style it.
|
|
461
|
+
edge.querySelectorAll('path, polygon').forEach(shape => {
|
|
462
|
+
shape.style.stroke = color;
|
|
463
|
+
if (shape.tagName.toLowerCase() === 'polygon') {
|
|
464
|
+
shape.style.fill = color; // arrowheads
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
else if (fadeOthers) {
|
|
469
|
+
edge.style.opacity = '0.2';
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
}
|
|
354
473
|
}
|
|
355
474
|
FslViz.styles = css `
|
|
356
475
|
:host {
|
|
@@ -371,6 +490,21 @@ FslViz.styles = css `
|
|
|
371
490
|
width: 90%;
|
|
372
491
|
height: 90%;
|
|
373
492
|
}
|
|
493
|
+
|
|
494
|
+
/* Smoothly animate the inline style overrides applied by highlightTrace()
|
|
495
|
+
so a trace fades in/out rather than snapping. */
|
|
496
|
+
.container svg g.node path,
|
|
497
|
+
.container svg g.node polygon,
|
|
498
|
+
.container svg g.node ellipse,
|
|
499
|
+
.container svg g.edge path,
|
|
500
|
+
.container svg g.edge polygon {
|
|
501
|
+
transition: fill 0.3s ease, stroke 0.3s ease, opacity 0.3s ease;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.container svg g.node text,
|
|
505
|
+
.container svg g.edge text {
|
|
506
|
+
transition: fill 0.3s ease, opacity 0.3s ease;
|
|
507
|
+
}
|
|
374
508
|
`;
|
|
375
509
|
__decorate([
|
|
376
510
|
property({ type: String })
|
package/dist/wc/widgets.js
CHANGED
|
@@ -136,14 +136,28 @@ async function encode_machine(fsl) {
|
|
|
136
136
|
const deflated = bytes_to_base64url(await deflate_raw(utf8));
|
|
137
137
|
return deflated.length < raw.length ? `1${deflated}` : `0${raw}`;
|
|
138
138
|
}
|
|
139
|
-
/**
|
|
139
|
+
/** `decodeURIComponent` that returns its input untouched on a malformed escape,
|
|
140
|
+
* so a hand-mangled fragment never throws out of {@link read_fragment_param}. */
|
|
141
|
+
function safe_decode(text) {
|
|
142
|
+
try {
|
|
143
|
+
return decodeURIComponent(text);
|
|
144
|
+
}
|
|
145
|
+
catch (_a) {
|
|
146
|
+
return text;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Split a fragment (leading `#` optional) into `[key, value]` pairs, dropping
|
|
151
|
+
* empties. Keys are percent-decoded (they are percent-encoded on write by
|
|
152
|
+
* {@link set_fragment_param}); values are the URL-safe base64 payload as-is.
|
|
153
|
+
*/
|
|
140
154
|
function fragment_pairs(hash) {
|
|
141
155
|
const body = hash.startsWith('#') ? hash.slice(1) : hash;
|
|
142
156
|
return body.split('&').filter(Boolean).map(seg => {
|
|
143
157
|
const eq = seg.indexOf('=');
|
|
144
158
|
return eq === -1
|
|
145
|
-
? [seg, '']
|
|
146
|
-
: [seg.slice(0, eq), seg.slice(eq + 1)];
|
|
159
|
+
? [safe_decode(seg), '']
|
|
160
|
+
: [safe_decode(seg.slice(0, eq)), seg.slice(eq + 1)];
|
|
147
161
|
});
|
|
148
162
|
}
|
|
149
163
|
/**
|
|
@@ -162,7 +176,9 @@ function set_fragment_param(hash, key, value) {
|
|
|
162
176
|
else {
|
|
163
177
|
pairs[at] = [key, value];
|
|
164
178
|
}
|
|
165
|
-
|
|
179
|
+
// Percent-encode the key so an `id`/`uhash` containing `=`, `&`, or `#` cannot
|
|
180
|
+
// break segmentation or collide with a sibling. Values are URL-safe base64.
|
|
181
|
+
return pairs.map(([k, v]) => `${encodeURIComponent(k)}=${v}`).join('&');
|
|
166
182
|
}
|
|
167
183
|
/**
|
|
168
184
|
* The fragment key an element owns: its `uhash` attribute if set, else its
|