sketchmark 1.1.0 → 1.1.2

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/dist/index.js CHANGED
@@ -10565,13 +10565,13 @@ function exportHTML(svg, dslSource, opts = {}) {
10565
10565
  </head>
10566
10566
  <body>
10567
10567
  <div class="diagram">${svgStr}</div>
10568
- <details class="dsl"><summary style="cursor:pointer;color:#f0c96a">DSL source</summary><pre>${escapeHtml(dslSource)}</pre></details>
10568
+ <details class="dsl"><summary style="cursor:pointer;color:#f0c96a">DSL source</summary><pre>${escapeHtml$1(dslSource)}</pre></details>
10569
10569
  </body>
10570
10570
  </html>`;
10571
10571
  const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
10572
10572
  download(blob, opts.filename ?? 'diagram.html');
10573
10573
  }
10574
- function escapeHtml(s) {
10574
+ function escapeHtml$1(s) {
10575
10575
  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
10576
10576
  }
10577
10577
  // ── GIF stub (requires gifshot or gif.js at runtime) ──────
@@ -10682,22 +10682,16 @@ class EventEmitter {
10682
10682
  }
10683
10683
  }
10684
10684
 
10685
- // ============================================================
10686
- // sketchmark — Public API
10687
- // ============================================================
10688
- // ── Core Pipeline ─────────────────────────────────────────
10689
10685
  function render(options) {
10690
- const { container: rawContainer, dsl, renderer = 'svg', injectCSS = true, svgOptions = {}, canvasOptions = {}, onNodeClick, onReady, } = options;
10691
- // Inject animation CSS once
10692
- if (injectCSS && !document.getElementById('ai-diagram-css')) {
10693
- const style = document.createElement('style');
10694
- style.id = 'ai-diagram-css';
10686
+ const { container: rawContainer, dsl, renderer = "svg", injectCSS = true, svgOptions = {}, canvasOptions = {}, onNodeClick, onReady, } = options;
10687
+ if (injectCSS && !document.getElementById("ai-diagram-css")) {
10688
+ const style = document.createElement("style");
10689
+ style.id = "ai-diagram-css";
10695
10690
  style.textContent = ANIMATION_CSS;
10696
10691
  document.head.appendChild(style);
10697
10692
  }
10698
- // Resolve container
10699
10693
  let el;
10700
- if (typeof rawContainer === 'string') {
10694
+ if (typeof rawContainer === "string") {
10701
10695
  el = document.querySelector(rawContainer);
10702
10696
  if (!el)
10703
10697
  throw new Error(`Container "${rawContainer}" not found`);
@@ -10705,19 +10699,22 @@ function render(options) {
10705
10699
  else {
10706
10700
  el = rawContainer;
10707
10701
  }
10708
- // Pipeline: DSL → AST → Scene → Layout → Render
10709
10702
  const ast = parse(dsl);
10710
10703
  const scene = buildSceneGraph(ast);
10711
10704
  layout(scene);
10712
10705
  let svg;
10713
10706
  let canvas;
10714
10707
  let anim;
10715
- if (renderer === 'canvas') {
10708
+ if (renderer === "canvas") {
10716
10709
  canvas = el instanceof HTMLCanvasElement
10717
10710
  ? el
10718
- : (() => { const c = document.createElement('canvas'); el.appendChild(c); return c; })();
10711
+ : (() => {
10712
+ const nextCanvas = document.createElement("canvas");
10713
+ el.appendChild(nextCanvas);
10714
+ return nextCanvas;
10715
+ })();
10719
10716
  renderToCanvas(scene, canvas, canvasOptions);
10720
- anim = new AnimationController(document.createElementNS('http://www.w3.org/2000/svg', 'svg'), ast.steps);
10717
+ anim = new AnimationController(document.createElementNS("http://www.w3.org/2000/svg", "svg"), ast.steps);
10721
10718
  }
10722
10719
  else {
10723
10720
  svg = renderToSVG(scene, el, {
@@ -10725,33 +10722,1554 @@ function render(options) {
10725
10722
  interactive: true,
10726
10723
  onNodeClick,
10727
10724
  });
10728
- // Create rough.js instance for annotations (same import as SVG renderer)
10729
10725
  let rc = null;
10730
10726
  try {
10731
10727
  rc = rough.svg(svg);
10732
10728
  }
10733
- catch { /* rough.js not available — annotations disabled */ }
10729
+ catch {
10730
+ rc = null;
10731
+ }
10734
10732
  const containerEl = el instanceof SVGSVGElement ? undefined : el;
10735
10733
  anim = new AnimationController(svg, ast.steps, containerEl, rc, ast.config);
10736
10734
  }
10737
10735
  onReady?.(anim, svg);
10738
- const instance = {
10739
- scene, anim, svg, canvas,
10740
- update: (newDsl) => { anim?.destroy(); return render({ ...options, dsl: newDsl }); },
10741
- exportSVG: (filename = 'diagram.svg') => {
10736
+ return {
10737
+ scene,
10738
+ anim,
10739
+ svg,
10740
+ canvas,
10741
+ update: (newDsl) => {
10742
+ anim?.destroy();
10743
+ return render({ ...options, dsl: newDsl });
10744
+ },
10745
+ exportSVG: (filename = "diagram.svg") => {
10742
10746
  if (svg) {
10743
- Promise.resolve().then(function () { return index; }).then(m => m.exportSVG(svg, { filename }));
10747
+ Promise.resolve().then(function () { return index; }).then((mod) => mod.exportSVG(svg, { filename }));
10744
10748
  }
10745
10749
  },
10746
- exportPNG: async (filename = 'diagram.png') => {
10750
+ exportPNG: async (filename = "diagram.png") => {
10747
10751
  if (svg) {
10748
- const m = await Promise.resolve().then(function () { return index; });
10749
- await m.exportPNG(svg, { filename });
10752
+ const mod = await Promise.resolve().then(function () { return index; });
10753
+ await mod.exportPNG(svg, { filename });
10750
10754
  }
10751
10755
  },
10752
10756
  };
10753
- return instance;
10754
10757
  }
10755
10758
 
10756
- export { ANIMATION_CSS, AnimationController, BUILTIN_FONTS, EventEmitter, PALETTES, ParseError, THEME_CONFIG_KEY, THEME_NAMES, buildSceneGraph, canvasToPNGBlob, canvasToPNGDataURL, clamp, connPoint, debounce, exportCanvasPNG, exportGIF, exportHTML, exportMP4, exportPNG, exportSVG, getSVGBlob, groupMap, hashStr, layout, lerp, listThemes, loadFont, markdownMap, nodeMap, parse, parseHex, registerFont, render, renderToCanvas, renderToSVG, resolveFont, resolvePalette, sleep, svgToPNGDataURL, svgToString, throttle };
10759
+ function resolveContainer(target) {
10760
+ if (typeof target === "string") {
10761
+ const el = document.querySelector(target);
10762
+ if (!el)
10763
+ throw new Error(`Container "${target}" not found`);
10764
+ return el;
10765
+ }
10766
+ return target;
10767
+ }
10768
+ function injectStyleOnce(id, cssText) {
10769
+ if (document.getElementById(id))
10770
+ return;
10771
+ const style = document.createElement("style");
10772
+ style.id = id;
10773
+ style.textContent = cssText;
10774
+ document.head.appendChild(style);
10775
+ }
10776
+ function normalizeNewlines(value) {
10777
+ return value.replace(/\r\n?/g, "\n");
10778
+ }
10779
+ function toError(error) {
10780
+ return error instanceof Error ? error : new Error(String(error));
10781
+ }
10782
+
10783
+ const CANVAS_STYLE_ID = "sketchmark-canvas-ui";
10784
+ const CANVAS_CSS = `
10785
+ .skm-canvas{display:flex;flex-direction:column;width:100%;height:100%;min-height:320px;overflow:hidden;border:1px solid #caba98;border-radius:10px;background:#f8f4ea;color:#3a2010;font-family:"Courier New",monospace}
10786
+ .skm-canvas__animbar{display:flex;align-items:center;gap:6px;padding:6px 10px;background:#eee7d8;border-bottom:1px solid #caba98;flex-shrink:0}
10787
+ .skm-canvas__status{min-width:96px;text-align:center;color:#6a4820;font-size:11px}
10788
+ .skm-canvas__label{color:#8a6040;font-size:11px;font-style:italic}
10789
+ .skm-canvas__spacer{flex:1}
10790
+ .skm-canvas__stats{color:#9a7848;font-size:10px}
10791
+ .skm-canvas__button{border:1px solid #caba98;background:#f5eedd;color:#3a2010;border-radius:6px;padding:4px 9px;font:inherit;font-size:11px;cursor:pointer;transition:background .12s ease,border-color .12s ease,color .12s ease}
10792
+ .skm-canvas__button:hover:not(:disabled){background:#c8a060;border-color:#c8a060;color:#fff}
10793
+ .skm-canvas__button:disabled{opacity:.45;cursor:default}
10794
+ .skm-canvas__error{display:none;padding:8px 12px;background:#280a0a;border-bottom:1px solid #5a1818;color:#f07070;font-size:11px;line-height:1.4;white-space:pre-wrap;flex-shrink:0}
10795
+ .skm-canvas__error.is-visible{display:block}
10796
+ .skm-canvas__viewport{position:relative;flex:1;overflow:hidden;background:#f8f4ea;cursor:grab;touch-action:none}
10797
+ .skm-canvas__viewport.is-panning{cursor:grabbing}
10798
+ .skm-canvas--dark .skm-canvas__viewport{background:#12100a}
10799
+ .skm-canvas__grid{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
10800
+ .skm-canvas__world{position:absolute;top:0;left:0;transform-origin:0 0;}
10801
+ .skm-canvas__controls{position:absolute;right:14px;bottom:14px;display:flex;flex-direction:column;align-items:center;gap:4px;z-index:2}
10802
+ .skm-canvas__zoom{min-width:40px;text-align:center;color:#8a6040;font-size:10px}
10803
+ .skm-canvas__minimap{position:absolute;left:14px;bottom:14px;width:120px;height:80px;background:rgba(255,248,234,.94);border:1px solid #caba98;border-radius:6px;overflow:hidden;z-index:2}
10804
+ .skm-canvas__minimap canvas{width:100%;height:100%;display:block}
10805
+ .skm-canvas__minimap-viewport{position:absolute;border:1.5px solid #c85428;background:rgba(200,84,40,.08);pointer-events:none}
10806
+ .skm-canvas--hide-anim .skm-canvas__animbar,.skm-canvas--hide-controls .skm-canvas__controls,.skm-canvas--hide-minimap .skm-canvas__minimap{display:none}
10807
+ `;
10808
+ let canvasUid = 0;
10809
+ class SketchmarkCanvas {
10810
+ constructor(options) {
10811
+ this.instance = null;
10812
+ this.emitter = new EventEmitter();
10813
+ this.dsl = "";
10814
+ this.panX = 60;
10815
+ this.panY = 60;
10816
+ this.zoom = 1;
10817
+ this.isPanning = false;
10818
+ this.panMoved = false;
10819
+ this.activePointerId = null;
10820
+ this.lastPX = 0;
10821
+ this.lastPY = 0;
10822
+ this.suppressClickUntil = 0;
10823
+ this.hasRenderedOnce = false;
10824
+ this.playInFlight = false;
10825
+ this.minimapToken = 0;
10826
+ this.animUnsub = null;
10827
+ this.editorCleanup = null;
10828
+ this.mirroredEditor = null;
10829
+ this.onPointerDown = (event) => {
10830
+ if (event.button !== 0 && event.button !== 1)
10831
+ return;
10832
+ const target = event.target;
10833
+ if (target instanceof Element && target.closest(".skm-canvas__controls, .skm-canvas__minimap"))
10834
+ return;
10835
+ this.isPanning = true;
10836
+ this.panMoved = false;
10837
+ this.activePointerId = event.pointerId;
10838
+ this.lastPX = event.clientX;
10839
+ this.lastPY = event.clientY;
10840
+ try {
10841
+ this.viewport.setPointerCapture(event.pointerId);
10842
+ }
10843
+ catch {
10844
+ // ignore pointer capture failures
10845
+ }
10846
+ };
10847
+ this.onPointerMove = (event) => {
10848
+ if (!this.isPanning)
10849
+ return;
10850
+ if (this.activePointerId !== null && event.pointerId !== this.activePointerId)
10851
+ return;
10852
+ const dx = event.clientX - this.lastPX;
10853
+ const dy = event.clientY - this.lastPY;
10854
+ if (!this.panMoved && Math.abs(dx) + Math.abs(dy) > 4) {
10855
+ this.panMoved = true;
10856
+ this.viewport.classList.add("is-panning");
10857
+ }
10858
+ if (this.panMoved) {
10859
+ this.panX += dx;
10860
+ this.panY += dy;
10861
+ this.applyTransform();
10862
+ }
10863
+ this.lastPX = event.clientX;
10864
+ this.lastPY = event.clientY;
10865
+ };
10866
+ this.onStopPanning = (event) => {
10867
+ if (this.activePointerId !== null && event?.pointerId != null && event.pointerId !== this.activePointerId)
10868
+ return;
10869
+ if (this.panMoved)
10870
+ this.suppressClickUntil = performance.now() + 180;
10871
+ if (this.activePointerId !== null && this.viewport.hasPointerCapture?.(this.activePointerId)) {
10872
+ try {
10873
+ this.viewport.releasePointerCapture(this.activePointerId);
10874
+ }
10875
+ catch {
10876
+ // ignore pointer capture release failures
10877
+ }
10878
+ }
10879
+ this.activePointerId = null;
10880
+ this.isPanning = false;
10881
+ this.panMoved = false;
10882
+ this.viewport.classList.remove("is-panning");
10883
+ };
10884
+ this.onViewportClick = (event) => {
10885
+ if (performance.now() <= this.suppressClickUntil) {
10886
+ event.preventDefault();
10887
+ event.stopPropagation();
10888
+ }
10889
+ };
10890
+ this.onWheel = (event) => {
10891
+ event.preventDefault();
10892
+ const rect = this.viewport.getBoundingClientRect();
10893
+ const pivotX = event.clientX - rect.left;
10894
+ const pivotY = event.clientY - rect.top;
10895
+ const factor = event.deltaY > 0 ? 0.9 : 1.1;
10896
+ this.zoomTo(this.zoom * factor, pivotX, pivotY);
10897
+ };
10898
+ this.options = options;
10899
+ this.renderer = options.renderer ?? "svg";
10900
+ this.theme = options.theme ?? "light";
10901
+ this.dsl = normalizeNewlines(options.dsl ?? "");
10902
+ injectStyleOnce(CANVAS_STYLE_ID, CANVAS_CSS);
10903
+ const host = resolveContainer(options.container);
10904
+ host.innerHTML = "";
10905
+ this.root = document.createElement("div");
10906
+ this.root.className = "skm-canvas";
10907
+ this.root.classList.toggle("skm-canvas--dark", this.theme === "dark");
10908
+ this.root.classList.toggle("skm-canvas--hide-anim", options.showAnimationBar === false);
10909
+ this.root.classList.toggle("skm-canvas--hide-controls", options.showControls === false);
10910
+ this.root.classList.toggle("skm-canvas--hide-minimap", options.showMinimap === false);
10911
+ const patternId = `skm-grid-${++canvasUid}`;
10912
+ this.root.innerHTML = `
10913
+ <div class="skm-canvas__animbar">
10914
+ <button type="button" class="skm-canvas__button" data-action="reset">Reset</button>
10915
+ <button type="button" class="skm-canvas__button" data-action="prev">Prev</button>
10916
+ <span class="skm-canvas__status">No steps</span>
10917
+ <button type="button" class="skm-canvas__button" data-action="next">Next</button>
10918
+ <button type="button" class="skm-canvas__button" data-action="play">Play</button>
10919
+ <span class="skm-canvas__label"></span>
10920
+ <span class="skm-canvas__spacer"></span>
10921
+ <span class="skm-canvas__stats"></span>
10922
+ </div>
10923
+ <div class="skm-canvas__error"></div>
10924
+ <div class="skm-canvas__viewport">
10925
+ <svg class="skm-canvas__grid" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
10926
+ <defs><pattern id="${patternId}" x="0" y="0" width="24" height="24" patternUnits="userSpaceOnUse"><circle cx="12" cy="12" r="0.9" fill="rgba(170,145,100,0.38)"></circle></pattern></defs>
10927
+ <rect width="100%" height="100%" fill="url(#${patternId})"></rect>
10928
+ </svg>
10929
+ <div class="skm-canvas__world"><div class="skm-canvas__diagram"></div></div>
10930
+ <div class="skm-canvas__controls">
10931
+ <button type="button" class="skm-canvas__button" data-action="fit">Fit</button>
10932
+ <button type="button" class="skm-canvas__button" data-action="reset-view">Reset</button>
10933
+ <button type="button" class="skm-canvas__button" data-action="zoom-in">+</button>
10934
+ <span class="skm-canvas__zoom">100%</span>
10935
+ <button type="button" class="skm-canvas__button" data-action="zoom-out">-</button>
10936
+ </div>
10937
+ <div class="skm-canvas__minimap"><canvas width="120" height="80"></canvas><div class="skm-canvas__minimap-viewport"></div></div>
10938
+ </div>`;
10939
+ this.errorElement = this.root.querySelector(".skm-canvas__error");
10940
+ this.viewport = this.root.querySelector(".skm-canvas__viewport");
10941
+ this.world = this.root.querySelector(".skm-canvas__world");
10942
+ this.diagramWrap = this.root.querySelector(".skm-canvas__diagram");
10943
+ this.zoomLabel = this.root.querySelector(".skm-canvas__zoom");
10944
+ this.stepDisplay = this.root.querySelector(".skm-canvas__status");
10945
+ this.stepLabel = this.root.querySelector(".skm-canvas__label");
10946
+ this.statsLabel = this.root.querySelector(".skm-canvas__stats");
10947
+ this.minimapCanvas = this.root.querySelector(".skm-canvas__minimap canvas");
10948
+ this.minimapIndicator = this.root.querySelector(".skm-canvas__minimap-viewport");
10949
+ this.playButton = this.root.querySelector('[data-action="play"]');
10950
+ this.prevButton = this.root.querySelector('[data-action="prev"]');
10951
+ this.nextButton = this.root.querySelector('[data-action="next"]');
10952
+ this.resetButton = this.root.querySelector('[data-action="reset"]');
10953
+ this.gridPattern = this.root.querySelector(`#${patternId}`);
10954
+ this.gridDot = this.gridPattern.querySelector("circle");
10955
+ this.root.querySelector('[data-action="fit"]')?.addEventListener("click", () => this.fitContent());
10956
+ this.root.querySelector('[data-action="reset-view"]')?.addEventListener("click", () => this.resetView());
10957
+ this.root.querySelector('[data-action="zoom-in"]')?.addEventListener("click", () => this.zoomTo(this.zoom * 1.2, this.viewport.clientWidth / 2, this.viewport.clientHeight / 2));
10958
+ this.root.querySelector('[data-action="zoom-out"]')?.addEventListener("click", () => this.zoomTo(this.zoom * 0.8, this.viewport.clientWidth / 2, this.viewport.clientHeight / 2));
10959
+ this.resetButton.addEventListener("click", () => this.resetAnimation());
10960
+ this.prevButton.addEventListener("click", () => this.prevStep());
10961
+ this.nextButton.addEventListener("click", () => this.nextStep());
10962
+ this.playButton.addEventListener("click", () => void this.play());
10963
+ this.viewport.addEventListener("pointerdown", this.onPointerDown);
10964
+ this.viewport.addEventListener("pointermove", this.onPointerMove);
10965
+ this.viewport.addEventListener("pointerup", this.onStopPanning);
10966
+ this.viewport.addEventListener("pointercancel", this.onStopPanning);
10967
+ this.viewport.addEventListener("lostpointercapture", this.onStopPanning);
10968
+ this.viewport.addEventListener("click", this.onViewportClick, true);
10969
+ this.viewport.addEventListener("wheel", this.onWheel, { passive: false });
10970
+ host.appendChild(this.root);
10971
+ this.applyTransform();
10972
+ this.syncAnimationUi();
10973
+ if (this.dsl.trim())
10974
+ this.render();
10975
+ }
10976
+ getDsl() {
10977
+ return this.dsl;
10978
+ }
10979
+ setDsl(dsl, renderNow = false) {
10980
+ this.dsl = normalizeNewlines(dsl);
10981
+ if (renderNow)
10982
+ this.render();
10983
+ }
10984
+ bindEditor(editor, options = {}) {
10985
+ this.editorCleanup?.();
10986
+ const renderOnRun = options.renderOnRun !== false;
10987
+ const renderOnChange = options.renderOnChange === true;
10988
+ const mirrorErrors = options.mirrorErrors !== false;
10989
+ const initialRender = options.initialRender !== false;
10990
+ this.mirroredEditor = mirrorErrors ? editor : null;
10991
+ const unsubs = [];
10992
+ if (renderOnRun)
10993
+ unsubs.push(editor.on("run", ({ value }) => this.render(value)));
10994
+ if (renderOnChange)
10995
+ unsubs.push(editor.on("change", ({ value }) => this.render(value)));
10996
+ if (initialRender)
10997
+ this.render(editor.getValue());
10998
+ this.editorCleanup = () => {
10999
+ unsubs.forEach((unsub) => unsub());
11000
+ this.mirroredEditor = null;
11001
+ this.editorCleanup = null;
11002
+ };
11003
+ return this.editorCleanup;
11004
+ }
11005
+ on(event, listener) {
11006
+ this.emitter.on(event, listener);
11007
+ return () => this.emitter.off(event, listener);
11008
+ }
11009
+ render(nextDsl) {
11010
+ if (typeof nextDsl === "string")
11011
+ this.dsl = normalizeNewlines(nextDsl);
11012
+ this.clearError();
11013
+ this.mirroredEditor?.clearError();
11014
+ this.animUnsub?.();
11015
+ this.animUnsub = null;
11016
+ this.instance?.anim?.destroy();
11017
+ this.diagramWrap.innerHTML = "";
11018
+ try {
11019
+ const instance = render({
11020
+ container: this.diagramWrap,
11021
+ dsl: this.dsl,
11022
+ renderer: this.renderer,
11023
+ svgOptions: { interactive: true, showTitle: true, theme: this.options.svgOptions?.theme ?? this.theme, ...this.options.svgOptions },
11024
+ canvasOptions: this.options.canvasOptions,
11025
+ onNodeClick: this.options.onNodeClick,
11026
+ });
11027
+ this.instance = instance;
11028
+ this.statsLabel.textContent = `${instance.scene.nodes.length}n / ${instance.scene.edges.length}e / ${instance.scene.groups.length}g`;
11029
+ if (this.renderer === "svg") {
11030
+ this.animUnsub = instance.anim.on((event) => {
11031
+ this.syncAnimationUi();
11032
+ if (event.type === "step-change") {
11033
+ const targetId = this.getStepTarget(event.step);
11034
+ if (targetId)
11035
+ requestAnimationFrame(() => window.setTimeout(() => this.focusAnimatedElement(targetId), 40));
11036
+ this.emitter.emit("stepchange", { stepIndex: event.stepIndex, step: event.step, canvas: this });
11037
+ }
11038
+ });
11039
+ }
11040
+ this.syncAnimationUi();
11041
+ this.renderMinimapPreview();
11042
+ if (!this.hasRenderedOnce || this.options.preserveViewOnRender === false) {
11043
+ this.hasRenderedOnce = true;
11044
+ if (this.options.autoFit !== false)
11045
+ requestAnimationFrame(() => this.fitContent());
11046
+ else
11047
+ this.applyTransform();
11048
+ }
11049
+ else {
11050
+ this.applyTransform();
11051
+ }
11052
+ this.options.onRender?.(instance, this);
11053
+ this.emitter.emit("render", { instance, canvas: this });
11054
+ return instance;
11055
+ }
11056
+ catch (error) {
11057
+ const normalized = toError(error);
11058
+ this.instance = null;
11059
+ this.statsLabel.textContent = "";
11060
+ this.showError(normalized.message);
11061
+ this.mirroredEditor?.showError(normalized.message);
11062
+ this.syncAnimationUi();
11063
+ this.renderMinimapPreview();
11064
+ this.emitter.emit("error", { error: normalized, canvas: this });
11065
+ return null;
11066
+ }
11067
+ }
11068
+ async play() {
11069
+ if (!this.instance || this.playInFlight || this.renderer !== "svg" || !this.instance.anim.total)
11070
+ return;
11071
+ this.playInFlight = true;
11072
+ this.syncAnimationUi();
11073
+ try {
11074
+ await this.instance.anim.play(this.options.playStepDelay ?? 800);
11075
+ }
11076
+ finally {
11077
+ this.playInFlight = false;
11078
+ this.syncAnimationUi();
11079
+ }
11080
+ }
11081
+ nextStep() {
11082
+ if (!this.instance || this.renderer !== "svg")
11083
+ return;
11084
+ this.instance.anim.next();
11085
+ this.syncAnimationUi();
11086
+ this.focusCurrentStep();
11087
+ }
11088
+ prevStep() {
11089
+ if (!this.instance || this.renderer !== "svg")
11090
+ return;
11091
+ this.instance.anim.prev();
11092
+ this.syncAnimationUi();
11093
+ this.focusCurrentStep();
11094
+ }
11095
+ resetAnimation() {
11096
+ if (!this.instance || this.renderer !== "svg")
11097
+ return;
11098
+ this.instance.anim.reset();
11099
+ this.syncAnimationUi();
11100
+ }
11101
+ fitContent() {
11102
+ const size = this.getContentSize();
11103
+ if (!size)
11104
+ return;
11105
+ const vpW = this.viewport.clientWidth || size.width;
11106
+ const vpH = this.viewport.clientHeight || size.height;
11107
+ const padding = this.options.fitPadding ?? 80;
11108
+ const nextZoom = Math.min((vpW - padding) / size.width, (vpH - padding) / size.height, 1);
11109
+ this.zoom = clamp(nextZoom || 1, this.options.zoomMin ?? 0.08, this.options.zoomMax ?? 4);
11110
+ this.panX = (vpW - size.width * this.zoom) / 2;
11111
+ this.panY = (vpH - size.height * this.zoom) / 2;
11112
+ this.applyTransform();
11113
+ }
11114
+ resetView() {
11115
+ this.panX = 60;
11116
+ this.panY = 60;
11117
+ this.zoom = 1;
11118
+ this.applyTransform();
11119
+ }
11120
+ setTheme(theme) {
11121
+ this.theme = theme;
11122
+ this.root.classList.toggle("skm-canvas--dark", theme === "dark");
11123
+ this.render();
11124
+ }
11125
+ destroy() {
11126
+ this.editorCleanup?.();
11127
+ this.animUnsub?.();
11128
+ this.instance?.anim?.destroy();
11129
+ this.viewport.removeEventListener("pointerdown", this.onPointerDown);
11130
+ this.viewport.removeEventListener("pointermove", this.onPointerMove);
11131
+ this.viewport.removeEventListener("pointerup", this.onStopPanning);
11132
+ this.viewport.removeEventListener("pointercancel", this.onStopPanning);
11133
+ this.viewport.removeEventListener("lostpointercapture", this.onStopPanning);
11134
+ this.viewport.removeEventListener("click", this.onViewportClick, true);
11135
+ this.viewport.removeEventListener("wheel", this.onWheel);
11136
+ this.root.remove();
11137
+ }
11138
+ applyTransform() {
11139
+ this.world.style.transform = `translate(${this.panX}px, ${this.panY}px) scale(${this.zoom})`;
11140
+ this.zoomLabel.textContent = `${Math.round(this.zoom * 100)}%`;
11141
+ const gridWidth = 24 * this.zoom;
11142
+ this.gridPattern.setAttribute("x", String(this.panX % gridWidth));
11143
+ this.gridPattern.setAttribute("y", String(this.panY % gridWidth));
11144
+ this.gridPattern.setAttribute("width", String(gridWidth));
11145
+ this.gridPattern.setAttribute("height", String(gridWidth));
11146
+ this.gridDot.setAttribute("cx", String(gridWidth / 2));
11147
+ this.gridDot.setAttribute("cy", String(gridWidth / 2));
11148
+ this.gridDot.setAttribute("r", String(Math.min(1.1, this.zoom * 0.85)));
11149
+ this.updateMinimapIndicator();
11150
+ this.emitter.emit("viewchange", { panX: this.panX, panY: this.panY, zoom: this.zoom, canvas: this });
11151
+ }
11152
+ zoomTo(nextZoom, pivotX, pivotY) {
11153
+ const clampedZoom = clamp(nextZoom, this.options.zoomMin ?? 0.08, this.options.zoomMax ?? 4);
11154
+ const ratio = clampedZoom / this.zoom;
11155
+ this.panX = pivotX - (pivotX - this.panX) * ratio;
11156
+ this.panY = pivotY - (pivotY - this.panY) * ratio;
11157
+ this.zoom = clampedZoom;
11158
+ this.applyTransform();
11159
+ }
11160
+ syncAnimationUi() {
11161
+ const anim = this.instance?.anim;
11162
+ const canAnimate = this.renderer === "svg" && !!anim && anim.total > 0;
11163
+ if (!anim || !canAnimate) {
11164
+ this.stepDisplay.textContent = this.renderer === "canvas" ? "Static view" : "No steps";
11165
+ this.stepLabel.textContent = "";
11166
+ this.prevButton.disabled = true;
11167
+ this.nextButton.disabled = true;
11168
+ this.resetButton.disabled = true;
11169
+ this.playButton.disabled = true;
11170
+ return;
11171
+ }
11172
+ this.stepDisplay.textContent = anim.currentStep < 0 ? `${anim.total} steps` : `${anim.currentStep + 1} / ${anim.total}`;
11173
+ this.stepLabel.textContent = anim.currentStep >= 0 ? this.getStepLabel(anim.steps[anim.currentStep]) : "";
11174
+ this.prevButton.disabled = !anim.canPrev;
11175
+ this.nextButton.disabled = !anim.canNext;
11176
+ this.resetButton.disabled = false;
11177
+ this.playButton.disabled = this.playInFlight || !anim.canNext;
11178
+ }
11179
+ getStepTarget(stepItem) {
11180
+ if (!stepItem)
11181
+ return null;
11182
+ return stepItem.kind === "beat" ? stepItem.children?.[0]?.target ?? null : stepItem.target ?? null;
11183
+ }
11184
+ getStepLabel(stepItem) {
11185
+ if (!stepItem)
11186
+ return "";
11187
+ if (stepItem.kind === "beat") {
11188
+ const first = stepItem.children?.[0];
11189
+ return first ? `beat ${first.action} ${first.target ?? ""}`.trim() : "beat";
11190
+ }
11191
+ return `${stepItem.action} ${stepItem.target ?? ""}`.trim();
11192
+ }
11193
+ focusCurrentStep() {
11194
+ const anim = this.instance?.anim;
11195
+ if (!anim || anim.currentStep < 0 || anim.currentStep >= anim.total)
11196
+ return;
11197
+ const targetId = this.getStepTarget(anim.steps[anim.currentStep]);
11198
+ if (targetId)
11199
+ window.setTimeout(() => this.focusAnimatedElement(targetId), 40);
11200
+ }
11201
+ findSvgElement(svg, id) {
11202
+ const prefixes = ["group-", "node-", "edge-", "table-", "chart-", "markdown-", "note-", ""];
11203
+ const esc = typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape : (value) => value;
11204
+ for (const prefix of prefixes) {
11205
+ const found = svg.querySelector(`#${esc(prefix + id)}`);
11206
+ if (found)
11207
+ return found;
11208
+ }
11209
+ for (const attr of ["data-id", "data-node", "data-group", "sketchmark-id"]) {
11210
+ const found = svg.querySelector(`[${attr}="${id}"]`);
11211
+ if (found)
11212
+ return found;
11213
+ }
11214
+ return null;
11215
+ }
11216
+ focusAnimatedElement(targetId) {
11217
+ const svg = this.instance?.svg;
11218
+ if (!svg)
11219
+ return;
11220
+ const searchIds = this.splitEdgeTarget(targetId);
11221
+ let target = null;
11222
+ for (const id of searchIds) {
11223
+ target = this.findSvgElement(svg, id);
11224
+ if (target)
11225
+ break;
11226
+ }
11227
+ if (!target)
11228
+ return;
11229
+ const box = target.getBoundingClientRect();
11230
+ if (!box.width && !box.height)
11231
+ return;
11232
+ const vpBox = this.viewport.getBoundingClientRect();
11233
+ const centerX = box.left + box.width / 2 - vpBox.left;
11234
+ const centerY = box.top + box.height / 2 - vpBox.top;
11235
+ const margin = 100;
11236
+ if (centerX >= margin && centerX <= vpBox.width - margin && centerY >= margin && centerY <= vpBox.height - margin)
11237
+ return;
11238
+ const targetPanX = this.panX + (vpBox.width / 2 - centerX);
11239
+ const targetPanY = this.panY + (vpBox.height / 2 - centerY);
11240
+ const startPanX = this.panX;
11241
+ const startPanY = this.panY;
11242
+ const startTs = performance.now();
11243
+ const duration = 350;
11244
+ const frame = (now) => {
11245
+ const t = Math.min((now - startTs) / duration, 1);
11246
+ const eased = 1 - Math.pow(1 - t, 3);
11247
+ this.panX = startPanX + (targetPanX - startPanX) * eased;
11248
+ this.panY = startPanY + (targetPanY - startPanY) * eased;
11249
+ this.applyTransform();
11250
+ if (t < 1)
11251
+ requestAnimationFrame(frame);
11252
+ };
11253
+ requestAnimationFrame(frame);
11254
+ }
11255
+ splitEdgeTarget(targetId) {
11256
+ const connectors = ["<-->", "<->", "-->", "<--", "---", "--", "->", "<-"];
11257
+ for (const connector of connectors) {
11258
+ if (targetId.includes(connector)) {
11259
+ return targetId.split(connector).map((part) => part.trim()).filter(Boolean);
11260
+ }
11261
+ }
11262
+ return [targetId.trim()];
11263
+ }
11264
+ getContentSize() {
11265
+ if (this.instance?.svg) {
11266
+ return { width: parseFloat(this.instance.svg.getAttribute("width") || "400"), height: parseFloat(this.instance.svg.getAttribute("height") || "300") };
11267
+ }
11268
+ if (this.instance?.canvas) {
11269
+ return { width: this.instance.canvas.width || 400, height: this.instance.canvas.height || 300 };
11270
+ }
11271
+ return null;
11272
+ }
11273
+ updateMinimapIndicator() {
11274
+ if (this.options.showMinimap === false)
11275
+ return;
11276
+ const size = this.getContentSize();
11277
+ if (!size) {
11278
+ this.minimapIndicator.style.width = "0px";
11279
+ this.minimapIndicator.style.height = "0px";
11280
+ return;
11281
+ }
11282
+ const mW = this.minimapCanvas.width;
11283
+ const mH = this.minimapCanvas.height;
11284
+ const scale = Math.min(mW / size.width, mH / size.height) * 0.9;
11285
+ const offX = (mW - size.width * scale) / 2;
11286
+ const offY = (mH - size.height * scale) / 2;
11287
+ const vpW = this.viewport.clientWidth || size.width;
11288
+ const vpH = this.viewport.clientHeight || size.height;
11289
+ const ix = offX + (-this.panX / this.zoom) * scale;
11290
+ const iy = offY + (-this.panY / this.zoom) * scale;
11291
+ const iw = (vpW / this.zoom) * scale;
11292
+ const ih = (vpH / this.zoom) * scale;
11293
+ this.minimapIndicator.style.left = `${Math.max(0, ix)}px`;
11294
+ this.minimapIndicator.style.top = `${Math.max(0, iy)}px`;
11295
+ this.minimapIndicator.style.width = `${Math.min(mW - Math.max(0, ix), iw)}px`;
11296
+ this.minimapIndicator.style.height = `${Math.min(mH - Math.max(0, iy), ih)}px`;
11297
+ }
11298
+ renderMinimapPreview() {
11299
+ if (this.options.showMinimap === false)
11300
+ return;
11301
+ const ctx = this.minimapCanvas.getContext("2d");
11302
+ const size = this.getContentSize();
11303
+ if (!ctx)
11304
+ return;
11305
+ const width = this.minimapCanvas.width;
11306
+ const height = this.minimapCanvas.height;
11307
+ ctx.clearRect(0, 0, width, height);
11308
+ ctx.fillStyle = this.theme === "dark" ? "#1a140b" : "#fff8ea";
11309
+ ctx.fillRect(0, 0, width, height);
11310
+ if (!size) {
11311
+ this.updateMinimapIndicator();
11312
+ return;
11313
+ }
11314
+ const scale = Math.min(width / size.width, height / size.height) * 0.9;
11315
+ const drawW = size.width * scale;
11316
+ const drawH = size.height * scale;
11317
+ const offX = (width - drawW) / 2;
11318
+ const offY = (height - drawH) / 2;
11319
+ const token = ++this.minimapToken;
11320
+ const drawFallback = () => {
11321
+ if (token !== this.minimapToken)
11322
+ return;
11323
+ ctx.fillStyle = this.theme === "dark" ? "#20180e" : "#f7f1e2";
11324
+ ctx.fillRect(offX, offY, drawW, drawH);
11325
+ ctx.strokeStyle = this.theme === "dark" ? "#5a4525" : "#caba98";
11326
+ ctx.strokeRect(offX, offY, drawW, drawH);
11327
+ this.updateMinimapIndicator();
11328
+ };
11329
+ if (this.instance?.canvas) {
11330
+ try {
11331
+ ctx.drawImage(this.instance.canvas, offX, offY, drawW, drawH);
11332
+ ctx.strokeStyle = this.theme === "dark" ? "#5a4525" : "#caba98";
11333
+ ctx.strokeRect(offX, offY, drawW, drawH);
11334
+ }
11335
+ catch {
11336
+ drawFallback();
11337
+ }
11338
+ this.updateMinimapIndicator();
11339
+ return;
11340
+ }
11341
+ if (!this.instance?.svg || typeof XMLSerializer === "undefined") {
11342
+ drawFallback();
11343
+ return;
11344
+ }
11345
+ try {
11346
+ const serialized = new XMLSerializer().serializeToString(this.instance.svg);
11347
+ const blob = new Blob([serialized], { type: "image/svg+xml;charset=utf-8" });
11348
+ const url = URL.createObjectURL(blob);
11349
+ const image = new Image();
11350
+ image.onload = () => {
11351
+ if (token !== this.minimapToken) {
11352
+ URL.revokeObjectURL(url);
11353
+ return;
11354
+ }
11355
+ try {
11356
+ ctx.drawImage(image, offX, offY, drawW, drawH);
11357
+ ctx.strokeStyle = this.theme === "dark" ? "#5a4525" : "#caba98";
11358
+ ctx.strokeRect(offX, offY, drawW, drawH);
11359
+ }
11360
+ catch {
11361
+ drawFallback();
11362
+ }
11363
+ finally {
11364
+ URL.revokeObjectURL(url);
11365
+ this.updateMinimapIndicator();
11366
+ }
11367
+ };
11368
+ image.onerror = () => {
11369
+ URL.revokeObjectURL(url);
11370
+ drawFallback();
11371
+ };
11372
+ image.src = url;
11373
+ }
11374
+ catch {
11375
+ drawFallback();
11376
+ }
11377
+ }
11378
+ showError(message) {
11379
+ this.errorElement.textContent = message;
11380
+ this.errorElement.classList.add("is-visible");
11381
+ }
11382
+ clearError() {
11383
+ this.errorElement.textContent = "";
11384
+ this.errorElement.classList.remove("is-visible");
11385
+ }
11386
+ }
11387
+
11388
+ const EDITOR_STYLE_ID = "sketchmark-editor-ui";
11389
+ const DEFAULT_CLEAR_VALUE = "diagram\n\nend";
11390
+ const EDITOR_CSS = `
11391
+ .skm-editor {
11392
+ display: flex;
11393
+ flex-direction: column;
11394
+ width: 100%;
11395
+ height: 100%;
11396
+ min-height: 240px;
11397
+ background: #1c1608;
11398
+ color: #e0c898;
11399
+ border: 1px solid #3a2a12;
11400
+ border-radius: 10px;
11401
+ overflow: hidden;
11402
+ font-family: "Courier New", monospace;
11403
+ }
11404
+
11405
+ .skm-editor__toolbar {
11406
+ display: flex;
11407
+ align-items: center;
11408
+ gap: 8px;
11409
+ padding: 8px 10px;
11410
+ background: #12100a;
11411
+ border-bottom: 1px solid #3a2a12;
11412
+ flex-shrink: 0;
11413
+ }
11414
+
11415
+ .skm-editor__hint {
11416
+ margin-left: auto;
11417
+ color: #9a7848;
11418
+ font-size: 11px;
11419
+ }
11420
+
11421
+ .skm-editor__button {
11422
+ border: 1px solid #4a3520;
11423
+ background: #22190e;
11424
+ color: #dcc48a;
11425
+ border-radius: 6px;
11426
+ padding: 4px 10px;
11427
+ font: inherit;
11428
+ font-size: 11px;
11429
+ cursor: pointer;
11430
+ transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
11431
+ }
11432
+
11433
+ .skm-editor__button:hover {
11434
+ border-color: #f0c96a;
11435
+ color: #f0c96a;
11436
+ }
11437
+
11438
+ .skm-editor__button--primary {
11439
+ background: #c85428;
11440
+ border-color: #c85428;
11441
+ color: #fff9ef;
11442
+ }
11443
+
11444
+ .skm-editor__button--primary:hover {
11445
+ background: #db6437;
11446
+ border-color: #db6437;
11447
+ color: #fff;
11448
+ }
11449
+
11450
+ .skm-editor__surface {
11451
+ position: relative;
11452
+ flex: 1;
11453
+ min-height: 0;
11454
+ background: #1c1608;
11455
+ overflow: hidden;
11456
+ }
11457
+
11458
+ .skm-editor__highlight,
11459
+ .skm-editor__input {
11460
+ position: absolute;
11461
+ inset: 0;
11462
+ width: 100%;
11463
+ height: 100%;
11464
+ padding: 12px 14px;
11465
+ font: inherit;
11466
+ font-size: 12px;
11467
+ line-height: 1.7;
11468
+ tab-size: 2;
11469
+ white-space: pre-wrap;
11470
+ overflow: auto;
11471
+ }
11472
+
11473
+ .skm-editor__highlight {
11474
+ margin: 0;
11475
+ border: 0;
11476
+ background: #1c1608;
11477
+ color: #e0c898;
11478
+ pointer-events: none;
11479
+ word-break: break-word;
11480
+ }
11481
+
11482
+ .skm-editor__input {
11483
+ border: 0;
11484
+ outline: 0;
11485
+ resize: none;
11486
+ background: transparent;
11487
+ color: transparent;
11488
+ caret-color: #f0c96a;
11489
+ }
11490
+
11491
+ .skm-editor__input::placeholder {
11492
+ color: #80633b;
11493
+ }
11494
+
11495
+ .skm-editor__input::selection {
11496
+ background: rgba(240, 201, 106, 0.22);
11497
+ }
11498
+
11499
+ .skm-editor__token--keyword {
11500
+ color: #e07040;
11501
+ }
11502
+
11503
+ .skm-editor__token--property {
11504
+ color: #70a8d0;
11505
+ }
11506
+
11507
+ .skm-editor__token--string {
11508
+ color: #8db870;
11509
+ }
11510
+
11511
+ .skm-editor__token--number {
11512
+ color: #d4a020;
11513
+ }
11514
+
11515
+ .skm-editor__token--comment {
11516
+ color: #6a5a3a;
11517
+ }
11518
+
11519
+ .skm-editor__token--connector {
11520
+ color: #c8b070;
11521
+ }
11522
+
11523
+ .skm-editor__token--color {
11524
+ color: var(--skm-editor-color, #f0c96a);
11525
+ box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.08);
11526
+ font-weight: 600;
11527
+ }
11528
+
11529
+ .skm-editor__error {
11530
+ display: none;
11531
+ flex-shrink: 0;
11532
+ padding: 8px 12px;
11533
+ background: #280a0a;
11534
+ border-top: 1px solid #5a1818;
11535
+ color: #f07070;
11536
+ font-size: 11px;
11537
+ line-height: 1.4;
11538
+ white-space: pre-wrap;
11539
+ }
11540
+
11541
+ .skm-editor__error.is-visible {
11542
+ display: block;
11543
+ }
11544
+ `;
11545
+ const CONNECTORS = ["<-->", "<->", "-->", "<--", "---", "--", "->", "<-"];
11546
+ const HEX_COLOR_RE = /#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})\b/g;
11547
+ function defaultFormatter(value) {
11548
+ return normalizeNewlines(value)
11549
+ .split("\n")
11550
+ .map((line) => line.replace(/[ \t]+$/g, ""))
11551
+ .join("\n");
11552
+ }
11553
+ function escapeHtml(value) {
11554
+ return value
11555
+ .replace(/&/g, "&amp;")
11556
+ .replace(/</g, "&lt;")
11557
+ .replace(/>/g, "&gt;")
11558
+ .replace(/"/g, "&quot;");
11559
+ }
11560
+ function wrapToken(kind, value) {
11561
+ return `<span class="skm-editor__token skm-editor__token--${kind}">${escapeHtml(value)}</span>`;
11562
+ }
11563
+ function renderColorLiteral(value) {
11564
+ return `<span class="skm-editor__token skm-editor__token--color" style="--skm-editor-color:${value}">${escapeHtml(value)}</span>`;
11565
+ }
11566
+ function renderStringToken(value) {
11567
+ HEX_COLOR_RE.lastIndex = 0;
11568
+ if (!HEX_COLOR_RE.test(value)) {
11569
+ return wrapToken("string", value);
11570
+ }
11571
+ HEX_COLOR_RE.lastIndex = 0;
11572
+ let html = "";
11573
+ let lastIndex = 0;
11574
+ let match = null;
11575
+ while ((match = HEX_COLOR_RE.exec(value))) {
11576
+ if (match.index > lastIndex) {
11577
+ html += wrapToken("string", value.slice(lastIndex, match.index));
11578
+ }
11579
+ html += renderColorLiteral(match[0]);
11580
+ lastIndex = match.index + match[0].length;
11581
+ }
11582
+ if (lastIndex < value.length) {
11583
+ html += wrapToken("string", value.slice(lastIndex));
11584
+ }
11585
+ return html;
11586
+ }
11587
+ function renderPlainToken(value, nextChar) {
11588
+ if (/^-?\d/.test(value)) {
11589
+ return wrapToken("number", value);
11590
+ }
11591
+ if (nextChar === "=") {
11592
+ return wrapToken("property", value);
11593
+ }
11594
+ if (KEYWORDS.has(value)) {
11595
+ return wrapToken("keyword", value);
11596
+ }
11597
+ return escapeHtml(value);
11598
+ }
11599
+ function highlightLine(line) {
11600
+ let html = "";
11601
+ let index = 0;
11602
+ while (index < line.length) {
11603
+ const rest = line.slice(index);
11604
+ if (rest.startsWith("//") || rest.startsWith("#")) {
11605
+ html += wrapToken("comment", rest);
11606
+ break;
11607
+ }
11608
+ if (line[index] === "\"") {
11609
+ let end = index + 1;
11610
+ while (end < line.length) {
11611
+ if (line[end] === "\"" && line[end - 1] !== "\\") {
11612
+ end += 1;
11613
+ break;
11614
+ }
11615
+ end += 1;
11616
+ }
11617
+ html += renderStringToken(line.slice(index, end));
11618
+ index = end;
11619
+ continue;
11620
+ }
11621
+ const connector = CONNECTORS.find((candidate) => line.startsWith(candidate, index));
11622
+ if (connector) {
11623
+ html += wrapToken("connector", connector);
11624
+ index += connector.length;
11625
+ continue;
11626
+ }
11627
+ const wordMatch = /^[A-Za-z_][A-Za-z0-9_-]*/.exec(rest);
11628
+ if (wordMatch) {
11629
+ const word = wordMatch[0];
11630
+ const nextChar = line[index + word.length] ?? "";
11631
+ html += renderPlainToken(word, nextChar);
11632
+ index += word.length;
11633
+ continue;
11634
+ }
11635
+ const numberMatch = /^-?\d+(?:\.\d+)?/.exec(rest);
11636
+ if (numberMatch) {
11637
+ html += wrapToken("number", numberMatch[0]);
11638
+ index += numberMatch[0].length;
11639
+ continue;
11640
+ }
11641
+ html += escapeHtml(line[index]);
11642
+ index += 1;
11643
+ }
11644
+ return html;
11645
+ }
11646
+ function renderHighlightedValue(value) {
11647
+ const normalized = normalizeNewlines(value);
11648
+ const html = normalized.split("\n").map(highlightLine).join("\n");
11649
+ return html || " ";
11650
+ }
11651
+ class SketchmarkEditor {
11652
+ constructor(options) {
11653
+ this.emitter = new EventEmitter();
11654
+ this.options = options;
11655
+ injectStyleOnce(EDITOR_STYLE_ID, EDITOR_CSS);
11656
+ const host = resolveContainer(options.container);
11657
+ host.innerHTML = "";
11658
+ this.root = document.createElement("div");
11659
+ this.root.className = "skm-editor";
11660
+ this.toolbar = document.createElement("div");
11661
+ this.toolbar.className = "skm-editor__toolbar";
11662
+ const runButton = document.createElement("button");
11663
+ runButton.type = "button";
11664
+ runButton.className = "skm-editor__button skm-editor__button--primary";
11665
+ runButton.textContent = options.runLabel ?? "Run";
11666
+ runButton.addEventListener("click", () => this.run());
11667
+ const formatButton = document.createElement("button");
11668
+ formatButton.type = "button";
11669
+ formatButton.className = "skm-editor__button";
11670
+ formatButton.textContent = options.formatLabel ?? "Format";
11671
+ formatButton.addEventListener("click", () => this.format());
11672
+ const clearButton = document.createElement("button");
11673
+ clearButton.type = "button";
11674
+ clearButton.className = "skm-editor__button";
11675
+ clearButton.textContent = options.clearLabel ?? "Clear";
11676
+ clearButton.addEventListener("click", () => this.clear());
11677
+ const hint = document.createElement("span");
11678
+ hint.className = "skm-editor__hint";
11679
+ hint.textContent = "Ctrl+Enter";
11680
+ if (options.showRunButton !== false)
11681
+ this.toolbar.appendChild(runButton);
11682
+ if (options.showFormatButton)
11683
+ this.toolbar.appendChild(formatButton);
11684
+ if (options.showClearButton !== false)
11685
+ this.toolbar.appendChild(clearButton);
11686
+ this.toolbar.appendChild(hint);
11687
+ this.surface = document.createElement("div");
11688
+ this.surface.className = "skm-editor__surface";
11689
+ this.highlightElement = document.createElement("pre");
11690
+ this.highlightElement.className = "skm-editor__highlight";
11691
+ this.highlightElement.setAttribute("aria-hidden", "true");
11692
+ this.textarea = document.createElement("textarea");
11693
+ this.textarea.className = "skm-editor__input";
11694
+ this.textarea.spellcheck = false;
11695
+ this.textarea.placeholder = options.placeholder ?? "diagram\nbox a label=\"Hello\"\nend";
11696
+ this.textarea.value = normalizeNewlines(options.value ?? DEFAULT_CLEAR_VALUE);
11697
+ this.textarea.addEventListener("input", () => {
11698
+ this.syncHighlight();
11699
+ const payload = { value: this.getValue(), editor: this };
11700
+ options.onChange?.(payload.value, this);
11701
+ this.emitter.emit("change", payload);
11702
+ });
11703
+ this.textarea.addEventListener("scroll", () => this.syncScroll());
11704
+ this.textarea.addEventListener("keydown", (event) => {
11705
+ if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
11706
+ event.preventDefault();
11707
+ this.run();
11708
+ }
11709
+ });
11710
+ this.errorElement = document.createElement("div");
11711
+ this.errorElement.className = "skm-editor__error";
11712
+ if (options.showToolbar !== false) {
11713
+ this.root.appendChild(this.toolbar);
11714
+ }
11715
+ this.surface.appendChild(this.highlightElement);
11716
+ this.surface.appendChild(this.textarea);
11717
+ this.root.appendChild(this.surface);
11718
+ this.root.appendChild(this.errorElement);
11719
+ host.appendChild(this.root);
11720
+ this.syncHighlight();
11721
+ if (options.autoFocus) {
11722
+ this.focus();
11723
+ }
11724
+ }
11725
+ getValue() {
11726
+ return this.textarea.value;
11727
+ }
11728
+ setValue(value, emitChange = false) {
11729
+ this.textarea.value = normalizeNewlines(value);
11730
+ this.syncHighlight();
11731
+ if (emitChange) {
11732
+ const payload = { value: this.getValue(), editor: this };
11733
+ this.options.onChange?.(payload.value, this);
11734
+ this.emitter.emit("change", payload);
11735
+ }
11736
+ }
11737
+ focus() {
11738
+ this.textarea.focus();
11739
+ }
11740
+ format() {
11741
+ const formatter = this.options.formatter ?? defaultFormatter;
11742
+ const value = formatter(this.getValue());
11743
+ this.setValue(value, true);
11744
+ this.emitter.emit("format", { value, editor: this });
11745
+ }
11746
+ clear() {
11747
+ const value = this.options.clearValue ?? DEFAULT_CLEAR_VALUE;
11748
+ this.setValue(value, true);
11749
+ this.clearError();
11750
+ this.emitter.emit("clear", { value: this.getValue(), editor: this });
11751
+ }
11752
+ run() {
11753
+ const value = this.getValue();
11754
+ this.options.onRun?.(value, this);
11755
+ this.emitter.emit("run", { value, editor: this });
11756
+ }
11757
+ showError(message) {
11758
+ this.errorElement.textContent = message;
11759
+ this.errorElement.classList.add("is-visible");
11760
+ }
11761
+ clearError() {
11762
+ this.errorElement.textContent = "";
11763
+ this.errorElement.classList.remove("is-visible");
11764
+ }
11765
+ on(event, listener) {
11766
+ this.emitter.on(event, listener);
11767
+ return () => this.emitter.off(event, listener);
11768
+ }
11769
+ destroy() {
11770
+ this.root.remove();
11771
+ }
11772
+ syncHighlight() {
11773
+ this.highlightElement.innerHTML = renderHighlightedValue(this.textarea.value);
11774
+ this.syncScroll();
11775
+ }
11776
+ syncScroll() {
11777
+ this.highlightElement.scrollTop = this.textarea.scrollTop;
11778
+ this.highlightElement.scrollLeft = this.textarea.scrollLeft;
11779
+ }
11780
+ }
11781
+
11782
+ const EMBED_STYLE_ID = "sketchmark-embed-ui";
11783
+ const EMBED_CSS = `
11784
+ .skm-embed {
11785
+ display: flex;
11786
+ flex-direction: column;
11787
+ overflow: hidden;
11788
+ border: 1px solid #caba98;
11789
+ border-radius: 12px;
11790
+ background: #fff8ea;
11791
+ color: #3a2010;
11792
+ font-family: "Courier New", monospace;
11793
+ }
11794
+
11795
+ .skm-embed--dark {
11796
+ background: #12100a;
11797
+ border-color: #4a3520;
11798
+ color: #f3ddaf;
11799
+ }
11800
+
11801
+ .skm-embed__viewport {
11802
+ position: relative;
11803
+ flex: 1;
11804
+ overflow: hidden;
11805
+ min-height: 0;
11806
+ background: inherit;
11807
+ }
11808
+
11809
+ .skm-embed__world {
11810
+ position: absolute;
11811
+ top: 0;
11812
+ left: 0;
11813
+ transform-origin: 0 0;
11814
+ will-change: transform;
11815
+ }
11816
+
11817
+ .skm-embed__error {
11818
+ display: none;
11819
+ padding: 8px 12px;
11820
+ background: #280a0a;
11821
+ border-top: 1px solid #5a1818;
11822
+ color: #f07070;
11823
+ font-size: 11px;
11824
+ line-height: 1.4;
11825
+ white-space: pre-wrap;
11826
+ }
11827
+
11828
+ .skm-embed__error.is-visible {
11829
+ display: block;
11830
+ }
11831
+
11832
+ .skm-embed__controls {
11833
+ display: flex;
11834
+ align-items: center;
11835
+ gap: 8px;
11836
+ padding: 10px 12px;
11837
+ border-top: 1px solid #d8ccb1;
11838
+ background: rgba(255, 248, 234, 0.88);
11839
+ backdrop-filter: blur(6px);
11840
+ }
11841
+
11842
+ .skm-embed--dark .skm-embed__controls {
11843
+ border-top-color: #3a2a12;
11844
+ background: rgba(26, 18, 8, 0.9);
11845
+ }
11846
+
11847
+ .skm-embed__controls.is-hidden {
11848
+ display: none;
11849
+ }
11850
+
11851
+ .skm-embed__button {
11852
+ border: 1px solid #caba98;
11853
+ background: #f5eedd;
11854
+ color: #3a2010;
11855
+ border-radius: 6px;
11856
+ padding: 5px 10px;
11857
+ font: inherit;
11858
+ font-size: 11px;
11859
+ cursor: pointer;
11860
+ transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
11861
+ }
11862
+
11863
+ .skm-embed__button:hover:not(:disabled) {
11864
+ background: #c8a060;
11865
+ border-color: #c8a060;
11866
+ color: #fff;
11867
+ }
11868
+
11869
+ .skm-embed--dark .skm-embed__button {
11870
+ border-color: #4a3520;
11871
+ background: #22190e;
11872
+ color: #f3ddaf;
11873
+ }
11874
+
11875
+ .skm-embed--dark .skm-embed__button:hover:not(:disabled) {
11876
+ background: #c8a060;
11877
+ border-color: #c8a060;
11878
+ color: #fff;
11879
+ }
11880
+
11881
+ .skm-embed__button:disabled {
11882
+ opacity: 0.45;
11883
+ cursor: default;
11884
+ }
11885
+
11886
+ .skm-embed__step {
11887
+ margin-left: auto;
11888
+ min-width: 96px;
11889
+ text-align: center;
11890
+ color: #8a6040;
11891
+ font-size: 11px;
11892
+ }
11893
+
11894
+ .skm-embed--dark .skm-embed__step {
11895
+ color: #d0b176;
11896
+ }
11897
+ `;
11898
+ class SketchmarkEmbed {
11899
+ constructor(options) {
11900
+ this.instance = null;
11901
+ this.emitter = new EventEmitter();
11902
+ this.animUnsub = null;
11903
+ this.playInFlight = false;
11904
+ this.offsetX = 0;
11905
+ this.offsetY = 0;
11906
+ this.motionFrame = null;
11907
+ this.resizeObserver = null;
11908
+ this.options = options;
11909
+ this.dsl = normalizeNewlines(options.dsl);
11910
+ this.theme = options.theme ?? "light";
11911
+ injectStyleOnce(EMBED_STYLE_ID, EMBED_CSS);
11912
+ const host = resolveContainer(options.container);
11913
+ host.innerHTML = "";
11914
+ this.root = document.createElement("div");
11915
+ this.root.className = "skm-embed";
11916
+ this.root.classList.toggle("skm-embed--dark", this.theme === "dark");
11917
+ this.applySize(options.width, options.height);
11918
+ this.root.innerHTML = `
11919
+ <div class="skm-embed__viewport">
11920
+ <div class="skm-embed__world">
11921
+ <div class="skm-embed__diagram"></div>
11922
+ </div>
11923
+ </div>
11924
+ <div class="skm-embed__error"></div>
11925
+ <div class="skm-embed__controls">
11926
+ <button type="button" class="skm-embed__button" data-action="reset">Reset</button>
11927
+ <button type="button" class="skm-embed__button" data-action="prev">Prev</button>
11928
+ <button type="button" class="skm-embed__button" data-action="next">Next</button>
11929
+ <button type="button" class="skm-embed__button" data-action="play">Play</button>
11930
+ <span class="skm-embed__step">No steps</span>
11931
+ </div>
11932
+ `;
11933
+ this.viewport = this.root.querySelector(".skm-embed__viewport");
11934
+ this.world = this.root.querySelector(".skm-embed__world");
11935
+ this.diagramWrap = this.root.querySelector(".skm-embed__diagram");
11936
+ this.errorElement = this.root.querySelector(".skm-embed__error");
11937
+ this.controlsElement = this.root.querySelector(".skm-embed__controls");
11938
+ this.stepInfoElement = this.root.querySelector(".skm-embed__step");
11939
+ this.btnReset = this.root.querySelector('[data-action="reset"]');
11940
+ this.btnPrev = this.root.querySelector('[data-action="prev"]');
11941
+ this.btnNext = this.root.querySelector('[data-action="next"]');
11942
+ this.btnPlay = this.root.querySelector('[data-action="play"]');
11943
+ this.controlsElement.classList.toggle("is-hidden", options.showControls === false);
11944
+ this.btnReset.addEventListener("click", () => this.resetAnimation());
11945
+ this.btnPrev.addEventListener("click", () => this.prevStep());
11946
+ this.btnNext.addEventListener("click", () => this.nextStep());
11947
+ this.btnPlay.addEventListener("click", () => {
11948
+ void this.play();
11949
+ });
11950
+ if (typeof ResizeObserver !== "undefined") {
11951
+ this.resizeObserver = new ResizeObserver(() => {
11952
+ this.positionViewport(false);
11953
+ });
11954
+ this.resizeObserver.observe(this.viewport);
11955
+ }
11956
+ host.appendChild(this.root);
11957
+ this.render();
11958
+ }
11959
+ getDsl() {
11960
+ return this.dsl;
11961
+ }
11962
+ setDsl(dsl, renderNow = false) {
11963
+ this.dsl = normalizeNewlines(dsl);
11964
+ if (renderNow)
11965
+ this.render();
11966
+ }
11967
+ setSize(width, height) {
11968
+ this.applySize(width, height);
11969
+ this.positionViewport(false);
11970
+ }
11971
+ setTheme(theme) {
11972
+ this.theme = theme;
11973
+ this.root.classList.toggle("skm-embed--dark", theme === "dark");
11974
+ this.render();
11975
+ }
11976
+ on(event, listener) {
11977
+ this.emitter.on(event, listener);
11978
+ return () => this.emitter.off(event, listener);
11979
+ }
11980
+ render(nextDsl) {
11981
+ if (typeof nextDsl === "string") {
11982
+ this.dsl = normalizeNewlines(nextDsl);
11983
+ }
11984
+ this.clearError();
11985
+ this.stopMotion();
11986
+ this.animUnsub?.();
11987
+ this.animUnsub = null;
11988
+ this.instance?.anim?.destroy();
11989
+ this.instance = null;
11990
+ this.diagramWrap.innerHTML = "";
11991
+ try {
11992
+ const instance = render({
11993
+ container: this.diagramWrap,
11994
+ dsl: this.dsl,
11995
+ renderer: "svg",
11996
+ svgOptions: {
11997
+ showTitle: true,
11998
+ interactive: true,
11999
+ transparent: true,
12000
+ theme: this.options.svgOptions?.theme ?? this.theme,
12001
+ ...this.options.svgOptions,
12002
+ },
12003
+ onNodeClick: this.options.onNodeClick,
12004
+ });
12005
+ this.instance = instance;
12006
+ this.animUnsub = instance.anim.on((event) => {
12007
+ this.syncControls();
12008
+ if (event.type === "step-change") {
12009
+ if (this.options.autoFocusOnStep !== false) {
12010
+ requestAnimationFrame(() => {
12011
+ window.setTimeout(() => this.positionViewport(true), 40);
12012
+ });
12013
+ }
12014
+ this.emitter.emit("stepchange", {
12015
+ stepIndex: event.stepIndex,
12016
+ step: event.step,
12017
+ embed: this,
12018
+ });
12019
+ }
12020
+ });
12021
+ this.syncControls();
12022
+ requestAnimationFrame(() => {
12023
+ this.positionViewport(false);
12024
+ });
12025
+ this.options.onRender?.(instance, this);
12026
+ this.emitter.emit("render", { instance, embed: this });
12027
+ return instance;
12028
+ }
12029
+ catch (error) {
12030
+ const normalized = toError(error);
12031
+ this.showError(normalized.message);
12032
+ this.syncControls();
12033
+ this.emitter.emit("error", { error: normalized, embed: this });
12034
+ return null;
12035
+ }
12036
+ }
12037
+ async play() {
12038
+ if (!this.instance || this.playInFlight || !this.instance.anim.total)
12039
+ return;
12040
+ this.playInFlight = true;
12041
+ this.syncControls();
12042
+ try {
12043
+ await this.instance.anim.play(this.options.playStepDelay ?? 800);
12044
+ }
12045
+ finally {
12046
+ this.playInFlight = false;
12047
+ this.syncControls();
12048
+ }
12049
+ }
12050
+ nextStep() {
12051
+ if (!this.instance)
12052
+ return;
12053
+ this.instance.anim.next();
12054
+ this.syncControls();
12055
+ this.positionViewport(true);
12056
+ }
12057
+ prevStep() {
12058
+ if (!this.instance)
12059
+ return;
12060
+ this.instance.anim.prev();
12061
+ this.syncControls();
12062
+ this.positionViewport(true);
12063
+ }
12064
+ resetAnimation() {
12065
+ if (!this.instance)
12066
+ return;
12067
+ this.instance.anim.reset();
12068
+ this.syncControls();
12069
+ this.positionViewport(true);
12070
+ }
12071
+ exportSVG(filename) {
12072
+ this.instance?.exportSVG(filename);
12073
+ }
12074
+ async exportPNG(filename) {
12075
+ await this.instance?.exportPNG(filename);
12076
+ }
12077
+ destroy() {
12078
+ this.stopMotion();
12079
+ this.animUnsub?.();
12080
+ this.instance?.anim?.destroy();
12081
+ this.instance = null;
12082
+ this.resizeObserver?.disconnect();
12083
+ this.root.remove();
12084
+ }
12085
+ applySize(width, height) {
12086
+ this.root.style.width = this.formatSize(width ?? 960);
12087
+ this.root.style.height = this.formatSize(height ?? 540);
12088
+ }
12089
+ formatSize(value) {
12090
+ return typeof value === "number" ? `${value}px` : value;
12091
+ }
12092
+ syncControls() {
12093
+ const anim = this.instance?.anim;
12094
+ if (!anim || !anim.total) {
12095
+ this.stepInfoElement.textContent = "No steps";
12096
+ this.btnReset.disabled = true;
12097
+ this.btnPrev.disabled = true;
12098
+ this.btnNext.disabled = true;
12099
+ this.btnPlay.disabled = true;
12100
+ return;
12101
+ }
12102
+ this.stepInfoElement.textContent =
12103
+ anim.currentStep < 0 ? `${anim.total} steps` : `${anim.currentStep + 1} / ${anim.total}`;
12104
+ this.btnReset.disabled = false;
12105
+ this.btnPrev.disabled = !anim.canPrev;
12106
+ this.btnNext.disabled = !anim.canNext;
12107
+ this.btnPlay.disabled = this.playInFlight || !anim.canNext;
12108
+ }
12109
+ positionViewport(animated) {
12110
+ if (!this.instance?.svg)
12111
+ return;
12112
+ const svg = this.instance.svg;
12113
+ const svgWidth = parseFloat(svg.getAttribute("width") || "0");
12114
+ const svgHeight = parseFloat(svg.getAttribute("height") || "0");
12115
+ if (!svgWidth || !svgHeight)
12116
+ return;
12117
+ const viewportRect = this.viewport.getBoundingClientRect();
12118
+ const viewWidth = viewportRect.width || this.viewport.clientWidth;
12119
+ const viewHeight = viewportRect.height || this.viewport.clientHeight;
12120
+ if (!viewWidth || !viewHeight)
12121
+ return;
12122
+ const sceneIsLarge = svgWidth > viewWidth || svgHeight > viewHeight;
12123
+ const shouldFocus = sceneIsLarge &&
12124
+ this.options.autoFocus !== false &&
12125
+ !!this.getFocusTarget();
12126
+ if (!shouldFocus) {
12127
+ this.animateTo(svgWidth <= viewWidth ? (viewWidth - svgWidth) / 2 : 0, svgHeight <= viewHeight ? (viewHeight - svgHeight) / 2 : 0, animated);
12128
+ return;
12129
+ }
12130
+ const target = this.findTargetElement(this.getFocusTarget());
12131
+ if (!target) {
12132
+ this.animateTo(0, 0, animated);
12133
+ return;
12134
+ }
12135
+ const currentRect = target.getBoundingClientRect();
12136
+ const sceneX = currentRect.left - viewportRect.left - this.offsetX;
12137
+ const sceneY = currentRect.top - viewportRect.top - this.offsetY;
12138
+ const targetCenterX = sceneX + currentRect.width / 2;
12139
+ const targetCenterY = sceneY + currentRect.height / 2;
12140
+ let nextX = viewWidth / 2 - targetCenterX;
12141
+ let nextY = viewHeight / 2 - targetCenterY;
12142
+ const padding = this.options.focusPadding ?? 24;
12143
+ if (svgWidth <= viewWidth) {
12144
+ nextX = (viewWidth - svgWidth) / 2;
12145
+ }
12146
+ else {
12147
+ nextX = clamp(nextX, viewWidth - svgWidth - padding, padding);
12148
+ }
12149
+ if (svgHeight <= viewHeight) {
12150
+ nextY = (viewHeight - svgHeight) / 2;
12151
+ }
12152
+ else {
12153
+ nextY = clamp(nextY, viewHeight - svgHeight - padding, padding);
12154
+ }
12155
+ this.animateTo(nextX, nextY, animated);
12156
+ }
12157
+ animateTo(nextX, nextY, animated) {
12158
+ this.stopMotion();
12159
+ const duration = this.options.focusDuration ?? 320;
12160
+ if (!animated || duration <= 0) {
12161
+ this.offsetX = nextX;
12162
+ this.offsetY = nextY;
12163
+ this.applyTransform();
12164
+ return;
12165
+ }
12166
+ const startX = this.offsetX;
12167
+ const startY = this.offsetY;
12168
+ const start = performance.now();
12169
+ const frame = (now) => {
12170
+ const t = Math.min((now - start) / duration, 1);
12171
+ const eased = 1 - Math.pow(1 - t, 3);
12172
+ this.offsetX = startX + (nextX - startX) * eased;
12173
+ this.offsetY = startY + (nextY - startY) * eased;
12174
+ this.applyTransform();
12175
+ if (t < 1) {
12176
+ this.motionFrame = requestAnimationFrame(frame);
12177
+ }
12178
+ else {
12179
+ this.motionFrame = null;
12180
+ }
12181
+ };
12182
+ this.motionFrame = requestAnimationFrame(frame);
12183
+ }
12184
+ applyTransform() {
12185
+ this.world.style.transform = `translate(${this.offsetX}px, ${this.offsetY}px)`;
12186
+ }
12187
+ getFocusTarget() {
12188
+ const anim = this.instance?.anim;
12189
+ if (!anim || !anim.total)
12190
+ return null;
12191
+ const startIndex = anim.currentStep >= 0 ? anim.currentStep : 0;
12192
+ for (let index = startIndex; index < anim.steps.length; index += 1) {
12193
+ const target = this.getStepTarget(anim.steps[index]);
12194
+ if (target)
12195
+ return target;
12196
+ }
12197
+ for (let index = startIndex - 1; index >= 0; index -= 1) {
12198
+ const target = this.getStepTarget(anim.steps[index]);
12199
+ if (target)
12200
+ return target;
12201
+ }
12202
+ return null;
12203
+ }
12204
+ findTargetElement(targetId) {
12205
+ const svg = this.instance?.svg;
12206
+ if (!svg)
12207
+ return null;
12208
+ const edgeTarget = this.parseEdgeTarget(targetId);
12209
+ const esc = typeof CSS !== "undefined" && typeof CSS.escape === "function"
12210
+ ? CSS.escape
12211
+ : (value) => value;
12212
+ if (edgeTarget) {
12213
+ const edgeEl = svg.querySelector(`#${esc(`edge-${edgeTarget.from}-${edgeTarget.to}`)}`);
12214
+ if (edgeEl)
12215
+ return edgeEl;
12216
+ }
12217
+ const ids = this.splitEdgeTarget(targetId);
12218
+ const prefixes = ["group-", "node-", "edge-", "table-", "chart-", "markdown-", "note-", ""];
12219
+ for (const id of ids) {
12220
+ for (const prefix of prefixes) {
12221
+ const found = svg.querySelector(`#${esc(prefix + id)}`);
12222
+ if (found)
12223
+ return found;
12224
+ }
12225
+ for (const attr of ["data-id", "data-node", "data-group", "sketchmark-id"]) {
12226
+ const found = svg.querySelector(`[${attr}="${id}"]`);
12227
+ if (found)
12228
+ return found;
12229
+ }
12230
+ }
12231
+ return null;
12232
+ }
12233
+ getStepTarget(stepItem) {
12234
+ if (!stepItem)
12235
+ return null;
12236
+ return stepItem.kind === "beat" ? stepItem.children?.[0]?.target ?? null : stepItem.target ?? null;
12237
+ }
12238
+ parseEdgeTarget(targetId) {
12239
+ const connectors = ["<-->", "<->", "-->", "<--", "---", "--", "->", "<-"];
12240
+ for (const connector of connectors) {
12241
+ if (targetId.includes(connector)) {
12242
+ const [from, to] = targetId.split(connector).map((part) => part.trim());
12243
+ if (from && to)
12244
+ return { from, to };
12245
+ }
12246
+ }
12247
+ return null;
12248
+ }
12249
+ splitEdgeTarget(targetId) {
12250
+ const connectors = ["<-->", "<->", "-->", "<--", "---", "--", "->", "<-"];
12251
+ for (const connector of connectors) {
12252
+ if (targetId.includes(connector)) {
12253
+ return targetId.split(connector).map((part) => part.trim()).filter(Boolean);
12254
+ }
12255
+ }
12256
+ return [targetId.trim()];
12257
+ }
12258
+ showError(message) {
12259
+ this.errorElement.textContent = message;
12260
+ this.errorElement.classList.add("is-visible");
12261
+ }
12262
+ clearError() {
12263
+ this.errorElement.textContent = "";
12264
+ this.errorElement.classList.remove("is-visible");
12265
+ }
12266
+ stopMotion() {
12267
+ if (this.motionFrame === null)
12268
+ return;
12269
+ cancelAnimationFrame(this.motionFrame);
12270
+ this.motionFrame = null;
12271
+ }
12272
+ }
12273
+
12274
+ export { ANIMATION_CSS, AnimationController, BUILTIN_FONTS, EventEmitter, PALETTES, ParseError, SketchmarkCanvas, SketchmarkEditor, SketchmarkEmbed, THEME_CONFIG_KEY, THEME_NAMES, buildSceneGraph, canvasToPNGBlob, canvasToPNGDataURL, clamp, connPoint, debounce, exportCanvasPNG, exportGIF, exportHTML, exportMP4, exportPNG, exportSVG, getSVGBlob, groupMap, hashStr, layout, lerp, listThemes, loadFont, markdownMap, nodeMap, parse, parseHex, registerFont, render, renderToCanvas, renderToSVG, resolveFont, resolvePalette, sleep, svgToPNGDataURL, svgToString, throttle };
10757
12275
  //# sourceMappingURL=index.js.map