jssm 5.151.2 → 5.152.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.
@@ -461,6 +461,242 @@ function build_hook_descriptor(spec, wrapped) {
461
461
  return base;
462
462
  }
463
463
 
464
+ /**
465
+ * Compressed, URL-safe permalink wire format for FSL machines, shared by the
466
+ * toolbar's Export→Permalink and the per-instance URL sync controller.
467
+ *
468
+ * A machine is encoded to a `<scheme><payload>` segment: `<payload>` is URL-safe
469
+ * base64, `<scheme>` is `1` when DEFLATE shrank the source and `0` for the raw
470
+ * bytes when it did not (so a short machine's link never grows). Segments live in
471
+ * a URL fragment as `#<key>=<segment>` joined by `&`, so several machines can
472
+ * share one URL.
473
+ */
474
+ /**
475
+ * Default fragment key for the single-machine case (back-compat with 5.150).
476
+ *
477
+ * @example
478
+ * DEFAULT_PERMALINK_KEY; // 'm'
479
+ */
480
+ /**
481
+ * URL-safe base64 (RFC 4648 §5) of raw bytes: standard base64 with `+`→`-`,
482
+ * `/`→`_`, and trailing `=` padding stripped.
483
+ *
484
+ * @example
485
+ * bytes_to_base64url(new TextEncoder().encode("a")); // "YQ"
486
+ */
487
+ function bytes_to_base64url(bytes) {
488
+ let binary = '';
489
+ for (const byte of bytes) {
490
+ binary += String.fromCharCode(byte);
491
+ }
492
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
493
+ }
494
+ /**
495
+ * Inverse of {@link bytes_to_base64url}.
496
+ *
497
+ * @example
498
+ * new TextDecoder().decode(base64url_to_bytes("YQ")); // "a"
499
+ */
500
+ function base64url_to_bytes(text) {
501
+ const standard = text.replace(/-/g, '+').replace(/_/g, '/');
502
+ const padded = standard + '='.repeat((4 - (standard.length % 4)) % 4); // atob wants 4-aligned input
503
+ const binary = atob(padded);
504
+ const bytes = new Uint8Array(binary.length);
505
+ for (let i = 0; i < binary.length; i++) {
506
+ bytes[i] = binary.charCodeAt(i);
507
+ }
508
+ return bytes;
509
+ }
510
+ /**
511
+ * DEFLATE `bytes` (raw, headerless) via the platform `CompressionStream`.
512
+ *
513
+ * @example
514
+ * await deflate_raw(new TextEncoder().encode("aaaaaaaa")); // shorter Uint8Array of raw DEFLATE bytes
515
+ */
516
+ async function deflate_raw(bytes) {
517
+ const stream = new CompressionStream('deflate-raw');
518
+ const writer = stream.writable.getWriter();
519
+ void writer.write(bytes);
520
+ void writer.close();
521
+ return new Uint8Array(await new Response(stream.readable).arrayBuffer());
522
+ }
523
+ /**
524
+ * Inverse of {@link deflate_raw}.
525
+ *
526
+ * @example
527
+ * new TextDecoder().decode(await inflate_raw(await deflate_raw(new TextEncoder().encode("hi")))); // "hi"
528
+ */
529
+ async function inflate_raw(bytes) {
530
+ const stream = new DecompressionStream('deflate-raw');
531
+ const writer = stream.writable.getWriter();
532
+ void writer.write(bytes);
533
+ void writer.close();
534
+ return new Uint8Array(await new Response(stream.readable).arrayBuffer());
535
+ }
536
+ /**
537
+ * Encode FSL to a `<scheme><payload>` segment value (the part after `key=`).
538
+ * DEFLATE is used (scheme `1`) only when it is strictly shorter than the raw
539
+ * bytes (scheme `0`).
540
+ *
541
+ * @example
542
+ * await encode_machine("a -> b;"); // "0YSAtPiBiOw"
543
+ */
544
+ async function encode_machine(fsl) {
545
+ const utf8 = new TextEncoder().encode(fsl);
546
+ const raw = bytes_to_base64url(utf8);
547
+ const deflated = bytes_to_base64url(await deflate_raw(utf8));
548
+ return deflated.length < raw.length ? `1${deflated}` : `0${raw}`;
549
+ }
550
+ /**
551
+ * Inverse of {@link encode_machine}: decode a `<scheme><payload>` segment back
552
+ * to FSL. Async because inflate is async.
553
+ *
554
+ * @example
555
+ * await decode_machine("0YSAtPiBiOw"); // "a -> b;"
556
+ */
557
+ async function decode_machine(segment) {
558
+ const scheme = segment[0];
559
+ const bytes = base64url_to_bytes(segment.slice(1));
560
+ const plain = scheme === '1' ? await inflate_raw(bytes) : bytes;
561
+ return new TextDecoder().decode(plain);
562
+ }
563
+ /** Split a fragment (leading `#` optional) into `[key, value]` pairs, dropping empties. */
564
+ function fragment_pairs(hash) {
565
+ const body = hash.startsWith('#') ? hash.slice(1) : hash;
566
+ return body.split('&').filter(Boolean).map(seg => {
567
+ const eq = seg.indexOf('=');
568
+ return eq === -1
569
+ ? [seg, '']
570
+ : [seg.slice(0, eq), seg.slice(eq + 1)];
571
+ });
572
+ }
573
+ /**
574
+ * Read one segment's value out of a `#a=…&b=…` fragment.
575
+ *
576
+ * @returns The value, or `null` if `key` is absent.
577
+ *
578
+ * @example
579
+ * read_fragment_param('#a=0AAA&b=1BBB', 'b'); // "1BBB"
580
+ */
581
+ function read_fragment_param(hash, key) {
582
+ const found = fragment_pairs(hash).find(([k]) => k === key);
583
+ return found ? found[1] : null;
584
+ }
585
+ /**
586
+ * Return a new fragment body (no leading `#`) with `key`'s segment set to
587
+ * `value`, preserving every other segment and its order; appends if absent.
588
+ *
589
+ * @example
590
+ * set_fragment_param('#a=0AAA', 'b', '1BBB'); // "a=0AAA&b=1BBB"
591
+ */
592
+ function set_fragment_param(hash, key, value) {
593
+ const pairs = fragment_pairs(hash);
594
+ const at = pairs.findIndex(([k]) => k === key);
595
+ if (at === -1) {
596
+ pairs.push([key, value]);
597
+ }
598
+ else {
599
+ pairs[at] = [key, value];
600
+ }
601
+ return pairs.map(([k, v]) => `${k}=${v}`).join('&');
602
+ }
603
+ /**
604
+ * The fragment key an element owns: its `uhash` attribute if set, else its
605
+ * `id`, else `null` (does not participate in URL sync). The single source of
606
+ * this rule, shared by the toolbar export and the sync controller.
607
+ *
608
+ * @example
609
+ * permalink_key_for(el); // "myId" (when <el id="myId">, no uhash)
610
+ */
611
+ function permalink_key_for(host) {
612
+ var _a, _b;
613
+ return (_b = (_a = host.getAttribute('uhash')) !== null && _a !== void 0 ? _a : host.getAttribute('id')) !== null && _b !== void 0 ? _b : null;
614
+ }
615
+
616
+ /** Debounce before a live edit is written to the URL fragment. */
617
+ const PERMALINK_WRITE_DEBOUNCE_MS = 300;
618
+ /**
619
+ * Binds an `<fsl-instance>` to a segment of the URL fragment: restores from it
620
+ * on connect and writes back (debounced, via `history.replaceState`) whenever
621
+ * the machine is rebuilt. Inert when the host has no key ({@link permalink_key_for}
622
+ * returns `null`), so an `fsl-instance` without an `id`/`uhash` never touches
623
+ * `location`.
624
+ *
625
+ * Echo guard: `_last` holds the segment most recently read or written, so a
626
+ * restore→rebuild→write cycle and a self-induced `hashchange` are both no-ops.
627
+ *
628
+ * @example
629
+ * // In an element's constructor:
630
+ * new FslPermalinkSync(this); // reads <el id="k">'s #k=… on connect, writes it on edit
631
+ */
632
+ class FslPermalinkSync {
633
+ constructor(host) {
634
+ this.key = null;
635
+ this._last = null;
636
+ this._onRebuilt = () => { this._scheduleWrite(); };
637
+ this._onHashChange = () => { void this._restore(); };
638
+ this.host = host;
639
+ host.addController(this);
640
+ }
641
+ hostConnected() {
642
+ this.key = permalink_key_for(this.host);
643
+ if (this.key === null) {
644
+ return;
645
+ }
646
+ void this._restore();
647
+ this.host.addEventListener('fsl-machine-rebuilt', this._onRebuilt);
648
+ window.addEventListener('hashchange', this._onHashChange);
649
+ }
650
+ hostDisconnected() {
651
+ if (this.key === null) {
652
+ return;
653
+ }
654
+ this.host.removeEventListener('fsl-machine-rebuilt', this._onRebuilt);
655
+ window.removeEventListener('hashchange', this._onHashChange);
656
+ if (this._timer !== undefined) {
657
+ clearTimeout(this._timer);
658
+ this._timer = undefined;
659
+ }
660
+ }
661
+ /** Read our segment and, if new, push it into the host (overriding declared source). */
662
+ async _restore() {
663
+ const segment = read_fragment_param(location.hash, this.key);
664
+ if (segment === null || segment === this._last) {
665
+ return;
666
+ }
667
+ try {
668
+ const fsl = await decode_machine(segment);
669
+ this._last = segment;
670
+ this.host.fsl = fsl;
671
+ }
672
+ catch (_a) {
673
+ // Malformed/truncated segment, or no compression support: leave the
674
+ // declared source intact. A bad URL never bricks the page.
675
+ }
676
+ }
677
+ _scheduleWrite() {
678
+ if (this._timer !== undefined) {
679
+ clearTimeout(this._timer);
680
+ }
681
+ this._timer = setTimeout(() => { void this._write(); }, PERMALINK_WRITE_DEBOUNCE_MS);
682
+ }
683
+ /** Encode the current source and merge it into the fragment, history-silently. */
684
+ async _write() {
685
+ try {
686
+ const segment = await encode_machine(this.host.fsl);
687
+ if (segment === this._last) {
688
+ return;
689
+ }
690
+ this._last = segment;
691
+ const fragment = set_fragment_param(location.hash, this.key, segment);
692
+ history.replaceState(history.state, '', `#${fragment}`);
693
+ }
694
+ catch (_a) {
695
+ // No compression support: skip the write rather than throw.
696
+ }
697
+ }
698
+ }
699
+
464
700
  /**
465
701
  * Theme registry for the fsl-* suite. A **theme** is a named pair of light/dark
466
702
  * palettes; a **mode** (`system` | `light` | `dark`) picks which variant applies,
@@ -858,8 +1094,10 @@ function split_ratio(coord, start, size) {
858
1094
  * @slot footer - Footer slot.
859
1095
  */
