flowsvg 0.1.1 → 0.2.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/CHANGELOG.md +28 -0
- package/PATTERNS.md +61 -5
- package/README.md +149 -461
- package/README.zh-CN.md +149 -461
- package/dist/adapters/react.d.ts +7 -0
- package/dist/adapters/react.js +14 -2
- package/dist/adapters/vue.d.ts +29 -3
- package/dist/adapters/vue.js +20 -4
- package/dist/adapters/web-component.d.ts +14 -2
- package/dist/adapters/web-component.js +32 -3
- package/dist/browser/controls.d.ts +7 -3
- package/dist/browser/controls.js +23 -31
- package/dist/browser/export-controls.d.ts +32 -0
- package/dist/browser/export-controls.js +151 -0
- package/dist/browser/player.d.ts +9 -1
- package/dist/browser/player.js +83 -24
- package/dist/browser/select-control.d.ts +33 -0
- package/dist/browser/select-control.js +171 -0
- package/dist/browser.d.ts +2 -1
- package/dist/browser.js +2 -1
- package/dist/core/diagram.d.ts +4 -2
- package/dist/core/diagram.js +111 -21
- package/dist/core/edge-geometry.d.ts +16 -3
- package/dist/core/edge-geometry.js +264 -37
- package/dist/core/model.d.ts +35 -9
- package/dist/core/node-content-measurement.d.ts +34 -0
- package/dist/core/node-content-measurement.js +66 -0
- package/dist/core/node-content.d.ts +17 -0
- package/dist/core/node-content.js +30 -6
- package/dist/core/timeline.d.ts +11 -3
- package/dist/core/timeline.js +29 -5
- package/dist/core/validation.d.ts +1 -1
- package/dist/core/validation.js +13 -3
- package/dist/export/browser.d.ts +9 -0
- package/dist/export/browser.js +172 -0
- package/dist/export/captions.d.ts +18 -0
- package/dist/export/captions.js +44 -0
- package/dist/export/plan.d.ts +25 -0
- package/dist/export/plan.js +94 -0
- package/dist/export/types.d.ts +64 -0
- package/dist/export/types.js +9 -0
- package/dist/export/worker-source.d.ts +1 -0
- package/dist/export/worker-source.js +1 -0
- package/dist/export.d.ts +3 -0
- package/dist/export.js +2 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/node/cli.d.ts +2 -0
- package/dist/node/cli.js +72 -0
- package/dist/node/gif.d.ts +16 -0
- package/dist/node/gif.js +184 -0
- package/dist/node/html.d.ts +19 -0
- package/dist/node/html.js +61 -0
- package/dist/node/output.d.ts +2 -0
- package/dist/node/output.js +22 -0
- package/dist/node.d.ts +6 -0
- package/dist/node.js +3 -0
- package/dist/portable/LICENSE.esbuild.txt +21 -0
- package/dist/portable/LICENSE.flowsvg.txt +21 -0
- package/dist/portable/LICENSE.gifenc.txt +21 -0
- package/dist/portable/export.js +5 -0
- package/dist/portable/manifest.json +36 -0
- package/dist/portable/player.js +2 -0
- package/dist/portable/styles.css +227 -0
- package/dist/styles.css +194 -14
- package/docs/images/flowsvg-wordmark.gif +0 -0
- package/package.json +37 -6
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ThemeName } from "../core/model.js";
|
|
2
|
+
/** Stages belong to one independent export; counts represent acknowledged sampling frames. */
|
|
3
|
+
export interface GifExportProgress {
|
|
4
|
+
phase: "preparing" | "sampling" | "encoding" | "complete";
|
|
5
|
+
completed: number;
|
|
6
|
+
total: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Browser export settings, captured at invocation; times are milliseconds.
|
|
10
|
+
* Animation is limited to 120 effective seconds and 1800 samples. The final GIF
|
|
11
|
+
* permits at most 1801 encoded frames, including splits of long final holds.
|
|
12
|
+
* The final long edge, including captions, must not exceed 1920 pixels.
|
|
13
|
+
* Invalid values and computable limits fail before canvas allocation or sampling.
|
|
14
|
+
*/
|
|
15
|
+
export interface GifExportOptions {
|
|
16
|
+
/** Explicit theme; otherwise use the document theme, then light. Never reads system preferences. */
|
|
17
|
+
theme?: ThemeName;
|
|
18
|
+
/** Inclusive source start, default 0. Evaluates the full document to retain earlier scene state. */
|
|
19
|
+
startMs?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Exclusive source end, default total duration. A segment holds the end-left picture;
|
|
22
|
+
* the global end includes its completed state, even with finalHoldMs: 0.
|
|
23
|
+
* Animated ranges require 0 <= startMs < endMs <= total duration; static ranges require both 0.
|
|
24
|
+
*/
|
|
25
|
+
endMs?: number;
|
|
26
|
+
/** Positive finite source-time multiplier, default 1. Output duration is source range / speed. */
|
|
27
|
+
speed?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Positive diagram width in pixels, rounded to an integer. Diagram height preserves the authored viewport ratio.
|
|
30
|
+
* GIF ignores viewport.fitHeight for all frames to keep pixel dimensions and diagram scale stable.
|
|
31
|
+
* Defaults to min(viewport.width, 1280), without upscaling; captions add height below the diagram.
|
|
32
|
+
*/
|
|
33
|
+
width?: number;
|
|
34
|
+
/** Integer samples per second from 1 to 30, default 15; cumulative GIF delay rounding avoids drift. */
|
|
35
|
+
fps?: number;
|
|
36
|
+
/** Include a fixed caption region below the diagram, default true. False removes that region. */
|
|
37
|
+
includeCaptions?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Caption-region height in pixels, rounded to an integer; default 80, finite and nonnegative.
|
|
40
|
+
* Text uses 16px system font, 22px line height, 16px horizontal and 12px vertical padding.
|
|
41
|
+
* Every effective caption is measured before sampling; overflow reports its scene/beat without clipping or shrinking.
|
|
42
|
+
*/
|
|
43
|
+
captionHeight?: number;
|
|
44
|
+
/** Zero means infinite looping (default); integers 1–65,536 mean total plays, with 1 playing once. */
|
|
45
|
+
loopCount?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Additional final-picture delay, finite and nonnegative; default 1000. Zero adds no delay.
|
|
48
|
+
* Long delays split into bounded GIF frames and count toward the 1801 encoded-frame limit.
|
|
49
|
+
* Static documents produce one picture with at least 10ms delay, even when this value is zero.
|
|
50
|
+
*/
|
|
51
|
+
finalHoldMs?: number;
|
|
52
|
+
/** Default false, independent of system preferences. True completes transitions and hides flow markers without changing duration. */
|
|
53
|
+
reducedMotion?: boolean;
|
|
54
|
+
/** Cancels this task with code cancelled; task-owned workers, images, canvas and object URLs are released on every exit. */
|
|
55
|
+
signal?: AbortSignal;
|
|
56
|
+
/** Reports preparing, sampling, encoding and complete. Cancellation never reports complete. Does not change player state. */
|
|
57
|
+
onProgress?: (progress: GifExportProgress) => void;
|
|
58
|
+
}
|
|
59
|
+
export type GifExportErrorCode = "options" | "limit" | "document" | "caption-overflow" | "environment" | "rasterization" | "encoding" | "cancelled";
|
|
60
|
+
/** Distinguishable export failure with a message describing the corrective action. */
|
|
61
|
+
export declare class GifExportError extends Error {
|
|
62
|
+
readonly code: GifExportErrorCode;
|
|
63
|
+
constructor(code: GifExportErrorCode, message: string, options?: ErrorOptions);
|
|
64
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Distinguishable export failure with a message describing the corrective action. */
|
|
2
|
+
export class GifExportError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message, options) {
|
|
5
|
+
super(message, options);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = "GifExportError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const GIF_WORKER_SOURCE: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const GIF_WORKER_SOURCE = "(()=>{var se=Object.create;var W=Object.defineProperty;var le=Object.getOwnPropertyDescriptor;var ae=Object.getOwnPropertyNames;var fe=Object.getPrototypeOf,ue=Object.prototype.hasOwnProperty;var ge=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var de=(e,t,o,n)=>{if(t&&typeof t==\"object\"||typeof t==\"function\")for(let r of ae(t))!ue.call(e,r)&&r!==o&&W(e,r,{get:()=>t[r],enumerable:!(n=le(t,r))||n.enumerable});return e};var he=(e,t,o)=>(o=e!=null?se(fe(e)):{},de(t||!e||!e.__esModule?W(o,\"default\",{value:e,enumerable:!0}):o,e));var ce=ge(H=>{var J=Object.defineProperty,we=e=>J(e,\"__esModule\",{value:!0}),ye=(e,t)=>{for(var o in t)J(e,o,{get:t[o],enumerable:!0})};we(H);ye(H,{GIFEncoder:()=>oe,applyPalette:()=>Ce,default:()=>qe,nearestColor:()=>Fe,nearestColorIndex:()=>ne,nearestColorIndexWithDistance:()=>re,prequantize:()=>ve,quantize:()=>xe,snapColorsToPalette:()=>Ee});var be={signature:\"GIF\",version:\"89a\",trailer:59,extensionIntroducer:33,applicationExtensionLabel:255,graphicControlExtensionLabel:249,imageSeparator:44,signatureSize:3,versionSize:3,globalColorTableFlagMask:128,colorResolutionMask:112,sortFlagMask:8,globalColorTableSizeMask:7,applicationIdentifierSize:8,applicationAuthCodeSize:3,disposalMethodMask:28,userInputFlagMask:2,transparentColorFlagMask:1,localColorTableFlagMask:128,interlaceFlagMask:64,idSortFlagMask:32,localColorTableSizeMask:7};function Q(e=256){let t=0,o=new Uint8Array(e);return{get buffer(){return o.buffer},reset(){t=0},bytesView(){return o.subarray(0,t)},bytes(){return o.slice(0,t)},writeByte(r){n(t+1),o[t]=r,t++},writeBytes(r,i=0,c=r.length){n(t+c);for(let l=0;l<c;l++)o[t++]=r[l+i]},writeBytesView(r,i=0,c=r.byteLength){n(t+c),o.set(r.subarray(i,i+c),t),t+=c}};function n(r){var i=o.length;if(i>=r)return;var c=1024*1024;r=Math.max(r,i*(i<c?2:1.125)>>>0),i!=0&&(r=Math.max(r,256));let l=o;o=new Uint8Array(r),t>0&&o.set(l.subarray(0,t),0)}}var V=12,Y=5003,pe=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535];function Ae(e,t,o,n,r=Q(512),i=new Uint8Array(256),c=new Int32Array(Y),l=new Int32Array(Y)){let g=c.length,f=Math.max(2,n);i.fill(0),l.fill(0),c.fill(-1);let a=0,s=0,d=f+1,p=d,h=!1,w=p,B=(1<<w)-1,u=1<<d-1,I=u+1,M=u+2,y=0,x=o[0],F=0;for(let A=g;A<65536;A*=2)++F;F=8-F,r.writeByte(f),v(u);let b=o.length;for(let A=1;A<b;A++)e:{let k=o[A],E=(k<<V)+x,_=k<<F^x;if(c[_]===E){x=l[_];break e}let P=_===0?1:g-_;for(;c[_]>=0;)if(_-=P,_<0&&(_+=g),c[_]===E){x=l[_];break e}v(x),x=k,M<1<<V?(l[_]=M++,c[_]=E):(c.fill(-1),M=u+2,h=!0,v(u))}return v(x),v(I),r.writeByte(0),r.bytesView();function v(A){for(a&=pe[s],s>0?a|=A<<s:a=A,s+=w;s>=8;)i[y++]=a&255,y>=254&&(r.writeByte(y),r.writeBytesView(i,0,y),y=0),a>>=8,s-=8;if((M>B||h)&&(h?(w=p,B=(1<<w)-1,h=!1):(++w,B=w===V?1<<w:(1<<w)-1)),A==I){for(;s>0;)i[y++]=a&255,y>=254&&(r.writeByte(y),r.writeBytesView(i,0,y),y=0),a>>=8,s-=8;y>0&&(r.writeByte(y),r.writeBytesView(i,0,y),y=0)}}}var Me=Ae;function $(e,t,o){return e<<8&63488|t<<2&992|o>>3}function ee(e,t,o,n){return e>>4|t&240|(o&240)<<4|(n&240)<<8}function te(e,t,o){return e>>4<<8|t&240|o>>4}function q(e,t,o){return e<t?t:e>o?o:e}function S(e){return e*e}function Z(e,t,o){var n=0,r=1e100;let i=e[t],c=i.cnt,l=i.ac,g=i.rc,f=i.gc,a=i.bc;for(var s=i.fw;s!=0;s=e[s].fw){let p=e[s],h=p.cnt,w=c*h/(c+h);if(!(w>=r)){var d=0;o&&(d+=w*S(p.ac-l),d>=r)||(d+=w*S(p.rc-g),!(d>=r)&&(d+=w*S(p.gc-f),!(d>=r)&&(d+=w*S(p.bc-a),!(d>=r)&&(r=d,n=s))))}}i.err=r,i.nn=n}function O(){return{ac:0,rc:0,gc:0,bc:0,cnt:0,nn:0,fw:0,bk:0,tm:0,mtm:0,err:0}}function Be(e,t){let o=t===\"rgb444\"?4096:65536,n=new Array(o),r=e.length;if(t===\"rgba4444\")for(let i=0;i<r;++i){let c=e[i],l=c>>24&255,g=c>>16&255,f=c>>8&255,a=c&255,s=ee(a,f,g,l),d=s in n?n[s]:n[s]=O();d.rc+=a,d.gc+=f,d.bc+=g,d.ac+=l,d.cnt++}else if(t===\"rgb444\")for(let i=0;i<r;++i){let c=e[i],l=c>>16&255,g=c>>8&255,f=c&255,a=te(f,g,l),s=a in n?n[a]:n[a]=O();s.rc+=f,s.gc+=g,s.bc+=l,s.cnt++}else for(let i=0;i<r;++i){let c=e[i],l=c>>16&255,g=c>>8&255,f=c&255,a=$(f,g,l),s=a in n?n[a]:n[a]=O();s.rc+=f,s.gc+=g,s.bc+=l,s.cnt++}return n}function xe(e,t,o={}){let{format:n=\"rgb565\",clearAlpha:r=!0,clearAlphaColor:i=0,clearAlphaThreshold:c=0,oneBitAlpha:l=!1}=o;if(!e||!e.buffer)throw new Error(\"quantize() expected RGBA Uint8Array data\");if(!(e instanceof Uint8Array)&&!(e instanceof Uint8ClampedArray))throw new Error(\"quantize() expected RGBA Uint8Array data\");let g=new Uint32Array(e.buffer),f=o.useSqrt!==!1,a=n===\"rgba4444\",s=Be(g,n),d=s.length,p=d-1,h=new Uint32Array(d+1);for(var w=0,u=0;u<d;++u){let C=s[u];if(C!=null){var B=1/C.cnt;a&&(C.ac*=B),C.rc*=B,C.gc*=B,C.bc*=B,s[w++]=C}}S(t)/w<.022&&(f=!1);for(var u=0;u<w-1;++u)s[u].fw=u+1,s[u+1].bk=u,f&&(s[u].cnt=Math.sqrt(s[u].cnt));f&&(s[u].cnt=Math.sqrt(s[u].cnt));var I,M,y;for(u=0;u<w;++u){Z(s,u,!1);var x=s[u].err;for(M=++h[0];M>1&&(y=M>>1,!(s[I=h[y]].err<=x));M=y)h[M]=I;h[M]=u}var F=w-t;for(u=0;u<F;){for(var b;;){var v=h[1];if(b=s[v],b.tm>=b.mtm&&s[b.nn].mtm<=b.tm)break;b.mtm==p?v=h[1]=h[h[0]--]:(Z(s,v,!1),b.tm=u);var x=s[v].err;for(M=1;(y=M+M)<=h[0]&&(y<h[0]&&s[h[y]].err>s[h[y+1]].err&&y++,!(x<=s[I=h[y]].err));M=y)h[M]=I;h[M]=v}var A=s[b.nn],k=b.cnt,E=A.cnt,B=1/(k+E);a&&(b.ac=B*(k*b.ac+E*A.ac)),b.rc=B*(k*b.rc+E*A.rc),b.gc=B*(k*b.gc+E*A.gc),b.bc=B*(k*b.bc+E*A.bc),b.cnt+=A.cnt,b.mtm=++u,s[A.bk].fw=A.fw,s[A.fw].bk=A.bk,A.mtm=p}let _=[];var P=0;for(u=0;;++P){let R=q(Math.round(s[u].rc),0,255),C=q(Math.round(s[u].gc),0,255),L=q(Math.round(s[u].bc),0,255),T=255;a&&(T=q(Math.round(s[u].ac),0,255),l&&(T=T<=(typeof l==\"number\"?l:127)?0:255),r&&T<=c&&(R=C=L=i,T=0));let X=a?[R,C,L,T]:[R,C,L];if(_e(_,X)||_.push(X),(u=s[u].fw)==0)break}return _}function _e(e,t){for(let o=0;o<e.length;o++){let n=e[o],r=n[0]===t[0]&&n[1]===t[1]&&n[2]===t[2],i=n.length>=4&&t.length>=4?n[3]===t[3]:!0;if(r&&i)return!0}return!1}function G(e,t){var o=0,n;for(n=0;n<e.length;n++){let r=e[n]-t[n];o+=r*r}return o}function D(e,t){return t>1?Math.round(e/t)*t:e}function ve(e,{roundRGB:t=5,roundAlpha:o=10,oneBitAlpha:n=null}={}){let r=new Uint32Array(e.buffer);for(let i=0;i<r.length;i++){let c=r[i],l=c>>24&255,g=c>>16&255,f=c>>8&255,a=c&255;l=D(l,o),n&&(l=l<=(typeof n==\"number\"?n:127)?0:255),a=D(a,t),f=D(f,t),g=D(g,t),r[i]=l<<24|g<<16|f<<8|a<<0}}function Ce(e,t,o=\"rgb565\"){if(!e||!e.buffer)throw new Error(\"quantize() expected RGBA Uint8Array data\");if(!(e instanceof Uint8Array)&&!(e instanceof Uint8ClampedArray))throw new Error(\"quantize() expected RGBA Uint8Array data\");if(t.length>256)throw new Error(\"applyPalette() only works with 256 colors or less\");let n=new Uint32Array(e.buffer),r=n.length,i=o===\"rgb444\"?4096:65536,c=new Uint8Array(r),l=new Array(i),g=o===\"rgba4444\";if(o===\"rgba4444\")for(let f=0;f<r;f++){let a=n[f],s=a>>24&255,d=a>>16&255,p=a>>8&255,h=a&255,w=ee(h,p,d,s),B=w in l?l[w]:l[w]=Ie(h,p,d,s,t);c[f]=B}else{let f=o===\"rgb444\"?te:$;for(let a=0;a<r;a++){let s=n[a],d=s>>16&255,p=s>>8&255,h=s&255,w=f(h,p,d),B=w in l?l[w]:l[w]=ke(h,p,d,t);c[a]=B}}return c}function Ie(e,t,o,n,r){let i=0,c=1e100;for(let l=0;l<r.length;l++){let g=r[l],f=g[3],a=z(f-n);if(a>c)continue;let s=g[0];if(a+=z(s-e),a>c)continue;let d=g[1];if(a+=z(d-t),a>c)continue;let p=g[2];a+=z(p-o),!(a>c)&&(c=a,i=l)}return i}function ke(e,t,o,n){let r=0,i=1e100;for(let c=0;c<n.length;c++){let l=n[c],g=l[0],f=z(g-e);if(f>i)continue;let a=l[1];if(f+=z(a-t),f>i)continue;let s=l[2];f+=z(s-o),!(f>i)&&(i=f,r=c)}return r}function Ee(e,t,o=5){if(!e.length||!t.length)return;let n=e.map(c=>c.slice(0,3)),r=o*o,i=e[0].length;for(let c=0;c<t.length;c++){let l=t[c];l.length<i?l=[l[0],l[1],l[2],255]:l.length>i?l=l.slice(0,3):l=l.slice();let g=re(n,l.slice(0,3),G),f=g[0],a=g[1];a>0&&a<=r&&(e[f]=l)}}function z(e){return e*e}function ne(e,t,o=G){let n=1/0,r=-1;for(let i=0;i<e.length;i++){let c=e[i],l=o(t,c);l<n&&(n=l,r=i)}return r}function re(e,t,o=G){let n=1/0,r=-1;for(let i=0;i<e.length;i++){let c=e[i],l=o(t,c);l<n&&(n=l,r=i)}return[r,n]}function Fe(e,t,o=G){return e[ne(e,t,o)]}function oe(e={}){let{initialCapacity:t=4096,auto:o=!0}=e,n=Q(t),r=5003,i=new Uint8Array(256),c=new Int32Array(r),l=new Int32Array(r),g=!1;return{reset(){n.reset(),g=!1},finish(){n.writeByte(be.trailer)},bytes(){return n.bytes()},bytesView(){return n.bytesView()},get buffer(){return n.buffer},get stream(){return n},writeHeader:f,writeFrame(a,s,d,p={}){let{transparent:h=!1,transparentIndex:w=0,delay:B=0,palette:u=null,repeat:I=0,colorDepth:M=8,dispose:y=-1}=p,x=!1;if(o?g||(x=!0,f(),g=!0):x=!!p.first,s=Math.max(0,Math.floor(s)),d=Math.max(0,Math.floor(d)),x){if(!u)throw new Error(\"First frame must include a { palette } option\");ze(n,s,d,u,M),K(n,u),I>=0&&Te(n,I)}let F=Math.round(B/10);me(n,y,F,h,w);let b=!!u&&!x;Ue(n,s,d,b?u:null),b&&K(n,u),Se(n,a,s,d,M,i,c,l)}};function f(){ie(n,\"GIF89a\")}}function me(e,t,o,n,r){e.writeByte(33),e.writeByte(249),e.writeByte(4),r<0&&(r=0,n=!1);var i,c;n?(i=1,c=2):(i=0,c=0),t>=0&&(c=t&7),c<<=2,e.writeByte(0|c|0|i),m(e,o),e.writeByte(r||0),e.writeByte(0)}function ze(e,t,o,n,r=8){let l=j(n.length)-1,g=128|r-1<<4|0|l,f=0,a=0;m(e,t),m(e,o),e.writeBytes([g,f,a])}function Te(e,t){e.writeByte(33),e.writeByte(255),e.writeByte(11),ie(e,\"NETSCAPE2.0\"),e.writeByte(3),e.writeByte(1),m(e,t),e.writeByte(0)}function K(e,t){let o=1<<j(t.length);for(let n=0;n<o;n++){let r=[0,0,0];n<t.length&&(r=t[n]),e.writeByte(r[0]),e.writeByte(r[1]),e.writeByte(r[2])}}function Ue(e,t,o,n){if(e.writeByte(44),m(e,0),m(e,0),m(e,t),m(e,o),n){let c=j(n.length)-1;e.writeByte(128|c)}else e.writeByte(0)}function Se(e,t,o,n,r=8,i,c,l){Me(o,n,t,r,e,i,c,l)}function m(e,t){e.writeByte(t&255),e.writeByte(t>>8&255)}function ie(e,t){for(var o=0;o<t.length;o++)e.writeByte(t.charCodeAt(o))}function j(e){return Math.max(Math.ceil(Math.log2(e)),1)}var qe=oe});var U=he(ce(),1),N=(0,U.GIFEncoder)(),De=655350,Ge=256;self.onmessage=e=>{try{let t=e.data;if(t.type===\"finish\"){N.finish();let c=N.bytes();self.postMessage({bytes:c},{transfer:[c.buffer]});return}let o=new Uint8Array(t.rgba),n=(0,U.quantize)(o,Ge),r=(0,U.applyPalette)(o,n),i=t.delayMs;do{let c=Math.min(i,De);N.writeFrame(r,t.width,t.height,{palette:n,delay:c,repeat:t.repeat}),i-=c}while(i>0);self.postMessage({acknowledged:!0})}catch(t){self.postMessage({error:t instanceof Error?t.message:String(t)})}};})();\n";
|
package/dist/export.d.ts
ADDED
package/dist/export.js
ADDED
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { createDiagram, renderSvg, wrapText } from "./core/diagram.js";
|
|
2
2
|
export type { Diagram } from "./core/diagram.js";
|
|
3
3
|
export { validateDocument, DiagramValidationError } from "./core/validation.js";
|
|
4
|
-
export type { NodeKind, EdgeRoute, PortDirection, FlowDirection, ThemeName, Viewport, NodeStyle, EdgeStyle, DiagramNode, DiagramEdge, DiagramDocument, Scene, Beat, NodeVisualState, EdgeVisualState, Action, ValidationIssue, EvaluateOptions, TextLine, EvaluatedNode, EvaluatedEdge, EdgeFlowMarker, LayoutWarning, CameraState, HitObjectType, HitObject, DiagramPoint, DiagramFrameState } from "./core/model.js";
|
|
4
|
+
export type { NodeKind, EdgeRoute, PortDirection, FlowDirection, ThemeName, Viewport, NodeStyle, EdgeStyle, DiagramNode, DiagramEdge, DiagramDocument, Scene, Beat, DiagramBeat, NodeVisualState, EdgeVisualState, Action, ValidationIssue, EvaluateOptions, TextLine, EvaluatedNode, EvaluatedEdge, EdgeFlowMarker, LayoutWarning, CameraState, HitObjectType, HitObject, DiagramPoint, DiagramFrameState } from "./core/model.js";
|
|
5
5
|
export type { NodeContent, ContentBlock, ContentListItem, ContentListStyle, ContentTag, ContentTagStyle, ContentTagBackground, ContentGraphBlock, ContentGraphNode, ContentGraphEdge, ContentGraphLayout, ContentText, ContentTextSpan, ContentSpanStyle, ContentSpan, ContentTextStyle, ContentAlignment, ContentSafeArea, ContentLine, NodeContentLayout } from "./core/node-content.js";
|
|
6
|
+
export { measureNodeContent } from "./core/node-content-measurement.js";
|
|
7
|
+
export type { NodeContentMeasurementInput, NodeContentMeasurement, NodeContentMeasurementDiagnostic } from "./core/node-content-measurement.js";
|
package/dist/index.js
CHANGED
package/dist/node/cli.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Node CLI: render completely, then atomically publish HTML/GIF; cancellation never publishes partial bytes. */
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { renderHtml, assertNodeRuntime } from "./html.js";
|
|
7
|
+
import { exportGif } from "./gif.js";
|
|
8
|
+
import { publishOutput } from "./output.js";
|
|
9
|
+
const USAGE = `Usage: flowsvg INPUT.json --output OUTPUT --format html|gif [options]
|
|
10
|
+
Common: --theme light|dark --overwrite
|
|
11
|
+
HTML: --autoplay --no-controls --gif-export
|
|
12
|
+
GIF: --executable-path PATH --start-ms N --end-ms N --speed N --width N --fps N
|
|
13
|
+
--no-captions --caption-height N --loop-count N --final-hold-ms N --reduced-motion
|
|
14
|
+
GIF requires Node 20+ and preinstalled Chromium; browsers are never downloaded automatically.`;
|
|
15
|
+
const CLI_OPTIONS = {
|
|
16
|
+
output: { type: "string", short: "o" }, format: { type: "string" }, theme: { type: "string" },
|
|
17
|
+
autoplay: { type: "boolean" }, "gif-export": { type: "boolean" }, "no-controls": { type: "boolean" }, overwrite: { type: "boolean" },
|
|
18
|
+
"executable-path": { type: "string" }, "start-ms": { type: "string" }, "end-ms": { type: "string" },
|
|
19
|
+
speed: { type: "string" }, width: { type: "string" }, fps: { type: "string" },
|
|
20
|
+
"no-captions": { type: "boolean" }, "caption-height": { type: "string" }, "loop-count": { type: "string" },
|
|
21
|
+
"final-hold-ms": { type: "string" }, "reduced-motion": { type: "boolean" }, help: { type: "boolean", short: "h" }
|
|
22
|
+
};
|
|
23
|
+
function parseCli() { return parseArgs({ allowPositionals: true, options: CLI_OPTIONS }); }
|
|
24
|
+
async function main(signal) {
|
|
25
|
+
assertNodeRuntime();
|
|
26
|
+
const { values, positionals } = parseCli();
|
|
27
|
+
if (values.help) {
|
|
28
|
+
console.log(USAGE);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (positionals.length !== 1 || !values.output || !["html", "gif"].includes(values.format ?? ""))
|
|
32
|
+
throw new Error(USAGE);
|
|
33
|
+
if (values.theme !== undefined && values.theme !== "light" && values.theme !== "dark")
|
|
34
|
+
throw new Error("Theme must be light or dark.");
|
|
35
|
+
const input = JSON.parse(await readFile(resolve(positionals[0]), "utf8"));
|
|
36
|
+
signal.throwIfAborted();
|
|
37
|
+
const theme = values.theme;
|
|
38
|
+
const content = values.format === "gif"
|
|
39
|
+
? await exportGif(input, gifOptions(values, theme, signal))
|
|
40
|
+
: await renderHtml(input, { theme, autoplay: values.autoplay, controls: !values["no-controls"], gifExport: values["gif-export"] });
|
|
41
|
+
await publishOutput(resolve(values.output), content, values.overwrite === true, signal);
|
|
42
|
+
}
|
|
43
|
+
function gifOptions(values, theme, signal) {
|
|
44
|
+
return {
|
|
45
|
+
theme, signal, executablePath: values["executable-path"],
|
|
46
|
+
startMs: numericOption(values["start-ms"], "start-ms"), endMs: numericOption(values["end-ms"], "end-ms"),
|
|
47
|
+
speed: numericOption(values.speed, "speed"), width: numericOption(values.width, "width"), fps: numericOption(values.fps, "fps"),
|
|
48
|
+
includeCaptions: !values["no-captions"], captionHeight: numericOption(values["caption-height"], "caption-height"),
|
|
49
|
+
loopCount: numericOption(values["loop-count"], "loop-count"), finalHoldMs: numericOption(values["final-hold-ms"], "final-hold-ms"),
|
|
50
|
+
reducedMotion: values["reduced-motion"],
|
|
51
|
+
onProgress: (progress) => { console.error(`FlowSVG: ${progress.phase} ${progress.completed}/${progress.total}`); }
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function numericOption(value, name) {
|
|
55
|
+
if (value === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
const number = Number(value);
|
|
58
|
+
if (value.trim() === "" || !Number.isFinite(number))
|
|
59
|
+
throw new Error(`--${name} requires a finite number.`);
|
|
60
|
+
return number;
|
|
61
|
+
}
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const cancel = () => controller.abort(new Error("Export cancelled by signal."));
|
|
64
|
+
process.on("SIGINT", cancel);
|
|
65
|
+
process.on("SIGTERM", cancel);
|
|
66
|
+
main(controller.signal).catch((error) => {
|
|
67
|
+
console.error(`FlowSVG: ${error instanceof Error ? error.message : String(error)}`);
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
}).finally(() => {
|
|
70
|
+
process.off("SIGINT", cancel);
|
|
71
|
+
process.off("SIGTERM", cancel);
|
|
72
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Browser } from "playwright-core";
|
|
2
|
+
import type { DiagramDocument } from "../core/model.js";
|
|
3
|
+
import { type GifExportOptions } from "../export/types.js";
|
|
4
|
+
/** Node-only browser selection. Choose browser or executablePath; a supplied browser remains caller-owned. */
|
|
5
|
+
export interface NodeGifExportOptions extends GifExportOptions {
|
|
6
|
+
/** Use an already installed Chromium executable. This function never downloads a browser. */
|
|
7
|
+
executablePath?: string;
|
|
8
|
+
/** Already connected/launched Playwright browser; only the task's isolated context is closed. */
|
|
9
|
+
browser?: Browser;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Return GIF bytes using the same exporter as flowsvg/export. Requires Node 20+ and preinstalled Chromium.
|
|
13
|
+
* Snapshot inputs before asynchronous work. Success, error and cancellation release task-owned resources;
|
|
14
|
+
* a supplied browser is never closed. Progress is forwarded from the browser exporter.
|
|
15
|
+
*/
|
|
16
|
+
export declare function exportGif(document: DiagramDocument, options?: NodeGifExportOptions): Promise<Uint8Array>;
|
package/dist/node/gif.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/** Node GIF bridge. Reuse the built browser exporter in an isolated, offline Chromium page. */
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { createDiagram } from "../core/diagram.js";
|
|
7
|
+
import { planGif, validateGifOptions } from "../export/plan.js";
|
|
8
|
+
import { GifExportError } from "../export/types.js";
|
|
9
|
+
import { assertNodeRuntime } from "./html.js";
|
|
10
|
+
/**
|
|
11
|
+
* Return GIF bytes using the same exporter as flowsvg/export. Requires Node 20+ and preinstalled Chromium.
|
|
12
|
+
* Snapshot inputs before asynchronous work. Success, error and cancellation release task-owned resources;
|
|
13
|
+
* a supplied browser is never closed. Progress is forwarded from the browser exporter.
|
|
14
|
+
*/
|
|
15
|
+
export async function exportGif(document, options = {}) {
|
|
16
|
+
assertNodeRuntime();
|
|
17
|
+
validateGifOptions(options);
|
|
18
|
+
const { browser: externalBrowser, executablePath, signal, onProgress, ...settings } = options;
|
|
19
|
+
const snapshot = snapshotInput(document, "document");
|
|
20
|
+
const fixedOptions = snapshotInput(settings, "options");
|
|
21
|
+
if (executablePath !== undefined && (typeof executablePath !== "string" || executablePath.length === 0)) {
|
|
22
|
+
throw new GifExportError("options", "executablePath must identify an installed Chromium executable.");
|
|
23
|
+
}
|
|
24
|
+
if (externalBrowser !== undefined && executablePath !== undefined)
|
|
25
|
+
throw new GifExportError("options", "Choose browser or executablePath, not both.");
|
|
26
|
+
throwIfCancelled(signal);
|
|
27
|
+
let duration;
|
|
28
|
+
try {
|
|
29
|
+
duration = createDiagram(snapshot).totalDuration;
|
|
30
|
+
}
|
|
31
|
+
catch (cause) {
|
|
32
|
+
throw new GifExportError("document", "Invalid diagram document. Correct validation issues before exporting.", { cause });
|
|
33
|
+
}
|
|
34
|
+
planGif(snapshot, duration, fixedOptions);
|
|
35
|
+
const job = new BrowserExportJob(signal, onProgress);
|
|
36
|
+
try {
|
|
37
|
+
return await job.run(snapshot, fixedOptions, externalBrowser, executablePath);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
await job.dispose();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Own one export page/context, optional launched browser and temporary directory. Abort also interrupts launch. */
|
|
44
|
+
class BrowserExportJob {
|
|
45
|
+
signal;
|
|
46
|
+
onProgress;
|
|
47
|
+
browser;
|
|
48
|
+
context;
|
|
49
|
+
page;
|
|
50
|
+
directory;
|
|
51
|
+
ownedBrowser = false;
|
|
52
|
+
contextClosing;
|
|
53
|
+
callbackError;
|
|
54
|
+
callbackFailed = false;
|
|
55
|
+
rejectAbort;
|
|
56
|
+
cancellation;
|
|
57
|
+
abort = () => {
|
|
58
|
+
this.rejectAbort?.(cancelledError());
|
|
59
|
+
// Closing the isolated context interrupts font/raster/worker work even if the page cannot evaluate abort.
|
|
60
|
+
// dispose() awaits this same promise and reports cleanup failures after releasing other resources.
|
|
61
|
+
void this.closeContext().catch(() => undefined);
|
|
62
|
+
};
|
|
63
|
+
constructor(signal, onProgress) {
|
|
64
|
+
this.signal = signal;
|
|
65
|
+
this.onProgress = onProgress;
|
|
66
|
+
this.cancellation = new Promise((_, reject) => { this.rejectAbort = reject; });
|
|
67
|
+
// A cancellation can arrive during a file read, before launch starts observing this promise.
|
|
68
|
+
void this.cancellation.catch(() => undefined);
|
|
69
|
+
this.signal?.addEventListener("abort", this.abort, { once: true });
|
|
70
|
+
}
|
|
71
|
+
async run(document, options, externalBrowser, executablePath) {
|
|
72
|
+
try {
|
|
73
|
+
return await this.execute(document, options, externalBrowser, executablePath);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (this.callbackFailed)
|
|
77
|
+
throw new GifExportError("encoding", "GIF progress callback failed. Correct the callback and retry.", { cause: this.callbackError });
|
|
78
|
+
throwIfCancelled(this.signal);
|
|
79
|
+
if (error instanceof GifExportError)
|
|
80
|
+
throw error;
|
|
81
|
+
throw new GifExportError("environment", "Cannot run Chromium GIF export. Prepare Chromium with npx --no-install playwright-core install chromium (and --with-deps on supported Linux), or set executablePath to an installed Chromium executable.", { cause: error });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async execute(document, options, externalBrowser, executablePath) {
|
|
85
|
+
const script = await readFile(new URL("../portable/export.js", import.meta.url), "utf8");
|
|
86
|
+
throwIfCancelled(this.signal);
|
|
87
|
+
this.browser = externalBrowser ?? await this.launch(executablePath);
|
|
88
|
+
this.ownedBrowser = externalBrowser === undefined;
|
|
89
|
+
throwIfCancelled(this.signal);
|
|
90
|
+
this.context = await this.browser.newContext({ offline: true });
|
|
91
|
+
throwIfCancelled(this.signal);
|
|
92
|
+
this.page = await this.context.newPage();
|
|
93
|
+
await this.page.exposeFunction("flowSvgProgress", (progress) => {
|
|
94
|
+
if (this.signal?.aborted)
|
|
95
|
+
return;
|
|
96
|
+
try {
|
|
97
|
+
this.onProgress?.(progress);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
this.callbackFailed = true;
|
|
101
|
+
this.callbackError = error;
|
|
102
|
+
this.abort();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
this.directory = await mkdtemp(join(tmpdir(), "flowsvg-gif-"));
|
|
106
|
+
const html = join(this.directory, "export.html");
|
|
107
|
+
await writeFile(html, `<meta charset="utf-8"><script>${script}</script>`);
|
|
108
|
+
throwIfCancelled(this.signal);
|
|
109
|
+
await this.page.goto(pathToFileURL(html).href);
|
|
110
|
+
const result = await this.page.evaluate(async ({ document, options }) => {
|
|
111
|
+
const bridge = globalThis;
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
const progressPending = [];
|
|
114
|
+
try {
|
|
115
|
+
const bytes = await bridge.FlowSVGExport.exportGif(document, {
|
|
116
|
+
...options, signal: controller.signal,
|
|
117
|
+
onProgress: (progress) => { progressPending.push(bridge.flowSvgProgress(progress)); }
|
|
118
|
+
});
|
|
119
|
+
await Promise.all(progressPending);
|
|
120
|
+
return { bytes: Array.from(bytes) };
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
const failure = error;
|
|
124
|
+
return { error: { code: failure.code ?? "encoding", message: failure.message ?? "Browser GIF export failed." } };
|
|
125
|
+
}
|
|
126
|
+
}, { document, options });
|
|
127
|
+
throwIfCancelled(this.signal);
|
|
128
|
+
if ("error" in result)
|
|
129
|
+
throw new GifExportError(result.error.code, result.error.message);
|
|
130
|
+
return Uint8Array.from(result.bytes);
|
|
131
|
+
}
|
|
132
|
+
async launch(executablePath) {
|
|
133
|
+
let automation;
|
|
134
|
+
try {
|
|
135
|
+
automation = await import("playwright-core");
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
throw new GifExportError("environment", "GIF export requires playwright-core. Reinstall flowsvg with its runtime dependencies, then prepare Chromium explicitly.", { cause: error });
|
|
139
|
+
}
|
|
140
|
+
throwIfCancelled(this.signal);
|
|
141
|
+
// The host/CLI owns process signals; Playwright defaults can otherwise exit before our cleanup.
|
|
142
|
+
const launched = automation.chromium.launch({ executablePath, handleSIGINT: false, handleSIGTERM: false, handleSIGHUP: false }).then(async (browser) => {
|
|
143
|
+
// A cancelled launch can settle after run() returned; close that late process.
|
|
144
|
+
if (this.signal?.aborted) {
|
|
145
|
+
await browser.close();
|
|
146
|
+
throw cancelledError();
|
|
147
|
+
}
|
|
148
|
+
return browser;
|
|
149
|
+
});
|
|
150
|
+
return await Promise.race([launched, this.cancellation]);
|
|
151
|
+
}
|
|
152
|
+
closeContext() {
|
|
153
|
+
if (this.context)
|
|
154
|
+
this.contextClosing ??= this.context.close();
|
|
155
|
+
return this.contextClosing ?? Promise.resolve();
|
|
156
|
+
}
|
|
157
|
+
async dispose() {
|
|
158
|
+
this.signal?.removeEventListener("abort", this.abort);
|
|
159
|
+
try {
|
|
160
|
+
await this.closeContext();
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
try {
|
|
164
|
+
if (this.ownedBrowser)
|
|
165
|
+
await this.browser?.close();
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
if (this.directory)
|
|
169
|
+
await rm(this.directory, { recursive: true, force: true });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function cancelledError() { return new GifExportError("cancelled", "GIF export was cancelled. Start a new export when ready."); }
|
|
175
|
+
function throwIfCancelled(signal) { if (signal?.aborted)
|
|
176
|
+
throw cancelledError(); }
|
|
177
|
+
function snapshotInput(value, code) {
|
|
178
|
+
try {
|
|
179
|
+
return structuredClone(value);
|
|
180
|
+
}
|
|
181
|
+
catch (cause) {
|
|
182
|
+
throw new GifExportError(code, `GIF ${code} must contain serializable values. Remove functions or other unsupported values and retry.`, { cause });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DiagramDocument, ThemeName } from "../core/model.js";
|
|
2
|
+
/** Offline playback options. Default to document/light theme, paused at zero, with controls and caption. */
|
|
3
|
+
export interface HtmlRenderOptions {
|
|
4
|
+
theme?: ThemeName;
|
|
5
|
+
autoplay?: boolean;
|
|
6
|
+
/** Defaults to false. Loop live playback; GIF loopCount remains an independent export option. */
|
|
7
|
+
loop?: boolean;
|
|
8
|
+
controls?: boolean;
|
|
9
|
+
/** Embed offline GIF export assets and show GIF controls when controls are visible. Defaults to false. */
|
|
10
|
+
gifExport?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Return one offline HTML file containing validated data, the player, CSS and optional GIF assets. Requires Node 20+.
|
|
14
|
+
* Text remains data, including HTML delimiters and Unicode separators. No browser is required.
|
|
15
|
+
* @throws For invalid documents/options, an unsupported Node runtime, or missing built assets.
|
|
16
|
+
*/
|
|
17
|
+
export declare function renderHtml(document: DiagramDocument, options?: HtmlRenderOptions): Promise<string>;
|
|
18
|
+
/** Fail before reading files or allocating browser resources on the Node-only path. */
|
|
19
|
+
export declare function assertNodeRuntime(): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Node-only HTML assembly. Read versioned built assets; never start a browser or import automation/encoding dependencies. */
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { createDiagram } from "../core/diagram.js";
|
|
4
|
+
import { DIAGRAM_FONT_FAMILY, resolveThemePalette } from "../core/theme.js";
|
|
5
|
+
/**
|
|
6
|
+
* Return one offline HTML file containing validated data, the player, CSS and optional GIF assets. Requires Node 20+.
|
|
7
|
+
* Text remains data, including HTML delimiters and Unicode separators. No browser is required.
|
|
8
|
+
* @throws For invalid documents/options, an unsupported Node runtime, or missing built assets.
|
|
9
|
+
*/
|
|
10
|
+
export async function renderHtml(document, options = {}) {
|
|
11
|
+
assertNodeRuntime();
|
|
12
|
+
const snapshot = structuredClone(document);
|
|
13
|
+
const playbackOptions = structuredClone(options);
|
|
14
|
+
createDiagram(snapshot);
|
|
15
|
+
validateOptions(playbackOptions);
|
|
16
|
+
const [script, styles, exporter] = await Promise.all([
|
|
17
|
+
readFile(new URL("../portable/player.js", import.meta.url), "utf8"),
|
|
18
|
+
readFile(new URL("../portable/styles.css", import.meta.url), "utf8"),
|
|
19
|
+
playbackOptions.gifExport ? readFile(new URL("../portable/export.js", import.meta.url), "utf8") : ""
|
|
20
|
+
]);
|
|
21
|
+
const data = escapeJson({ document: snapshot, options: playbackOptions });
|
|
22
|
+
const title = escapeHtml(snapshot.title ?? "FlowSVG");
|
|
23
|
+
const palette = resolveThemePalette(playbackOptions.theme ?? snapshot.theme);
|
|
24
|
+
const pageStyles = `body{margin:0;background:${palette.background};color:${palette.text};font-family:${DIAGRAM_FONT_FAMILY}}
|
|
25
|
+
#flowsvg-player{max-width:1280px;margin:auto}.flowsvg-canvas svg{display:block;width:100%}
|
|
26
|
+
.flowsvg-caption{padding:0 16px;white-space:pre-wrap}
|
|
27
|
+
.flowsvg-controls{--flowsvg-control-background:${palette.background};--flowsvg-control-color:${palette.text}}`;
|
|
28
|
+
return `<!doctype html>
|
|
29
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
30
|
+
<title>${title}</title><style>${styles}\n${pageStyles}</style></head>
|
|
31
|
+
<body><main id="flowsvg-player"></main>
|
|
32
|
+
<script id="flowsvg-data" type="application/json">${data}</script>
|
|
33
|
+
${exporter ? `<script>${exporter}</script>\n` : ""}<script>${script}</script></body></html>`;
|
|
34
|
+
}
|
|
35
|
+
/** Fail before reading files or allocating browser resources on the Node-only path. */
|
|
36
|
+
export function assertNodeRuntime() {
|
|
37
|
+
const MIN_NODE_MAJOR = 20;
|
|
38
|
+
if (Number(process.versions.node.split(".")[0]) < MIN_NODE_MAJOR) {
|
|
39
|
+
throw new Error("FlowSVG Node tools require Node 20 or newer. Upgrade Node and retry.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function validateOptions(options) {
|
|
43
|
+
if (options.theme !== undefined && options.theme !== "light" && options.theme !== "dark") {
|
|
44
|
+
throw new Error("HTML theme must be light or dark.");
|
|
45
|
+
}
|
|
46
|
+
for (const key of ["autoplay", "controls", "gifExport", "loop"]) {
|
|
47
|
+
if (options[key] !== undefined && typeof options[key] !== "boolean") {
|
|
48
|
+
throw new Error(`HTML ${key} must be a boolean.`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function escapeJson(value) {
|
|
53
|
+
const ESCAPES = {
|
|
54
|
+
"<": "\\u003c", ">": "\\u003e", "&": "\\u0026", "\u2028": "\\u2028", "\u2029": "\\u2029"
|
|
55
|
+
};
|
|
56
|
+
return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (character) => ESCAPES[character]);
|
|
57
|
+
}
|
|
58
|
+
function escapeHtml(value) {
|
|
59
|
+
const ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
60
|
+
return value.replace(/[&<>"']/g, (character) => ESCAPES[character]);
|
|
61
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Publish complete CLI output on the destination filesystem; exclusive linking protects concurrent writers. */
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { link, rename, unlink, writeFile } from "node:fs/promises";
|
|
4
|
+
/** Atomically publish complete bytes or text. Only explicit overwrite replaces an existing output. */
|
|
5
|
+
export async function publishOutput(destination, content, overwrite, signal) {
|
|
6
|
+
signal.throwIfAborted();
|
|
7
|
+
const temporaryPath = `${destination}.${randomUUID()}.tmp`;
|
|
8
|
+
try {
|
|
9
|
+
await writeFile(temporaryPath, content, { flag: "wx", signal });
|
|
10
|
+
signal.throwIfAborted();
|
|
11
|
+
if (overwrite)
|
|
12
|
+
await rename(temporaryPath, destination);
|
|
13
|
+
else
|
|
14
|
+
await link(temporaryPath, destination);
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
await unlink(temporaryPath).catch((error) => {
|
|
18
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
19
|
+
throw error;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { renderHtml } from "./node/html.js";
|
|
2
|
+
export type { HtmlRenderOptions } from "./node/html.js";
|
|
3
|
+
export { exportGif } from "./node/gif.js";
|
|
4
|
+
export type { NodeGifExportOptions } from "./node/gif.js";
|
|
5
|
+
export { GifExportError } from "./export/types.js";
|
|
6
|
+
export type { GifExportOptions, GifExportProgress, GifExportErrorCode } from "./export/types.js";
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Evan Wallace
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zac_ma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
Copyright (c) 2017 Matt DesLauriers
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
18
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
19
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
|
|
20
|
+
OR OTHER DEALINGS IN THE SOFTWARE.
|
|
21
|
+
|