@markdy/renderer-dom 0.8.27 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -33,6 +33,7 @@ interface DiagramOptions {
33
33
  onWarning?: (warning: Diagnostic) => void;
34
34
  onTimeUpdate?: (seconds: number, durationSeconds: number) => void;
35
35
  onPlayStateChange?: (playing: boolean) => void;
36
+ onEnded?: () => void;
36
37
  }
37
38
  interface Diagram {
38
39
  play(): void;
@@ -81,10 +82,30 @@ interface SvgExportOptions {
81
82
  }
82
83
  declare function exportDiagramAsVectorSvg(containerEl: HTMLElement, options?: SvgExportOptions): string;
83
84
 
85
+ interface TimelineController {
86
+ seek(seconds: number): void;
87
+ currentTime(): number;
88
+ duration(): number;
89
+ isPlaying(): boolean;
90
+ play(): void;
91
+ pause(): void;
92
+ }
93
+ interface GifDiagramExportOptions extends SvgExportOptions {
94
+ fps?: number;
95
+ pixelRatio?: number;
96
+ loop?: boolean;
97
+ }
98
+ declare function exportDiagramAsGif(container: HTMLElement, timeline: TimelineController, options?: GifDiagramExportOptions): Promise<Blob>;
99
+
84
100
  /**
85
101
  * packages/renderer-dom/src/export/png-exporter.ts
86
102
  * High-DPI raster PNG export with 2x retina scaling.
87
103
  * Zero external dependencies.
104
+ *
105
+ * Fix: Inline all external resources (images, fonts) as base64 data URIs
106
+ * before drawing the SVG to canvas. A foreignObject-wrapped SVG taints the
107
+ * canvas whenever it references any external URL, so every resource must be
108
+ * inlined first.
88
109
  */
89
110
 
90
111
  interface PngExportOptions extends SvgExportOptions {
@@ -119,4 +140,4 @@ declare class DiagramPresentationController {
119
140
  destroy(): void;
120
141
  }
121
142
 
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 };
143
+ export { type AnimationRecordFrame, type ControllerOptions, type Diagram, type DiagramOptions, DiagramPresentationController, type GifDiagramExportOptions, type GifExportOptions, ICON_REGISTRY, type IconSpec, type PngExportOptions, type SvgExportOptions, type TimelineController, createDiagram, encodeGifSequence, exportDiagramAsGif, exportDiagramAsPng, exportDiagramAsVectorSvg };
package/dist/index.js CHANGED
@@ -2091,7 +2091,8 @@ function createDiagram(opts) {
2091
2091
  copyright: explicitCopyright,
2092
2092
  onWarning = (w) => console.warn(`[markdy] line ${w.line}: ${w.message}`),
2093
2093
  onTimeUpdate,
2094
- onPlayStateChange
2094
+ onPlayStateChange,
2095
+ onEnded
2095
2096
  } = opts;
2096
2097
  const { ast, plan } = parseAndCompile(code);
2097
2098
  for (const w of ast.diagnostics) {
@@ -2503,6 +2504,7 @@ function createDiagram(opts) {
2503
2504
  emitPlayStateChange(false);
2504
2505
  lastRafTs = null;
2505
2506
  rafId = null;
2507
+ onEnded?.();
2506
2508
  return;
2507
2509
  }
2508
2510
  }
@@ -2861,6 +2863,19 @@ function encodeGifSequence(frames, options = {}) {
2861
2863
  }
2862
2864
 
2863
2865
  // src/export/svg-exporter.ts
2866
+ function copyRenderedStyles(source, clone) {
2867
+ if (typeof window === "undefined" || typeof window.getComputedStyle !== "function") return;
2868
+ const sourceElements = [source, ...Array.from(source.querySelectorAll("*"))];
2869
+ const cloneElements = [clone, ...Array.from(clone.querySelectorAll("*"))];
2870
+ for (let index = 0; index < Math.min(sourceElements.length, cloneElements.length); index++) {
2871
+ const computed = window.getComputedStyle(sourceElements[index]);
2872
+ const target = cloneElements[index].style;
2873
+ for (let propertyIndex = 0; propertyIndex < computed.length; propertyIndex++) {
2874
+ const property = computed.item(propertyIndex);
2875
+ target.setProperty(property, computed.getPropertyValue(property), computed.getPropertyPriority(property));
2876
+ }
2877
+ }
2878
+ }
2864
2879
  function exportDiagramAsVectorSvg(containerEl, options = {}) {
2865
2880
  const sceneEl = containerEl.classList?.contains("markdy-scene-root") ? containerEl : containerEl.querySelector(".markdy-scene-root") || containerEl.querySelector("svg") || (containerEl.tagName?.toLowerCase() === "svg" ? containerEl : null);
2866
2881
  if (!sceneEl) throw new Error("No Markdy scene element found in container");
@@ -2874,6 +2889,7 @@ function exportDiagramAsVectorSvg(containerEl, options = {}) {
2874
2889
  ${serializer2.serializeToString(clonedSvg)}`;
2875
2890
  }
2876
2891
  const clonedScene = sceneEl.cloneNode(true);
2892
+ copyRenderedStyles(sceneEl, clonedScene);
2877
2893
  const widthStr = clonedScene.style.width || String(sceneEl.clientWidth || 800);
2878
2894
  const heightStr = clonedScene.style.height || String(sceneEl.clientHeight || 400);
2879
2895
  let width = parseFloat(widthStr);
@@ -2923,10 +2939,130 @@ ${serializer2.serializeToString(clonedSvg)}`;
2923
2939
  ` + serializer.serializeToString(svg);
2924
2940
  }
2925
2941
 
2942
+ // src/export/inline-resources.ts
2943
+ async function toDataUri(url) {
2944
+ try {
2945
+ const resp = await fetch(url, { mode: "cors", credentials: "same-origin" });
2946
+ if (!resp.ok) return null;
2947
+ const arrayBuffer = await resp.arrayBuffer();
2948
+ const mimeType = resp.headers.get("Content-Type") || "application/octet-stream";
2949
+ const base64 = btoa(
2950
+ new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), "")
2951
+ );
2952
+ return `data:${mimeType};base64,${base64}`;
2953
+ } catch {
2954
+ return null;
2955
+ }
2956
+ }
2957
+ async function inlineCssUrls(cssValue) {
2958
+ const urlPattern = /url\(["']?([^"')]+)["']?\)/g;
2959
+ const matches = [];
2960
+ let m;
2961
+ while ((m = urlPattern.exec(cssValue)) !== null) {
2962
+ const src = m[1];
2963
+ if (!src.startsWith("data:")) {
2964
+ matches.push({ full: m[0], src });
2965
+ }
2966
+ }
2967
+ let result = cssValue;
2968
+ await Promise.all(
2969
+ matches.map(async ({ full, src }) => {
2970
+ const absoluteSrc = new URL(src, document.baseURI).href;
2971
+ const dataUri = await toDataUri(absoluteSrc);
2972
+ if (dataUri) result = result.split(full).join(`url("${dataUri}")`);
2973
+ })
2974
+ );
2975
+ return result;
2976
+ }
2977
+ async function inlineExternalResources(root) {
2978
+ const tasks = [];
2979
+ root.querySelectorAll("img").forEach((img) => {
2980
+ const src = img.getAttribute("src");
2981
+ if (src && !src.startsWith("data:")) {
2982
+ const absoluteSrc = new URL(src, document.baseURI).href;
2983
+ tasks.push(
2984
+ toDataUri(absoluteSrc).then((dataUri) => {
2985
+ if (dataUri) img.setAttribute("src", dataUri);
2986
+ })
2987
+ );
2988
+ }
2989
+ });
2990
+ const allEls = [root, ...Array.from(root.querySelectorAll("*"))];
2991
+ allEls.forEach((el) => {
2992
+ const style = el.getAttribute("style");
2993
+ if (style && style.includes("url(")) {
2994
+ tasks.push(
2995
+ inlineCssUrls(style).then((inlined) => {
2996
+ if (inlined !== style) el.setAttribute("style", inlined);
2997
+ })
2998
+ );
2999
+ }
3000
+ });
3001
+ await Promise.all(tasks);
3002
+ }
3003
+
3004
+ // src/export/gif-exporter.ts
3005
+ var nextFrame = () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
3006
+ async function rasterizeFrame(container, pixelRatio, options) {
3007
+ const cloned = container.cloneNode(true);
3008
+ await inlineExternalResources(cloned);
3009
+ const svg = exportDiagramAsVectorSvg(cloned, options);
3010
+ const url = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml;charset=utf-8" }));
3011
+ try {
3012
+ const image = new Image();
3013
+ await new Promise((resolve, reject) => {
3014
+ image.onload = () => resolve();
3015
+ image.onerror = () => reject(new Error("Failed to rasterize GIF frame"));
3016
+ image.src = url;
3017
+ });
3018
+ const canvas = document.createElement("canvas");
3019
+ canvas.width = image.naturalWidth * pixelRatio;
3020
+ canvas.height = image.naturalHeight * pixelRatio;
3021
+ const context = canvas.getContext("2d");
3022
+ if (!context) throw new Error("Could not create GIF canvas context");
3023
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
3024
+ return context.getImageData(0, 0, canvas.width, canvas.height);
3025
+ } finally {
3026
+ URL.revokeObjectURL(url);
3027
+ }
3028
+ }
3029
+ async function exportDiagramAsGif(container, timeline, options = {}) {
3030
+ const fps = Math.min(24, Math.max(1, Math.round(options.fps ?? 12)));
3031
+ const duration = timeline.duration();
3032
+ const frameCount = Math.max(1, Math.ceil(duration * fps));
3033
+ const delayMs = Math.max(20, Math.round(1e3 / fps));
3034
+ const priorTime = timeline.currentTime();
3035
+ const wasPlaying = timeline.isPlaying();
3036
+ timeline.pause();
3037
+ try {
3038
+ const frames = [];
3039
+ for (let frame = 0; frame < frameCount; frame++) {
3040
+ timeline.seek(Math.min(duration, frame / fps));
3041
+ await nextFrame();
3042
+ frames.push({
3043
+ imageData: await rasterizeFrame(container, options.pixelRatio ?? 1, options),
3044
+ delayMs
3045
+ });
3046
+ }
3047
+ timeline.seek(duration);
3048
+ await nextFrame();
3049
+ frames.push({ imageData: await rasterizeFrame(container, options.pixelRatio ?? 1, options), delayMs: Math.max(delayMs, 800) });
3050
+ const encoded = encodeGifSequence(frames, { dither: true, loop: options.loop ?? true });
3051
+ const bytes = new Uint8Array(encoded.byteLength);
3052
+ bytes.set(encoded);
3053
+ return new Blob([bytes.buffer], { type: "image/gif" });
3054
+ } finally {
3055
+ timeline.seek(priorTime);
3056
+ if (wasPlaying) timeline.play();
3057
+ }
3058
+ }
3059
+
2926
3060
  // src/export/png-exporter.ts
2927
3061
  async function exportDiagramAsPng(containerEl, options = {}) {
2928
- const svgXml = exportDiagramAsVectorSvg(containerEl, options);
2929
3062
  const pixelRatio = options.pixelRatio || 2;
3063
+ const clonedContainer = containerEl.cloneNode(true);
3064
+ await inlineExternalResources(clonedContainer);
3065
+ const svgXml = exportDiagramAsVectorSvg(clonedContainer, options);
2930
3066
  const blob = new Blob([svgXml], { type: "image/svg+xml;charset=utf-8" });
2931
3067
  const url = URL.createObjectURL(blob);
2932
3068
  const img = new Image();
@@ -3044,6 +3180,7 @@ export {
3044
3180
  ICON_REGISTRY,
3045
3181
  createDiagram,
3046
3182
  encodeGifSequence,
3183
+ exportDiagramAsGif,
3047
3184
  exportDiagramAsPng,
3048
3185
  exportDiagramAsVectorSvg
3049
3186
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "0.8.27",
3
+ "version": "1.0.0",
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.27"
47
+ "@markdy/core": "1.0.0"
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.27"
54
+ "@markdy/stdlib-systems": "1.0.0"
55
55
  },
56
56
  "scripts": {
57
57
  "build": "tsup",