@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 +22 -1
- package/dist/index.js +139 -2
- package/package.json +3 -3
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.
|
|
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.
|
|
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.
|
|
54
|
+
"@markdy/stdlib-systems": "1.0.0"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "tsup",
|