@markdy/renderer-dom 0.8.25 → 0.8.26

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.d.ts CHANGED
@@ -81,6 +81,17 @@ interface SvgExportOptions {
81
81
  }
82
82
  declare function exportDiagramAsVectorSvg(containerEl: HTMLElement, options?: SvgExportOptions): string;
83
83
 
84
+ /**
85
+ * packages/renderer-dom/src/export/png-exporter.ts
86
+ * High-DPI raster PNG export with 2x retina scaling.
87
+ * Zero external dependencies.
88
+ */
89
+
90
+ interface PngExportOptions extends SvgExportOptions {
91
+ pixelRatio?: number;
92
+ }
93
+ declare function exportDiagramAsPng(containerEl: HTMLElement, options?: PngExportOptions): Promise<Blob>;
94
+
84
95
  /**
85
96
  * packages/renderer-dom/src/presentation-controller.ts
86
97
  * Interactive Beat Navigation & Keyboard-driven presentation controller.
@@ -95,13 +106,17 @@ declare class DiagramPresentationController {
95
106
  private diagram;
96
107
  private plan;
97
108
  private currentBeatIndex;
109
+ private options;
110
+ private isDestroyed;
98
111
  constructor(diagram: Diagram, plan: RenderPlan, options?: ControllerOptions);
99
112
  nextBeat(): void;
100
113
  prevBeat(): void;
114
+ private applyCurrentBeat;
101
115
  getCurrentBeatIndex(): number;
102
116
  togglePlay(): void;
103
117
  setSpeed(rate: number): void;
104
- private attachKeyboardListener;
118
+ private onKeyDown;
119
+ destroy(): void;
105
120
  }
106
121
 
107
- export { type AnimationRecordFrame, type ControllerOptions, type Diagram, type DiagramOptions, DiagramPresentationController, type GifExportOptions, ICON_REGISTRY, type IconSpec, type SvgExportOptions, createDiagram, encodeGifSequence, exportDiagramAsVectorSvg };
122
+ export { type AnimationRecordFrame, type ControllerOptions, type Diagram, type DiagramOptions, DiagramPresentationController, type GifExportOptions, ICON_REGISTRY, type IconSpec, type PngExportOptions, type SvgExportOptions, createDiagram, encodeGifSequence, exportDiagramAsPng, exportDiagramAsVectorSvg };
package/dist/index.js CHANGED
@@ -2694,7 +2694,7 @@ function lzwCompress(pixels, minCodeSize = 8) {
2694
2694
  if (nextCode === 1 << codeSize && codeSize < 12) {
2695
2695
  codeSize++;
2696
2696
  }
2697
- if (nextCode >= 4095) {
2697
+ if (nextCode >= 4096) {
2698
2698
  codes.push(clearCode);
2699
2699
  dict = /* @__PURE__ */ new Map();
2700
2700
  for (let j = 0; j < clearCode; j++) dict.set(String(j), j);
