@markdy/renderer-dom 0.8.28 → 1.0.1

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
@@ -97,12 +97,6 @@ interface GifDiagramExportOptions extends SvgExportOptions {
97
97
  }
98
98
  declare function exportDiagramAsGif(container: HTMLElement, timeline: TimelineController, options?: GifDiagramExportOptions): Promise<Blob>;
99
99
 
100
- /**
101
- * packages/renderer-dom/src/export/png-exporter.ts
102
- * High-DPI raster PNG export with 2x retina scaling.
103
- * Zero external dependencies.
104
- */
105
-
106
100
  interface PngExportOptions extends SvgExportOptions {
107
101
  pixelRatio?: number;
108
102
  }
package/dist/index.js CHANGED
@@ -2038,6 +2038,10 @@ var MIN_VIEWPORT_ZOOM = 0.5;
2038
2038
  var MAX_VIEWPORT_ZOOM = 3;
2039
2039
  var VIEWPORT_ZOOM_STEP = 15e-4;
2040
2040
  var DRAG_CLICK_THRESHOLD_PX = 4;
2041
+ var MARKDY_PLAYGROUND_URL = "https://markdy.com/playground/";
2042
+ function encodeCodeForPlaygroundHash(code) {
2043
+ return encodeURIComponent(btoa(encodeURIComponent(code)));
2044
+ }
2041
2045
  function createBeatCaptionLayer(doc, beats) {
2042
2046
  const layer = doc.createElement("div");
2043
2047
  layer.className = "markdy-beat-caption-layer";
@@ -2171,7 +2175,7 @@ function createDiagram(opts) {
2171
2175
  let badge = null;
2172
2176
  if (copyright) {
2173
2177
  badge = document.createElement("a");
2174
- badge.href = "https://markdy.com";
2178
+ badge.href = `${MARKDY_PLAYGROUND_URL}#code=${encodeCodeForPlaygroundHash(code)}`;
2175
2179
  badge.target = "_blank";
2176
2180
  badge.rel = "noopener noreferrer";
2177
2181
  badge.textContent = "Powered by Markdy";
@@ -2862,6 +2866,9 @@ function encodeGifSequence(frames, options = {}) {
2862
2866
  return new Uint8Array(buffer);
2863
2867
  }
2864
2868
 
2869
+ // src/export/png-exporter.ts
2870
+ import html2canvas from "html2canvas";
2871
+
2865
2872
  // src/export/svg-exporter.ts
2866
2873
  function copyRenderedStyles(source, clone) {
2867
2874
  if (typeof window === "undefined" || typeof window.getComputedStyle !== "function") return;
@@ -2876,20 +2883,22 @@ function copyRenderedStyles(source, clone) {
2876
2883
  }
2877
2884
  }
2878
2885
  }
2879
- function exportDiagramAsVectorSvg(containerEl, options = {}) {
2886
+ function normalizeExportViewport(scene) {
2887
+ scene.querySelectorAll(".markdy-viewport-transform").forEach((viewportTransform) => {
2888
+ viewportTransform.style.transform = "translate(0px, 0px) scale(1)";
2889
+ viewportTransform.style.transformOrigin = "0 0";
2890
+ viewportTransform.style.willChange = "auto";
2891
+ });
2892
+ }
2893
+ function getDiagramSceneElement(containerEl) {
2880
2894
  const sceneEl = containerEl.classList?.contains("markdy-scene-root") ? containerEl : containerEl.querySelector(".markdy-scene-root") || containerEl.querySelector("svg") || (containerEl.tagName?.toLowerCase() === "svg" ? containerEl : null);
2881
2895
  if (!sceneEl) throw new Error("No Markdy scene element found in container");
2882
- if (sceneEl.tagName?.toLowerCase() === "svg") {
2883
- const clonedSvg = sceneEl.cloneNode(true);
2884
- if (!clonedSvg.getAttribute("xmlns")) {
2885
- clonedSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
2886
- }
2887
- const serializer2 = new XMLSerializer();
2888
- return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2889
- ${serializer2.serializeToString(clonedSvg)}`;
2890
- }
2896
+ return sceneEl;
2897
+ }
2898
+ function prepareHtmlSceneForExport(sceneEl, options = {}) {
2891
2899
  const clonedScene = sceneEl.cloneNode(true);
2892
2900
  copyRenderedStyles(sceneEl, clonedScene);
2901
+ normalizeExportViewport(clonedScene);
2893
2902
  const widthStr = clonedScene.style.width || String(sceneEl.clientWidth || 800);
2894
2903
  const heightStr = clonedScene.style.height || String(sceneEl.clientHeight || 400);
2895
2904
  let width = parseFloat(widthStr);
@@ -2904,10 +2913,25 @@ ${serializer2.serializeToString(clonedSvg)}`;
2904
2913
  clonedScene.style.position = "relative";
2905
2914
  clonedScene.style.left = "0px";
2906
2915
  clonedScene.style.top = "0px";
2916
+ clonedScene.style.margin = "0";
2907
2917
  clonedScene.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
2908
2918
  if (options.transparentBackground) {
2909
2919
  clonedScene.style.background = "transparent";
2910
2920
  }
2921
+ return { sceneEl, clonedScene, width, height, scaledWidth, scaledHeight };
2922
+ }
2923
+ function exportDiagramAsVectorSvg(containerEl, options = {}) {
2924
+ const sceneEl = getDiagramSceneElement(containerEl);
2925
+ if (sceneEl.tagName?.toLowerCase() === "svg") {
2926
+ const clonedSvg = sceneEl.cloneNode(true);
2927
+ if (!clonedSvg.getAttribute("xmlns")) {
2928
+ clonedSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
2929
+ }
2930
+ const serializer2 = new XMLSerializer();
2931
+ return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2932
+ ${serializer2.serializeToString(clonedSvg)}`;
2933
+ }
2934
+ const { clonedScene, scaledWidth, scaledHeight } = prepareHtmlSceneForExport(sceneEl, options);
2911
2935
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2912
2936
  svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
2913
2937
  svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
@@ -2939,50 +2963,258 @@ ${serializer2.serializeToString(clonedSvg)}`;
2939
2963
  ` + serializer.serializeToString(svg);
2940
2964
  }
2941
2965
 
2942
- // src/export/gif-exporter.ts
2943
- var nextFrame = () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
2944
- async function rasterizeFrame(container, pixelRatio, options) {
2945
- const svg = exportDiagramAsVectorSvg(container, options);
2946
- const url = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml;charset=utf-8" }));
2947
- try {
2948
- const image = new Image();
2949
- await new Promise((resolve, reject) => {
2950
- image.onload = () => resolve();
2951
- image.onerror = () => reject(new Error("Failed to rasterize GIF frame"));
2952
- image.src = url;
2966
+ // src/export/inline-resources.ts
2967
+ var TRANSPARENT_PIXEL_DATA_URI = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
2968
+ var RESOURCE_FETCH_TIMEOUT_MS = 3e3;
2969
+ var dataUriCache = /* @__PURE__ */ new Map();
2970
+ function shouldInlineUrl(url) {
2971
+ const trimmed = url.trim();
2972
+ return !!trimmed && !trimmed.startsWith("data:") && !trimmed.startsWith("blob:") && !trimmed.startsWith("#");
2973
+ }
2974
+ async function toDataUri(url) {
2975
+ if (dataUriCache.has(url)) return dataUriCache.get(url);
2976
+ const request = (async () => {
2977
+ const controller = new AbortController();
2978
+ const timeout = setTimeout(() => controller.abort(), RESOURCE_FETCH_TIMEOUT_MS);
2979
+ try {
2980
+ const resp = await fetch(url, { mode: "cors", credentials: "same-origin", signal: controller.signal });
2981
+ if (!resp.ok) return null;
2982
+ const arrayBuffer = await resp.arrayBuffer();
2983
+ const mimeType = resp.headers.get("Content-Type") || "application/octet-stream";
2984
+ const base64 = btoa(
2985
+ new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), "")
2986
+ );
2987
+ return `data:${mimeType};base64,${base64}`;
2988
+ } catch {
2989
+ return null;
2990
+ } finally {
2991
+ clearTimeout(timeout);
2992
+ }
2993
+ })();
2994
+ dataUriCache.set(url, request);
2995
+ return request;
2996
+ }
2997
+ async function inlineCssUrls(cssValue) {
2998
+ const urlPattern = /url\(["']?([^"')]+)["']?\)/g;
2999
+ const matches = [];
3000
+ let m;
3001
+ while ((m = urlPattern.exec(cssValue)) !== null) {
3002
+ const src = m[1];
3003
+ if (shouldInlineUrl(src)) {
3004
+ matches.push({ full: m[0], src });
3005
+ }
3006
+ }
3007
+ let result = cssValue;
3008
+ await Promise.all(
3009
+ matches.map(async ({ full, src }) => {
3010
+ try {
3011
+ const absoluteSrc = new URL(src, document.baseURI).href;
3012
+ const dataUri = await toDataUri(absoluteSrc);
3013
+ result = result.split(full).join(dataUri ? `url("${dataUri}")` : "none");
3014
+ } catch {
3015
+ result = result.split(full).join("none");
3016
+ }
3017
+ })
3018
+ );
3019
+ return result;
3020
+ }
3021
+ async function inlineExternalResources(root) {
3022
+ const tasks = [];
3023
+ root.querySelectorAll("img").forEach((img) => {
3024
+ const src = img.getAttribute("src");
3025
+ if (src && shouldInlineUrl(src)) {
3026
+ tasks.push(
3027
+ Promise.resolve().then(() => toDataUri(new URL(src, document.baseURI).href)).then((dataUri) => {
3028
+ img.setAttribute("src", dataUri || TRANSPARENT_PIXEL_DATA_URI);
3029
+ })
3030
+ );
3031
+ }
3032
+ });
3033
+ const allEls = [root, ...Array.from(root.querySelectorAll("*"))];
3034
+ allEls.forEach((el) => {
3035
+ const style = el.getAttribute("style");
3036
+ if (style && style.includes("url(")) {
3037
+ tasks.push(
3038
+ inlineCssUrls(style).then((inlined) => {
3039
+ if (inlined !== style) el.setAttribute("style", inlined);
3040
+ })
3041
+ );
3042
+ }
3043
+ });
3044
+ await Promise.all(tasks);
3045
+ }
3046
+ async function inlineSerializedSvgResources(svgXml) {
3047
+ const doc = new DOMParser().parseFromString(svgXml, "image/svg+xml");
3048
+ if (doc.querySelector("parsererror")) return svgXml;
3049
+ await inlineExternalResources(doc.documentElement);
3050
+ const serializer = new XMLSerializer();
3051
+ const serializedSvg = serializer.serializeToString(doc.documentElement);
3052
+ return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
3053
+ ${serializedSvg}`;
3054
+ }
3055
+
3056
+ // src/export/png-exporter.ts
3057
+ function cssColorComponentToByte(value) {
3058
+ const trimmed = value.trim();
3059
+ const numeric = trimmed.endsWith("%") ? Number.parseFloat(trimmed) / 100 : Number.parseFloat(trimmed);
3060
+ if (!Number.isFinite(numeric)) return 0;
3061
+ return Math.round(Math.min(1, Math.max(0, numeric)) * 255);
3062
+ }
3063
+ function cssAlphaToNumber(value) {
3064
+ if (!value) return 1;
3065
+ const trimmed = value.trim();
3066
+ const numeric = trimmed.endsWith("%") ? Number.parseFloat(trimmed) / 100 : Number.parseFloat(trimmed);
3067
+ if (!Number.isFinite(numeric)) return 1;
3068
+ return Math.min(1, Math.max(0, numeric));
3069
+ }
3070
+ function normalizeCssColorFunctions(cssValue) {
3071
+ return cssValue.replace(/color\(\s*[-a-z0-9]+\s+([^)]*)\)/gi, (_match, rawComponents) => {
3072
+ const [rawChannels, rawAlpha] = String(rawComponents).split("/");
3073
+ const channels = rawChannels.trim().split(/\s+/);
3074
+ if (channels.length < 3) return "rgba(0, 0, 0, 0)";
3075
+ const red = cssColorComponentToByte(channels[0]);
3076
+ const green = cssColorComponentToByte(channels[1]);
3077
+ const blue = cssColorComponentToByte(channels[2]);
3078
+ const alpha = cssAlphaToNumber(rawAlpha);
3079
+ return alpha >= 1 ? `rgb(${red}, ${green}, ${blue})` : `rgba(${red}, ${green}, ${blue}, ${alpha})`;
3080
+ });
3081
+ }
3082
+ function sanitizeHtml2CanvasStyles(root) {
3083
+ [root, ...Array.from(root.querySelectorAll("*"))].forEach((element) => {
3084
+ const style = element.getAttribute("style");
3085
+ if (style?.includes("color(")) {
3086
+ element.setAttribute("style", normalizeCssColorFunctions(style));
3087
+ }
3088
+ });
3089
+ }
3090
+ async function exportDiagramAsPng(containerEl, options = {}) {
3091
+ const canvas = await rasterizeDiagramToCanvas(containerEl, options, options.pixelRatio || 2);
3092
+ return new Promise((resolve, reject) => {
3093
+ canvas.toBlob((b) => {
3094
+ if (b) resolve(b);
3095
+ else reject(new Error("Canvas toBlob failed for PNG export"));
3096
+ }, "image/png");
3097
+ });
3098
+ }
3099
+ async function rasterizeDiagramToCanvas(containerEl, options = {}, pixelRatio = 1) {
3100
+ const sceneEl = getDiagramSceneElement(containerEl);
3101
+ if (sceneEl.tagName?.toLowerCase() !== "svg") {
3102
+ const { clonedScene, scaledWidth, scaledHeight } = prepareHtmlSceneForExport(sceneEl, options);
3103
+ await inlineExternalResources(clonedScene);
3104
+ sanitizeHtml2CanvasStyles(clonedScene);
3105
+ const host = document.createElement("div");
3106
+ Object.assign(host.style, {
3107
+ position: "fixed",
3108
+ left: "-100000px",
3109
+ top: "0",
3110
+ width: `${scaledWidth}px`,
3111
+ height: `${scaledHeight}px`,
3112
+ overflow: "hidden",
3113
+ pointerEvents: "none",
3114
+ zIndex: "-1"
2953
3115
  });
2954
- const canvas = document.createElement("canvas");
2955
- canvas.width = image.naturalWidth * pixelRatio;
2956
- canvas.height = image.naturalHeight * pixelRatio;
2957
- const context = canvas.getContext("2d");
2958
- if (!context) throw new Error("Could not create GIF canvas context");
2959
- context.drawImage(image, 0, 0, canvas.width, canvas.height);
2960
- return context.getImageData(0, 0, canvas.width, canvas.height);
2961
- } finally {
3116
+ host.setAttribute("aria-hidden", "true");
3117
+ host.appendChild(clonedScene);
3118
+ document.body.appendChild(host);
3119
+ try {
3120
+ await document.fonts?.ready;
3121
+ return await html2canvas(clonedScene, {
3122
+ allowTaint: false,
3123
+ backgroundColor: options.transparentBackground ? null : void 0,
3124
+ height: scaledHeight,
3125
+ imageTimeout: 3e3,
3126
+ logging: false,
3127
+ onclone: (clonedDocument, clonedElement) => {
3128
+ clonedDocument.querySelectorAll("style, link[rel='stylesheet']").forEach((styleNode) => styleNode.remove());
3129
+ sanitizeHtml2CanvasStyles(clonedElement);
3130
+ },
3131
+ scale: pixelRatio,
3132
+ useCORS: true,
3133
+ width: scaledWidth
3134
+ });
3135
+ } finally {
3136
+ host.remove();
3137
+ }
3138
+ }
3139
+ const pixelRatioForSvg = pixelRatio || 1;
3140
+ const clonedContainer = containerEl.cloneNode(true);
3141
+ await inlineExternalResources(clonedContainer);
3142
+ const svgXml = await inlineSerializedSvgResources(exportDiagramAsVectorSvg(clonedContainer, options));
3143
+ const blob = new Blob([svgXml], { type: "image/svg+xml;charset=utf-8" });
3144
+ const url = URL.createObjectURL(blob);
3145
+ const img = new Image();
3146
+ await new Promise((resolve, reject) => {
3147
+ img.onload = () => resolve();
3148
+ img.onerror = () => {
3149
+ URL.revokeObjectURL(url);
3150
+ reject(new Error("Failed to rasterize SVG into Image for PNG export"));
3151
+ };
3152
+ img.src = url;
3153
+ });
3154
+ const width = img.naturalWidth || 800;
3155
+ const height = img.naturalHeight || 400;
3156
+ const canvas = document.createElement("canvas");
3157
+ canvas.width = width * pixelRatioForSvg;
3158
+ canvas.height = height * pixelRatioForSvg;
3159
+ const ctx = canvas.getContext("2d");
3160
+ if (!ctx) {
2962
3161
  URL.revokeObjectURL(url);
3162
+ throw new Error("Could not get 2D canvas context for PNG export");
2963
3163
  }
3164
+ ctx.scale(pixelRatioForSvg, pixelRatioForSvg);
3165
+ ctx.drawImage(img, 0, 0, width, height);
3166
+ URL.revokeObjectURL(url);
3167
+ return canvas;
3168
+ }
3169
+
3170
+ // src/export/gif-exporter.ts
3171
+ var nextFrame = () => new Promise((resolve) => {
3172
+ let settled = false;
3173
+ const finish = () => {
3174
+ if (settled) return;
3175
+ settled = true;
3176
+ resolve();
3177
+ };
3178
+ const fallback = setTimeout(finish, 80);
3179
+ requestAnimationFrame(() => requestAnimationFrame(() => {
3180
+ clearTimeout(fallback);
3181
+ finish();
3182
+ }));
3183
+ });
3184
+ var DEFAULT_GIF_PIXEL_RATIO = 0.3;
3185
+ var MAX_GIF_FRAMES = 24;
3186
+ async function rasterizeFrame(container, pixelRatio, options) {
3187
+ const canvas = await rasterizeDiagramToCanvas(container, options, pixelRatio);
3188
+ const context = canvas.getContext("2d");
3189
+ if (!context) throw new Error("Could not create GIF canvas context");
3190
+ return context.getImageData(0, 0, canvas.width, canvas.height);
2964
3191
  }
2965
3192
  async function exportDiagramAsGif(container, timeline, options = {}) {
2966
- const fps = Math.min(24, Math.max(1, Math.round(options.fps ?? 12)));
3193
+ const requestedFps = Math.min(24, Math.max(1, Math.round(options.fps ?? 12)));
2967
3194
  const duration = timeline.duration();
3195
+ const fps = Math.min(requestedFps, MAX_GIF_FRAMES / Math.max(duration, 1e-3));
2968
3196
  const frameCount = Math.max(1, Math.ceil(duration * fps));
2969
3197
  const delayMs = Math.max(20, Math.round(1e3 / fps));
3198
+ const pixelRatio = options.pixelRatio ?? DEFAULT_GIF_PIXEL_RATIO;
2970
3199
  const priorTime = timeline.currentTime();
2971
3200
  const wasPlaying = timeline.isPlaying();
2972
3201
  timeline.pause();
2973
3202
  try {
2974
3203
  const frames = [];
3204
+ timeline.seek(duration);
3205
+ await nextFrame();
3206
+ frames.push({ imageData: await rasterizeFrame(container, pixelRatio, options), delayMs: Math.max(delayMs, 800) });
2975
3207
  for (let frame = 0; frame < frameCount; frame++) {
2976
3208
  timeline.seek(Math.min(duration, frame / fps));
2977
3209
  await nextFrame();
2978
3210
  frames.push({
2979
- imageData: await rasterizeFrame(container, options.pixelRatio ?? 1, options),
3211
+ imageData: await rasterizeFrame(container, pixelRatio, options),
2980
3212
  delayMs
2981
3213
  });
2982
3214
  }
2983
3215
  timeline.seek(duration);
2984
3216
  await nextFrame();
2985
- frames.push({ imageData: await rasterizeFrame(container, options.pixelRatio ?? 1, options), delayMs: Math.max(delayMs, 800) });
3217
+ frames.push({ imageData: await rasterizeFrame(container, pixelRatio, options), delayMs: Math.max(delayMs, 800) });
2986
3218
  const encoded = encodeGifSequence(frames, { dither: true, loop: options.loop ?? true });
2987
3219
  const bytes = new Uint8Array(encoded.byteLength);
2988
3220
  bytes.set(encoded);
@@ -2993,42 +3225,6 @@ async function exportDiagramAsGif(container, timeline, options = {}) {
2993
3225
  }
2994
3226
  }
2995
3227
 
2996
- // src/export/png-exporter.ts
2997
- async function exportDiagramAsPng(containerEl, options = {}) {
2998
- const svgXml = exportDiagramAsVectorSvg(containerEl, options);
2999
- const pixelRatio = options.pixelRatio || 2;
3000
- const blob = new Blob([svgXml], { type: "image/svg+xml;charset=utf-8" });
3001
- const url = URL.createObjectURL(blob);
3002
- const img = new Image();
3003
- await new Promise((resolve, reject) => {
3004
- img.onload = () => resolve();
3005
- img.onerror = () => {
3006
- URL.revokeObjectURL(url);
3007
- reject(new Error("Failed to rasterize SVG into Image for PNG export"));
3008
- };
3009
- img.src = url;
3010
- });
3011
- const width = img.naturalWidth || 800;
3012
- const height = img.naturalHeight || 400;
3013
- const canvas = document.createElement("canvas");
3014
- canvas.width = width * pixelRatio;
3015
- canvas.height = height * pixelRatio;
3016
- const ctx = canvas.getContext("2d");
3017
- if (!ctx) {
3018
- URL.revokeObjectURL(url);
3019
- throw new Error("Could not get 2D canvas context for PNG export");
3020
- }
3021
- ctx.scale(pixelRatio, pixelRatio);
3022
- ctx.drawImage(img, 0, 0, width, height);
3023
- URL.revokeObjectURL(url);
3024
- return new Promise((resolve, reject) => {
3025
- canvas.toBlob((b) => {
3026
- if (b) resolve(b);
3027
- else reject(new Error("Canvas toBlob failed for PNG export"));
3028
- }, "image/png");
3029
- });
3030
- }
3031
-
3032
3228
  // src/presentation-controller.ts
3033
3229
  var DiagramPresentationController = class {
3034
3230
  diagram;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/renderer-dom",
3
- "version": "0.8.28",
3
+ "version": "1.0.1",
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,15 @@
44
44
  "access": "public"
45
45
  },
46
46
  "dependencies": {
47
- "@markdy/core": "0.8.28"
47
+ "html2canvas": "^1.4.1",
48
+ "@markdy/core": "1.0.1"
48
49
  },
49
50
  "devDependencies": {
50
51
  "jsdom": "^29.1.1",
51
52
  "tsup": "^8.5.1",
52
53
  "typescript": "^5.9.3",
53
54
  "vitest": "^4.1.7",
54
- "@markdy/stdlib-systems": "0.8.28"
55
+ "@markdy/stdlib-systems": "1.0.1"
55
56
  },
56
57
  "scripts": {
57
58
  "build": "tsup",