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/README.md +46 -9
- package/dist/index.cjs +1549 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +25 -50
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1547 -29
- package/dist/index.js.map +1 -1
- package/dist/render.d.ts +25 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/sketchmark.iife.js +1549 -28
- package/dist/ui/canvas.d.ts +129 -0
- package/dist/ui/canvas.d.ts.map +1 -0
- package/dist/ui/editor.d.ts +52 -0
- package/dist/ui/editor.d.ts.map +1 -0
- package/dist/ui/embed.d.ts +91 -0
- package/dist/ui/embed.d.ts.map +1 -0
- package/dist/ui/shared.d.ts +6 -0
- package/dist/ui/shared.d.ts.map +1 -0
- package/package.json +1 -1
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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 =
|
|
10691
|
-
|
|
10692
|
-
|
|
10693
|
-
|
|
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 ===
|
|
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 ===
|
|
10708
|
+
if (renderer === "canvas") {
|
|
10716
10709
|
canvas = el instanceof HTMLCanvasElement
|
|
10717
10710
|
? el
|
|
10718
|
-
: (() => {
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
10739
|
-
scene,
|
|
10740
|
-
|
|
10741
|
-
|
|
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(
|
|
10747
|
+
Promise.resolve().then(function () { return index; }).then((mod) => mod.exportSVG(svg, { filename }));
|
|
10744
10748
|
}
|
|
10745
10749
|
},
|
|
10746
|
-
exportPNG: async (filename =
|
|
10750
|
+
exportPNG: async (filename = "diagram.png") => {
|
|
10747
10751
|
if (svg) {
|
|
10748
|
-
const
|
|
10749
|
-
await
|
|
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
|
-
|
|
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, "&")
|
|
11556
|
+
.replace(/</g, "<")
|
|
11557
|
+
.replace(/>/g, ">")
|
|
11558
|
+
.replace(/"/g, """);
|
|
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
|