@@ -2785,31 +2785,96 @@ function encodeGifSequence(frames, options = {}) {
2785
2785
 
2786
2786
  // src/export/svg-exporter.ts
2787
2787
  function exportDiagramAsVectorSvg(containerEl, options = {}) {
2788
- const svgEl = containerEl.querySelector("svg");
2789
- if (!svgEl) throw new Error("No Markdy SVG element found in container");
2790
- const clonedSvg = svgEl.cloneNode(true);
2791
- const width = svgEl.getAttribute("width") || String(svgEl.clientWidth || 800);
2792
- const height = svgEl.getAttribute("height") || String(svgEl.clientHeight || 400);
2793
- clonedSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
2794
- clonedSvg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
2795
- clonedSvg.setAttribute("width", width);
2796
- clonedSvg.setAttribute("height", height);
2797
- clonedSvg.setAttribute("viewBox", `0 0 ${width} ${height}`);
2788
+ const sceneEl = containerEl.classList?.contains("markdy-scene-root") ? containerEl : containerEl.querySelector(".markdy-scene-root") || containerEl.querySelector("svg") || (containerEl.tagName?.toLowerCase() === "svg" ? containerEl : null);
2789
+ if (!sceneEl) throw new Error("No Markdy scene element found in container");
2790
+ if (sceneEl.tagName?.toLowerCase() === "svg") {
2791
+ const clonedSvg = sceneEl.cloneNode(true);
2792
+ if (!clonedSvg.getAttribute("xmlns")) {
2793
+ clonedSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
2794
+ }
2795
+ const serializer2 = new XMLSerializer();
2796
+ return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2797
+ ${serializer2.serializeToString(clonedSvg)}`;
2798
+ }
2799
+ const clonedScene = sceneEl.cloneNode(true);
2800
+ const widthStr = clonedScene.style.width || String(sceneEl.clientWidth || 800);
2801
+ const heightStr = clonedScene.style.height || String(sceneEl.clientHeight || 400);
2802
+ let width = parseFloat(widthStr);
2803
+ let height = parseFloat(heightStr);
2804
+ if (isNaN(width)) width = 800;
2805
+ if (isNaN(height)) height = 400;
2806
+ const scale = options.scale || 1;
2807
+ const scaledWidth = width * scale;
2808
+ const scaledHeight = height * scale;
2809
+ clonedScene.style.transform = `scale(${scale})`;
2810
+ clonedScene.style.transformOrigin = "0 0";
2811
+ clonedScene.style.position = "relative";
2812
+ clonedScene.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
2798
2813
  if (options.transparentBackground) {
2799
- const bgRect = clonedSvg.querySelector("rect");
2800
- if (bgRect) bgRect.remove();
2814
+ clonedScene.style.background = "transparent";
2801
2815
  }
2816
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2817
+ svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
2818
+ svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
2819
+ svg.setAttribute("width", String(scaledWidth));
2820
+ svg.setAttribute("height", String(scaledHeight));
2821
+ svg.setAttribute("viewBox", `0 0 ${scaledWidth} ${scaledHeight}`);
2802
2822
  if (options.includeThemeStyles !== false) {
2803
2823
  const styleEl = document.createElementNS("http://www.w3.org/2000/svg", "style");
2804
- styleEl.textContent = `
2805
- text { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
2824
+ let combinedStyles = `
2825
+ foreignObject { width: 100%; height: 100%; }
2806
2826
  .markdy-node { transition: opacity 0.3s ease; }
2807
2827
  `;
2808
- clonedSvg.insertBefore(styleEl, clonedSvg.firstChild);
2809
- }
2828
+ if (typeof document !== "undefined") {
2829
+ const styles = document.querySelectorAll("style[id^='markdy-']");
2830
+ for (let i = 0; i < styles.length; i++) {
2831
+ combinedStyles += styles[i].textContent + "\n";
2832
+ }
2833
+ }
2834
+ styleEl.textContent = combinedStyles;
2835
+ svg.appendChild(styleEl);
2836
+ }
2837
+ const foreignObject = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
2838
+ foreignObject.setAttribute("width", "100%");
2839
+ foreignObject.setAttribute("height", "100%");
2840
+ foreignObject.appendChild(clonedScene);
2841
+ svg.appendChild(foreignObject);
2810
2842
  const serializer = new XMLSerializer();
2811
2843
  return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2812
- ` + serializer.serializeToString(clonedSvg);
2844
+ ` + serializer.serializeToString(svg);
2845
+ }
2846
+
2847
+ // src/export/png-exporter.ts
2848
+ async function exportDiagramAsPng(containerEl, options = {}) {
2849
+ const svgXml = exportDiagramAsVectorSvg(containerEl, options);
2850
+ const pixelRatio = options.pixelRatio || 2;
2851
+ const blob = new Blob([svgXml], { type: "image/svg+xml;charset=utf-8" });
2852
+ const url = URL.createObjectURL(blob);
2853
+ const img = new Image();
2854
+ await new Promise((resolve, reject) => {
2855
+ img.onload = () => resolve();
2856
+ img.onerror = () => reject(new Error("Failed to rasterize SVG into Image for PNG export"));
2857
+ img.src = url;
2858
+ });
2859
+ const width = img.naturalWidth || 800;
2860
+ const height = img.naturalHeight || 400;
2861
+ const canvas = document.createElement("canvas");
2862
+ canvas.width = width * pixelRatio;
2863
+ canvas.height = height * pixelRatio;
2864
+ const ctx = canvas.getContext("2d");
2865
+ if (!ctx) {
2866
+ URL.revokeObjectURL(url);
2867
+ throw new Error("Could not get 2D canvas context for PNG export");
2868
+ }
2869
+ ctx.scale(pixelRatio, pixelRatio);
2870
+ ctx.drawImage(img, 0, 0, width, height);
2871
+ URL.revokeObjectURL(url);
2872
+ return new Promise((resolve, reject) => {
2873
+ canvas.toBlob((b) => {
2874
+ if (b) resolve(b);
2875
+ else reject(new Error("Canvas toBlob failed for PNG export"));
2876
+ }, "image/png");
2877
+ });
2813
2878
  }
2814
2879
 
2815
2880
  // src/presentation-controller.ts
@@ -2817,60 +2882,77 @@ var DiagramPresentationController = class {
2817
2882
  diagram;
2818
2883
  plan;
2819
2884
  currentBeatIndex = 0;
2885
+ options;
2886
+ isDestroyed = false;
2820
2887
  constructor(diagram, plan, options = {}) {
2821
2888
  this.diagram = diagram;
2822
2889
  this.plan = plan;
2890
+ this.options = options;
2823
2891
  if (options.enableKeyboard !== false && typeof window !== "undefined") {
2824
- this.attachKeyboardListener();
2892
+ window.addEventListener("keydown", this.onKeyDown);
2825
2893
  }
2826
2894
  }
2827
2895
  nextBeat() {
2828
2896
  if (this.currentBeatIndex < this.plan.beats.length - 1) {
2829
2897
  this.currentBeatIndex++;
2830
- const beat = this.plan.beats[this.currentBeatIndex];
2831
- this.diagram.seek(beat.start);
2898
+ this.applyCurrentBeat();
2832
2899
  }
2833
2900
  }
2834
2901
  prevBeat() {
2835
2902
  if (this.currentBeatIndex > 0) {
2836
2903
  this.currentBeatIndex--;
2837
- const beat = this.plan.beats[this.currentBeatIndex];
2838
- this.diagram.seek(beat.start);
2904
+ this.applyCurrentBeat();
2905
+ }
2906
+ }
2907
+ applyCurrentBeat() {
2908
+ const beat = this.plan.beats[this.currentBeatIndex];
2909
+ this.diagram.seek(beat.start);
2910
+ if (this.options.onBeatChange) {
2911
+ this.options.onBeatChange(beat.name, this.currentBeatIndex);
2839
2912
  }
2840
2913
  }
2841
2914
  getCurrentBeatIndex() {
2842
2915
  return this.currentBeatIndex;
2843
2916
  }
2844
2917
  togglePlay() {
2845
- this.diagram.play();
2918
+ if (this.diagram.isPlaying()) {
2919
+ this.diagram.pause();
2920
+ } else {
2921
+ this.diagram.play();
2922
+ }
2846
2923
  }
2847
2924
  setSpeed(rate) {
2848
2925
  this.diagram.setPlaybackRate(rate);
2849
2926
  }
2850
- attachKeyboardListener() {
2851
- window.addEventListener("keydown", (e) => {
2852
- if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
2853
- switch (e.key) {
2854
- case "ArrowRight":
2855
- case "PageDown":
2856
- this.nextBeat();
2857
- break;
2858
- case "ArrowLeft":
2859
- case "PageUp":
2860
- this.prevBeat();
2861
- break;
2862
- case " ":
2863
- e.preventDefault();
2864
- this.togglePlay();
2865
- break;
2866
- case "1":
2867
- this.setSpeed(1);
2868
- break;
2869
- case "2":
2870
- this.setSpeed(2);
2871
- break;
2872
- }
2873
- });
2927
+ onKeyDown = (e) => {
2928
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
2929
+ switch (e.key) {
2930
+ case "ArrowRight":
2931
+ case "PageDown":
2932
+ this.nextBeat();
2933
+ break;
2934
+ case "ArrowLeft":
2935
+ case "PageUp":
2936
+ this.prevBeat();
2937
+ break;
2938
+ case " ":
2939
+ e.preventDefault();
2940
+ this.togglePlay();
2941
+ break;
2942
+ case "1":
2943
+ this.setSpeed(1);
2944
+ break;
2945
+ case "2":
2946
+ this.setSpeed(2);
2947
+ break;
2948
+ }
2949
+ };
2950
+ destroy() {
2951
+ if (this.isDestroyed) return;
2952
+ this.isDestroyed = true;
2953
+ if (typeof window !== "undefined") {
2954
+ window.removeEventListener("keydown", this.onKeyDown);
2955
+ }
2874
2956
  }
2875
2957
  };
2876
2958
  export {
@@ -2878,5 +2960,6 @@ export {
2878
2960
  ICON_REGISTRY,
2879
2961
  createDiagram,
2880
2962
  encodeGifSequence,
2963
+ exportDiagramAsPng,
2881
2964
  exportDiagramAsVectorSvg
2882
2965
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "0.8.25",
3
+ "version": "0.8.26",
4
4
  "description": "Browser renderer for diagram-native animated MarkdyScript architecture diagrams, built on the Web Animations API.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -44,14 +44,14 @@
44
44
  "access": "public"
45
45
  },
46
46
  "dependencies": {
47
- "@markdy/core": "0.8.25"
47
+ "@markdy/core": "0.8.26"
48
48
  },
49
49
  "devDependencies": {
50
50
  "jsdom": "^29.1.1",
51
51
  "tsup": "^8.5.1",
52
52
  "typescript": "^5.9.3",
53
53
  "vitest": "^4.1.7",
54
- "@markdy/stdlib-systems": "0.8.25"
54
+ "@markdy/stdlib-systems": "0.8.26"
55
55
  },
56
56
  "scripts": {
57
57
  "build": "tsup",