860
1096
  class FslInstance extends LitElement {
1097
+ /** Bind this instance to a URL-fragment segment keyed by its `uhash`/`id`
1098
+ * (inert if it has neither): restore on connect, write debounced on edit. */
861
1099
  constructor() {
862
- super(...arguments);
1100
+ super();
863
1101
  /**
864
1102
  * FSL source attribute. When non-empty, this is the sole channel
865
1103
  * supplying the machine's source. Setting both this and a child
@@ -1011,6 +1249,7 @@ class FslInstance extends LitElement {
1011
1249
  this._split = 50;
1012
1250
  this.requestUpdate();
1013
1251
  };
1252
+ new FslPermalinkSync(this);
1014
1253
  }
1015
1254
  /**
1016
1255
  * Raw machine accessor. Returns the owned {@link Machine} instance.
@@ -1214,30 +1453,43 @@ class FslInstance extends LitElement {
1214
1453
  super.connectedCallback();
1215
1454
  // Step 1: resolve FSL source.
1216
1455
  const resolved = resolve_fsl_source(this, this.fsl);
1217
- if (resolved.error !== undefined) {
1456
+ // A permalink-only instance has no declared source, but its own URL segment
1457
+ // will supply one asynchronously: FslPermalinkSync (attached in the
1458
+ // constructor) restores `fsl` just after connect, which rebuilds the machine
1459
+ // via willUpdate -> _rebuild_machine. Defer to that instead of throwing;
1460
+ // render() shows the placeholder until the restore lands.
1461
+ const deferToPermalink = resolved.provided_count === 0 && this._permalinkSegmentPresent();
1462
+ if (resolved.error !== undefined && !deferToPermalink) {
1218
1463
  throw new Error(`fsl-instance: ${resolved.error}`);
1219
1464
  }
1220
- // Step 2: construct the machine.
1221
- // (The resolver guarantees `fsl` is a non-empty string when error is undefined.)
1222
- const fsl_source = resolved.fsl;
1223
- this._machine = this._build_machine(fsl_source);
1224
- this._applyEditorConfig();
1225
- // Step 3: paint initial host attributes + CSS custom properties.
1226
- this._paint_state_reflection();
1227
- // Step 4: shadow DOM render is automatic via Lit; requesting an update
1228
- // here ensures the first paint sees the freshly painted attributes.
1229
- this.requestUpdate();
1230
- // #639 mechanism 4: subscribe to library events and re-emit them as
1231
- // DOM CustomEvents from this host (#638 supplies the event API).
1232
- this._install_event_reemission();
1233
- // #641: <jssm-hook> declarative discovery.
1234
- this._install_declarative_hooks();
1235
- // #643: <jssm-on> declarative event observation.
1236
- this._install_jssm_on_children();
1237
- // #645: discover <jssm-bind> tags and `data-jssm-bind` descendants,
1238
- // install live machine-to-DOM projections.
1239
- this._unsubs.push(...install_bindings(this, this._machine));
1240
- // #640: <jssm-action> DOM event → machine action wiring.
1465
+ // Steps 2-4 + machine-scoped wiring: only when a source is available now. In
1466
+ // the deferred case these run later, from _rebuild_machine, once the restore
1467
+ // sets `fsl`.
1468
+ if (!deferToPermalink) {
1469
+ // Step 2: construct the machine.
1470
+ // (The resolver guarantees `fsl` is a non-empty string when error is undefined.)
1471
+ const fsl_source = resolved.fsl;
1472
+ this._machine = this._build_machine(fsl_source);
1473
+ this._applyEditorConfig();
1474
+ // Step 3: paint initial host attributes + CSS custom properties.
1475
+ this._paint_state_reflection();
1476
+ // Step 4: shadow DOM render is automatic via Lit; requesting an update
1477
+ // here ensures the first paint sees the freshly painted attributes.
1478
+ this.requestUpdate();
1479
+ // #639 mechanism 4: subscribe to library events and re-emit them as
1480
+ // DOM CustomEvents from this host (#638 supplies the event API).
1481
+ this._install_event_reemission();
1482
+ // #641: <jssm-hook> declarative discovery.
1483
+ this._install_declarative_hooks();
1484
+ // #643: <jssm-on> declarative event observation.
1485
+ this._install_jssm_on_children();
1486
+ // #645: discover <jssm-bind> tags and `data-jssm-bind` descendants,
1487
+ // install live machine-to-DOM projections.
1488
+ this._unsubs.push(...install_bindings(this, this._machine));
1489
+ }
1490
+ // #640: <jssm-action> DOM event -> machine action wiring. The listeners read
1491
+ // `this.machine` live on event, so discovery is correct even before a
1492
+ // deferred build completes.
1241
1493
  this._discover_jssm_actions();
1242
1494
  // Theme: register the palette tokens as animatable colors (once, globally, so
1243
1495
  // switches can ease), follow the OS while in `system` mode, then apply the
@@ -1249,6 +1501,16 @@ class FslInstance extends LitElement {
1249
1501
  }
1250
1502
  this._applyTheme();
1251
1503
  }
1504
+ /**
1505
+ * True when this instance owns a URL permalink segment (keyed by its
1506
+ * `uhash`/`id`) that a pending {@link FslPermalinkSync} restore will turn into
1507
+ * its FSL source — so `connectedCallback` can defer the machine build to that
1508
+ * restore instead of throwing on an otherwise-absent source.
1509
+ */
1510
+ _permalinkSegmentPresent() {
1511
+ const key = permalink_key_for(this);
1512
+ return key !== null && read_fragment_param(location.hash, key) !== null;
1513
+ }
1252
1514
  /**
1253
1515
  * Discover direct-child `<jssm-on>` elements and install their
1254
1516
  * subscriptions on the owned machine. Per #643:
@@ -1575,6 +1837,12 @@ class FslInstance extends LitElement {
1575
1837
  */
1576
1838
  _install_action_listener(config) {
1577
1839
  const handler = (e) => {
1840
+ // A permalink-only instance wires its actions at connect but builds its
1841
+ // machine asynchronously (deferred restore). An event in that window is a
1842
+ // no-op rather than a throw via the `machine` getter.
1843
+ if (this._machine === undefined) {
1844
+ return;
1845
+ }
1578
1846
  if (config.prevent_default) {
1579
1847
  e.preventDefault();
1580
1848
  }
@@ -77,33 +77,26 @@ function closest_wc(el, suffix) {
77
77
  return el.closest(`fsl-${suffix}, jssm-${suffix}`);
78
78
  }
79
79
 
80
- var __decorate$7 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
81
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
82
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
83
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
84
- return c > 3 && r && Object.defineProperty(target, key, r), r;
85
- };
86
- /** Theme modes offered by the Theme pulldown. */
87
- const THEME_MODES = [
88
- { value: 'system', label: 'System' },
89
- { value: 'light', label: 'Light' },
90
- { value: 'dark', label: 'Dark' },
91
- ];
92
- /** Export formats offered by the Export pulldown. */
93
- const EXPORT_FORMATS = [
94
- { value: 'dot', label: 'Graphviz DOT' },
95
- { value: 'json', label: 'JSON (serialized)' },
96
- { value: 'fsl', label: 'FSL source' },
97
- { value: 'svg', label: 'SVG' },
98
- { value: 'permalink', label: 'Permalink (URL)' },
99
- { value: 'embed', label: 'Embed snippet' },
100
- ];
101
- /** Hash parameter that carries a permalink's encoded machine: `#m=<scheme><payload>`. */
102
- const PERMALINK_HASH_KEY = 'm';
80
+ /**
81
+ * Compressed, URL-safe permalink wire format for FSL machines, shared by the
82
+ * toolbar's Export→Permalink and the per-instance URL sync controller.
83
+ *
84
+ * A machine is encoded to a `<scheme><payload>` segment: `<payload>` is URL-safe
85
+ * base64, `<scheme>` is `1` when DEFLATE shrank the source and `0` for the raw
86
+ * bytes when it did not (so a short machine's link never grows). Segments live in
87
+ * a URL fragment as `#<key>=<segment>` joined by `&`, so several machines can
88
+ * share one URL.
89
+ */
90
+ /**
91
+ * Default fragment key for the single-machine case (back-compat with 5.150).
92
+ *
93
+ * @example
94
+ * DEFAULT_PERMALINK_KEY; // 'm'
95
+ */
96
+ const DEFAULT_PERMALINK_KEY = 'm';
103
97
  /**
104
98
  * URL-safe base64 (RFC 4648 §5) of raw bytes: standard base64 with `+`→`-`,
105
- * `/`→`_`, and trailing `=` padding stripped, so the result rides in a URL
106
- * fragment with no further percent-encoding.
99
+ * `/`→`_`, and trailing `=` padding stripped.
107
100
  *
108
101
  * @example
109
102
  * bytes_to_base64url(new TextEncoder().encode("a")); // "YQ"
@@ -115,7 +108,12 @@ function bytes_to_base64url(bytes) {
115
108
  }
116
109
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
117
110
  }
118
- /** DEFLATE `bytes` (raw, headerless) via the platform `CompressionStream`. */
111
+ /**
112
+ * DEFLATE `bytes` (raw, headerless) via the platform `CompressionStream`.
113
+ *
114
+ * @example
115
+ * await deflate_raw(new TextEncoder().encode("aaaaaaaa")); // shorter Uint8Array of raw DEFLATE bytes
116
+ */
119
117
  async function deflate_raw(bytes) {
120
118
  const stream = new CompressionStream('deflate-raw');
121
119
  const writer = stream.writable.getWriter();
@@ -124,29 +122,98 @@ async function deflate_raw(bytes) {
124
122
  return new Uint8Array(await new Response(stream.readable).arrayBuffer());
125
123
  }
126
124
  /**
127
- * A shareable URL for the given FSL: the current page URL with the source
128
- * compressed into the hash as `#m=<scheme><payload>`. The payload is URL-safe
129
- * base64, so no characters need percent-escaping; `<scheme>` is a single digit
130
- * — `1` when DEFLATE made the source smaller, `0` for the raw bytes when it did
131
- * not — so a short machine's link is never longer than its uncompressed form.
132
- * Decode with {@link fsl_from_permalink}. Browser-only (uses `location` and the
133
- * platform compression streams), like the rest of the toolbar.
134
- *
135
- * @returns The absolute page URL carrying the encoded machine in its fragment.
125
+ * Encode FSL to a `<scheme><payload>` segment value (the part after `key=`).
126
+ * DEFLATE is used (scheme `1`) only when it is strictly shorter than the raw
127
+ * bytes (scheme `0`).
136
128
  *
137
129
  * @example
138
- * await permalink_for("a -> b;"); // "https://host/path#m=0YSAtPiBiOw" (too short to gain from DEFLATE)
139
- * await permalink_for("Off -> On -> Off; On -> Idle -> Off;"); // "https://host/path#m=1809LU9C1U_DPA5NpadZQpmdKTipMCAA"
140
- *
141
- * @see fsl_from_permalink
130
+ * await encode_machine("a -> b;"); // "0YSAtPiBiOw"
142
131
  */
143
- async function permalink_for(fsl) {
132
+ async function encode_machine(fsl) {
144
133
  const utf8 = new TextEncoder().encode(fsl);
145
134
  const raw = bytes_to_base64url(utf8);
146
135
  const deflated = bytes_to_base64url(await deflate_raw(utf8));
147
- const [scheme, payload] = deflated.length < raw.length ? ['1', deflated] : ['0', raw];
148
- return `${location.href.split('#')[0]}#${PERMALINK_HASH_KEY}=${scheme}${payload}`;
136
+ return deflated.length < raw.length ? `1${deflated}` : `0${raw}`;
137
+ }
138
+ /** Split a fragment (leading `#` optional) into `[key, value]` pairs, dropping empties. */
139
+ function fragment_pairs(hash) {
140
+ const body = hash.startsWith('#') ? hash.slice(1) : hash;
141
+ return body.split('&').filter(Boolean).map(seg => {
142
+ const eq = seg.indexOf('=');
143
+ return eq === -1
144
+ ? [seg, '']
145
+ : [seg.slice(0, eq), seg.slice(eq + 1)];
146
+ });
149
147
  }
148
+ /**
149
+ * Return a new fragment body (no leading `#`) with `key`'s segment set to
150
+ * `value`, preserving every other segment and its order; appends if absent.
151
+ *
152
+ * @example
153
+ * set_fragment_param('#a=0AAA', 'b', '1BBB'); // "a=0AAA&b=1BBB"
154
+ */
155
+ function set_fragment_param(hash, key, value) {
156
+ const pairs = fragment_pairs(hash);
157
+ const at = pairs.findIndex(([k]) => k === key);
158
+ if (at === -1) {
159
+ pairs.push([key, value]);
160
+ }
161
+ else {
162
+ pairs[at] = [key, value];
163
+ }
164
+ return pairs.map(([k, v]) => `${k}=${v}`).join('&');
165
+ }
166
+ /**
167
+ * The fragment key an element owns: its `uhash` attribute if set, else its
168
+ * `id`, else `null` (does not participate in URL sync). The single source of
169
+ * this rule, shared by the toolbar export and the sync controller.
170
+ *
171
+ * @example
172
+ * permalink_key_for(el); // "myId" (when <el id="myId">, no uhash)
173
+ */
174
+ function permalink_key_for(host) {
175
+ var _a, _b;
176
+ return (_b = (_a = host.getAttribute('uhash')) !== null && _a !== void 0 ? _a : host.getAttribute('id')) !== null && _b !== void 0 ? _b : null;
177
+ }
178
+ /**
179
+ * A shareable URL for `fsl` under `key`, merging into `currentHash` so sibling
180
+ * machines' segments survive. Browser-defaulted (`location`) but injectable for
181
+ * tests.
182
+ *
183
+ * @returns The absolute URL carrying the merged fragment.
184
+ *
185
+ * @example
186
+ * await permalink_for('a -> b;', 'm', 'https://h/p', ''); // "https://h/p#m=0YSAtPiBiOw"
187
+ *
188
+ * @see fsl_from_permalink
189
+ */
190
+ async function permalink_for(fsl, key = DEFAULT_PERMALINK_KEY, href = location.href, currentHash = location.hash) {
191
+ const segment = await encode_machine(fsl);
192
+ const fragment = set_fragment_param(currentHash, key, segment);
193
+ return `${href.split('#')[0]}#${fragment}`;
194
+ }
195
+
196
+ var __decorate$7 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
197
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
198
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
199
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
200
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
201
+ };
202
+ /** Theme modes offered by the Theme pulldown. */
203
+ const THEME_MODES = [
204
+ { value: 'system', label: 'System' },
205
+ { value: 'light', label: 'Light' },
206
+ { value: 'dark', label: 'Dark' },
207
+ ];
208
+ /** Export formats offered by the Export pulldown. */
209
+ const EXPORT_FORMATS = [
210
+ { value: 'dot', label: 'Graphviz DOT' },
211
+ { value: 'json', label: 'JSON (serialized)' },
212
+ { value: 'fsl', label: 'FSL source' },
213
+ { value: 'svg', label: 'SVG' },
214
+ { value: 'permalink', label: 'Permalink (URL)' },
215
+ { value: 'embed', label: 'Embed snippet' },
216
+ ];
150
217
  /**
151
218
  * A paste-able HTML snippet that renders the given FSL from the CDN builds: an
152
219
  * `<fsl-instance>` reading its source from a `<script type="text/fsl">` child,
@@ -283,6 +350,7 @@ class FslToolbar extends LitElement {
283
350
  /** Emit `fsl-export` with the chosen format's content + the active destination.
284
351
  * The embedder performs the actual clipboard / file save. */
285
352
  async _export(format) {
353
+ var _a;
286
354
  const host = this._host;
287
355
  const destination = this._dest;
288
356
  this._openMenu = '';
@@ -300,7 +368,7 @@ class FslToolbar extends LitElement {
300
368
  content = await machine_to_svg_string(host.machine);
301
369
  }
302
370
  else if (format === 'permalink') {
303
- content = await permalink_for(host.fsl);
371
+ content = await permalink_for(host.fsl, (_a = permalink_key_for(host)) !== null && _a !== void 0 ? _a : undefined);
304
372
  }
305
373
  else if (format === 'embed') {
306
374
  content = embed_snippet_for(host.fsl);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jssm",
3
- "version": "5.151.2",
3
+ "version": "5.152.0",
4
4
  "engines": {
5
5
  "node": ">=10.0.0"
6
6
  },