@bicharts/chart-host 0.6.84 → 0.6.85
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/README.md +43 -0
- package/dist/chunk-BQFG5HWZ.mjs +582 -0
- package/dist/{chunk-2HKO6GEK.mjs → chunk-CDWNZHIN.mjs} +5 -575
- package/dist/chunk-ZGQURBRJ.mjs +265 -0
- package/dist/index.mjs +37 -275
- package/dist/legend-fit.mjs +1613 -0
- package/dist/react.mjs +2 -1
- package/dist/types/legendFit/frameFit.d.ts +89 -0
- package/dist/types/legendFit/frameFitDom.d.ts +70 -0
- package/dist/types/legendFit/index.d.ts +4 -0
- package/dist/types/legendFit/legendReconcile.d.ts +295 -0
- package/dist/types/legendFit/legendReconcileDom.d.ts +109 -0
- package/package.json +6 -1
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/*! @bicharts/chart-host — Apache-2.0. Bundled reference data: GeoNames (https://www.geonames.org/) CC BY 4.0; Natural Earth and US Census/TIGER (public domain). Full text: NOTICE in this package. */
|
|
2
|
+
import {
|
|
3
|
+
SNAPSHOT_SVG_ATTR
|
|
4
|
+
} from "./chunk-MR6TL7QZ.mjs";
|
|
5
|
+
|
|
6
|
+
// src/snapshot.ts
|
|
7
|
+
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
8
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
9
|
+
var XHTML_NS = "http://www.w3.org/1999/xhtml";
|
|
10
|
+
function toBase64(str) {
|
|
11
|
+
const bytes = new TextEncoder().encode(str);
|
|
12
|
+
let binary = "";
|
|
13
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
14
|
+
return btoa(binary);
|
|
15
|
+
}
|
|
16
|
+
function viewOf(el) {
|
|
17
|
+
return el.ownerDocument && el.ownerDocument.defaultView || globalThis;
|
|
18
|
+
}
|
|
19
|
+
var MAX_STYLED_ELEMENTS = 4e3;
|
|
20
|
+
var TEXT_FONT_PROPS = ["font-family", "font-size", "font-weight", "font-style"];
|
|
21
|
+
function inlineTextFonts(original, clone) {
|
|
22
|
+
const view = viewOf(original);
|
|
23
|
+
if (!view || typeof view.getComputedStyle !== "function") return;
|
|
24
|
+
const src = original.querySelectorAll("text");
|
|
25
|
+
const dst = clone.querySelectorAll("text");
|
|
26
|
+
if (src.length !== dst.length) return;
|
|
27
|
+
const n = Math.min(src.length, MAX_STYLED_ELEMENTS);
|
|
28
|
+
for (let i = 0; i < n; i++) {
|
|
29
|
+
let cs;
|
|
30
|
+
try {
|
|
31
|
+
cs = view.getComputedStyle(src[i]);
|
|
32
|
+
} catch {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const style = dst[i].style;
|
|
36
|
+
if (!style) continue;
|
|
37
|
+
for (const prop of TEXT_FONT_PROPS) {
|
|
38
|
+
const v = cs.getPropertyValue(prop);
|
|
39
|
+
if (v && !style.getPropertyValue(prop)) style.setProperty(prop, v);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function svgToDataUrl(svg) {
|
|
44
|
+
const cloned = svg.cloneNode(true);
|
|
45
|
+
const w = svg.clientWidth || svg.getBoundingClientRect && svg.getBoundingClientRect().width || 600;
|
|
46
|
+
const h = svg.clientHeight || svg.getBoundingClientRect && svg.getBoundingClientRect().height || 400;
|
|
47
|
+
cloned.setAttribute("width", String(Math.round(w)));
|
|
48
|
+
cloned.setAttribute("height", String(Math.round(h)));
|
|
49
|
+
if (!cloned.getAttribute("xmlns")) cloned.setAttribute("xmlns", SVG_NS);
|
|
50
|
+
try {
|
|
51
|
+
inlineTextFonts(svg, cloned);
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
return "data:image/svg+xml;base64," + toBase64(new XMLSerializer().serializeToString(cloned));
|
|
55
|
+
}
|
|
56
|
+
function tableHostOf(root) {
|
|
57
|
+
if (!root || typeof root.querySelector !== "function") return null;
|
|
58
|
+
if (root.namespaceURI === SVG_NS) return null;
|
|
59
|
+
const firstSvg = root.querySelector("svg");
|
|
60
|
+
for (const sel of [".lch-table-frame", "table"]) {
|
|
61
|
+
const host = root.querySelector(sel);
|
|
62
|
+
if (host && (!firstSvg || host.contains(firstSvg))) return host;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function chartSvgOf(root) {
|
|
67
|
+
if (!root || typeof root.querySelector !== "function") return null;
|
|
68
|
+
return root.querySelector(`svg[${SNAPSHOT_SVG_ATTR}]`) ?? root.querySelector("svg");
|
|
69
|
+
}
|
|
70
|
+
function inlinePx(v) {
|
|
71
|
+
const m = /^\s*([0-9.]+)px\s*$/.exec(v || "");
|
|
72
|
+
return m ? parseFloat(m[1]) : 0;
|
|
73
|
+
}
|
|
74
|
+
function htmlNaturalSize(el) {
|
|
75
|
+
const r = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
|
|
76
|
+
const w = el.clientWidth || r && r.width || inlinePx(el.style && el.style.width) || 600;
|
|
77
|
+
const h = el.clientHeight || r && r.height || inlinePx(el.style && el.style.height) || 400;
|
|
78
|
+
return { width: Math.round(w), height: Math.round(h) };
|
|
79
|
+
}
|
|
80
|
+
var HTML_STYLE_PROPS = [
|
|
81
|
+
"display",
|
|
82
|
+
"box-sizing",
|
|
83
|
+
"width",
|
|
84
|
+
"height",
|
|
85
|
+
"margin-top",
|
|
86
|
+
"margin-right",
|
|
87
|
+
"margin-bottom",
|
|
88
|
+
"margin-left",
|
|
89
|
+
"padding-top",
|
|
90
|
+
"padding-right",
|
|
91
|
+
"padding-bottom",
|
|
92
|
+
"padding-left",
|
|
93
|
+
"border-top-width",
|
|
94
|
+
"border-right-width",
|
|
95
|
+
"border-bottom-width",
|
|
96
|
+
"border-left-width",
|
|
97
|
+
"border-top-style",
|
|
98
|
+
"border-right-style",
|
|
99
|
+
"border-bottom-style",
|
|
100
|
+
"border-left-style",
|
|
101
|
+
"border-top-color",
|
|
102
|
+
"border-right-color",
|
|
103
|
+
"border-bottom-color",
|
|
104
|
+
"border-left-color",
|
|
105
|
+
"border-collapse",
|
|
106
|
+
"border-spacing",
|
|
107
|
+
"background-color",
|
|
108
|
+
"color",
|
|
109
|
+
"opacity",
|
|
110
|
+
"font-family",
|
|
111
|
+
"font-size",
|
|
112
|
+
"font-weight",
|
|
113
|
+
"font-style",
|
|
114
|
+
"line-height",
|
|
115
|
+
"letter-spacing",
|
|
116
|
+
"text-align",
|
|
117
|
+
"vertical-align",
|
|
118
|
+
"white-space",
|
|
119
|
+
"text-overflow",
|
|
120
|
+
"text-decoration-line",
|
|
121
|
+
"flex-direction",
|
|
122
|
+
"flex-wrap",
|
|
123
|
+
"flex-grow",
|
|
124
|
+
"flex-shrink",
|
|
125
|
+
"flex-basis",
|
|
126
|
+
"align-items",
|
|
127
|
+
"justify-content",
|
|
128
|
+
"position",
|
|
129
|
+
"top",
|
|
130
|
+
"right",
|
|
131
|
+
"bottom",
|
|
132
|
+
"left"
|
|
133
|
+
];
|
|
134
|
+
function inlineHtmlStyles(original, clone) {
|
|
135
|
+
const view = viewOf(original);
|
|
136
|
+
if (!view || typeof view.getComputedStyle !== "function") return;
|
|
137
|
+
const src = [original, ...Array.from(original.querySelectorAll("*"))];
|
|
138
|
+
const dst = [clone, ...Array.from(clone.querySelectorAll("*"))];
|
|
139
|
+
if (src.length !== dst.length) return;
|
|
140
|
+
let styled = 0;
|
|
141
|
+
for (let i = 0; i < src.length && styled < MAX_STYLED_ELEMENTS; i++) {
|
|
142
|
+
const o = src[i], c = dst[i];
|
|
143
|
+
if (o.namespaceURI !== XHTML_NS || !c.style) continue;
|
|
144
|
+
let cs;
|
|
145
|
+
try {
|
|
146
|
+
cs = view.getComputedStyle(o);
|
|
147
|
+
} catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
for (const prop of HTML_STYLE_PROPS) {
|
|
151
|
+
const v = cs.getPropertyValue(prop);
|
|
152
|
+
if (v) c.style.setProperty(prop, v);
|
|
153
|
+
}
|
|
154
|
+
c.style.setProperty("overflow", "hidden");
|
|
155
|
+
styled++;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
inlineTextFonts(original, clone);
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function htmlToSvgDataUrl(el) {
|
|
163
|
+
const { width, height } = htmlNaturalSize(el);
|
|
164
|
+
const clone = el.cloneNode(true);
|
|
165
|
+
inlineHtmlStyles(el, clone);
|
|
166
|
+
clone.style.setProperty("width", `${width}px`);
|
|
167
|
+
clone.style.setProperty("height", `${height}px`);
|
|
168
|
+
clone.style.setProperty("margin", "0");
|
|
169
|
+
let xhtml = new XMLSerializer().serializeToString(clone);
|
|
170
|
+
if (!/^<[^>]*\sxmlns=/.test(xhtml)) xhtml = xhtml.replace(/^<([a-zA-Z0-9-]+)/, `<$1 xmlns="${XHTML_NS}"`);
|
|
171
|
+
const svg = `<svg xmlns="${SVG_NS}" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><foreignObject x="0" y="0" width="${width}" height="${height}">${xhtml}</foreignObject></svg>`;
|
|
172
|
+
return { url: "data:image/svg+xml;base64," + toBase64(svg), width, height };
|
|
173
|
+
}
|
|
174
|
+
function svgNaturalSize(svg) {
|
|
175
|
+
const w = svg.clientWidth || svg.getBoundingClientRect && svg.getBoundingClientRect().width || 600;
|
|
176
|
+
const h = svg.clientHeight || svg.getBoundingClientRect && svg.getBoundingClientRect().height || 400;
|
|
177
|
+
return { width: Math.round(w), height: Math.round(h) };
|
|
178
|
+
}
|
|
179
|
+
function rasterizeSvgToPngDataUrl(svgDataUrl, width, height, opts = {}) {
|
|
180
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
181
|
+
return new Promise((resolve) => {
|
|
182
|
+
let img;
|
|
183
|
+
try {
|
|
184
|
+
img = new Image();
|
|
185
|
+
} catch (e) {
|
|
186
|
+
opts.onWarn?.("snapshot-no-image", e instanceof Error ? e.message : String(e));
|
|
187
|
+
resolve(null);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const timer = setTimeout(() => {
|
|
191
|
+
opts.onWarn?.("snapshot-raster-timeout", { width, height, timeoutMs });
|
|
192
|
+
resolve(null);
|
|
193
|
+
}, timeoutMs);
|
|
194
|
+
img.onload = () => {
|
|
195
|
+
clearTimeout(timer);
|
|
196
|
+
try {
|
|
197
|
+
const cap = opts.maxSide && opts.maxSide > 0 ? opts.maxSide : Math.max(width, height);
|
|
198
|
+
const scale = Math.min(1, cap / Math.max(width, height));
|
|
199
|
+
const cw = Math.max(1, Math.round(width * scale));
|
|
200
|
+
const ch = Math.max(1, Math.round(height * scale));
|
|
201
|
+
const canvas = document.createElement("canvas");
|
|
202
|
+
canvas.width = cw;
|
|
203
|
+
canvas.height = ch;
|
|
204
|
+
const c2d = canvas.getContext("2d");
|
|
205
|
+
if (!c2d) {
|
|
206
|
+
resolve(null);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
c2d.fillStyle = "white";
|
|
210
|
+
c2d.fillRect(0, 0, cw, ch);
|
|
211
|
+
c2d.drawImage(img, 0, 0, cw, ch);
|
|
212
|
+
resolve(canvas.toDataURL("image/png"));
|
|
213
|
+
} catch (e) {
|
|
214
|
+
opts.onWarn?.("snapshot-raster-draw", e instanceof Error ? e.message : String(e));
|
|
215
|
+
resolve(null);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
img.onerror = () => {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
opts.onWarn?.("snapshot-raster-decode", { width, height });
|
|
221
|
+
resolve(null);
|
|
222
|
+
};
|
|
223
|
+
img.src = svgDataUrl;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
async function captureSvgSnapshot(root, opts = {}) {
|
|
227
|
+
if (!root) return null;
|
|
228
|
+
const table = root instanceof SVGSVGElement ? null : tableHostOf(root);
|
|
229
|
+
const svg = table ? null : root instanceof SVGSVGElement ? root : chartSvgOf(root);
|
|
230
|
+
if (!table && !svg) return null;
|
|
231
|
+
let svgUrl;
|
|
232
|
+
let width, height;
|
|
233
|
+
try {
|
|
234
|
+
if (table) {
|
|
235
|
+
const r = htmlToSvgDataUrl(table);
|
|
236
|
+
svgUrl = r.url;
|
|
237
|
+
width = r.width;
|
|
238
|
+
height = r.height;
|
|
239
|
+
} else {
|
|
240
|
+
svgUrl = svgToDataUrl(svg);
|
|
241
|
+
({ width, height } = svgNaturalSize(svg));
|
|
242
|
+
}
|
|
243
|
+
} catch (e) {
|
|
244
|
+
opts.onWarn?.("snapshot-serialize", e instanceof Error ? e.message : String(e));
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const png = await rasterizeSvgToPngDataUrl(svgUrl, width, height, opts);
|
|
249
|
+
return png || svgUrl;
|
|
250
|
+
} catch (e) {
|
|
251
|
+
opts.onWarn?.("snapshot-raster-unexpected", e instanceof Error ? e.message : String(e));
|
|
252
|
+
return svgUrl;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export {
|
|
257
|
+
svgToDataUrl,
|
|
258
|
+
tableHostOf,
|
|
259
|
+
chartSvgOf,
|
|
260
|
+
htmlNaturalSize,
|
|
261
|
+
htmlToSvgDataUrl,
|
|
262
|
+
svgNaturalSize,
|
|
263
|
+
rasterizeSvgToPngDataUrl,
|
|
264
|
+
captureSvgSnapshot
|
|
265
|
+
};
|
package/dist/index.mjs
CHANGED
|
@@ -6,36 +6,38 @@ import {
|
|
|
6
6
|
isExcelDateFormat,
|
|
7
7
|
normaliseAggregation
|
|
8
8
|
} from "./chunk-GG4U66E4.mjs";
|
|
9
|
+
import {
|
|
10
|
+
captureSvgSnapshot,
|
|
11
|
+
chartSvgOf,
|
|
12
|
+
htmlNaturalSize,
|
|
13
|
+
htmlToSvgDataUrl,
|
|
14
|
+
rasterizeSvgToPngDataUrl,
|
|
15
|
+
svgNaturalSize,
|
|
16
|
+
svgToDataUrl,
|
|
17
|
+
tableHostOf
|
|
18
|
+
} from "./chunk-ZGQURBRJ.mjs";
|
|
9
19
|
import {
|
|
10
20
|
ANNOTATION_CLASS,
|
|
11
21
|
ANNOTATION_LAYER_CLASS,
|
|
12
22
|
AUTO_RAMP_CLASSES,
|
|
13
23
|
AUTO_RAMP_DRAWN_OPACITY,
|
|
14
24
|
AUTO_RAMP_MIN_STEP_DELTA_E,
|
|
15
|
-
AXIS_PIN_BAND_PAD_PX,
|
|
16
|
-
AXIS_PIN_MAX_BAND_FRACTION,
|
|
17
|
-
AXIS_PIN_MIN_LABELS,
|
|
18
|
-
AXIS_PIN_TRACK_REACH_PX,
|
|
19
25
|
BACKING_MAJORITY,
|
|
20
26
|
CLIP_TOLERANCE_PX,
|
|
21
27
|
D3_PLUGIN_PACKAGES,
|
|
22
28
|
DARK_TEXT,
|
|
23
|
-
FIT_CONTENT_SELECTOR,
|
|
24
29
|
GLYPH_SAMPLE_N,
|
|
25
30
|
LABEL_CONTRAST_CAP,
|
|
26
31
|
LABEL_CONTRAST_DONE_ATTR,
|
|
27
32
|
LABEL_PILL_CLASS,
|
|
28
33
|
LIGHT_TEXT,
|
|
29
|
-
MAX_FRAME_GROW_FACTOR,
|
|
30
34
|
MIN_CONTRAST,
|
|
31
35
|
MIN_RAMP_FILLS,
|
|
32
36
|
PAGE_MATCH_TOLERANCE,
|
|
33
|
-
PHANTOM_FRACTION,
|
|
34
37
|
PILL_MIN_ALPHA,
|
|
35
38
|
PILL_MIN_COVERAGE,
|
|
36
39
|
PILL_OPAQUE_ALPHA,
|
|
37
40
|
SAME_SHADE_DELTA_E,
|
|
38
|
-
SCROLL_SLACK_PX,
|
|
39
41
|
STRADDLE_COLS,
|
|
40
42
|
STRADDLE_MIN_SHARE,
|
|
41
43
|
STRADDLE_PILL_ALPHA,
|
|
@@ -47,7 +49,6 @@ import {
|
|
|
47
49
|
assembleD3,
|
|
48
50
|
autoRampCanvas,
|
|
49
51
|
autoRampMinStep,
|
|
50
|
-
axisPinPlacement,
|
|
51
52
|
backingHoldsGlyph,
|
|
52
53
|
blankRenderFlag,
|
|
53
54
|
buildRenderPayload,
|
|
@@ -61,37 +62,25 @@ import {
|
|
|
61
62
|
colourSpreadFlag,
|
|
62
63
|
compileRenderFn,
|
|
63
64
|
compositeOver,
|
|
64
|
-
contentExtentOf,
|
|
65
65
|
contrastRatio,
|
|
66
66
|
createChartGroup,
|
|
67
67
|
createChartHost,
|
|
68
|
-
ctmScaleOf,
|
|
69
68
|
decideLabelColor,
|
|
70
69
|
deltaEHex,
|
|
71
70
|
explainRenderFailure,
|
|
72
|
-
fitReadingFor,
|
|
73
|
-
fitRenderedChart,
|
|
74
71
|
geoAssetFor,
|
|
75
72
|
geoFromCache,
|
|
76
73
|
glyphSampleGrid,
|
|
77
74
|
invalidSentinelReason,
|
|
78
75
|
isBlankRender,
|
|
79
|
-
isHorizontalLabelRow,
|
|
80
76
|
isInvalidSentinelError,
|
|
81
|
-
isPhantomBox,
|
|
82
77
|
isPillBackdropAlpha,
|
|
83
|
-
labelBand,
|
|
84
78
|
loadGeo,
|
|
85
|
-
measureContainerBoxes,
|
|
86
79
|
minContrastOver,
|
|
87
|
-
needsScroll,
|
|
88
80
|
newIdScope,
|
|
89
81
|
noopViewStateProvider,
|
|
90
82
|
pickColorOver,
|
|
91
83
|
pillBacksGlyph,
|
|
92
|
-
pinScrolledAxis,
|
|
93
|
-
planAxisPin,
|
|
94
|
-
planFrameGrow,
|
|
95
84
|
registerGeo,
|
|
96
85
|
registerGeoAsset,
|
|
97
86
|
relativeLuminance,
|
|
@@ -99,19 +88,16 @@ import {
|
|
|
99
88
|
resolveAutoColorScale,
|
|
100
89
|
resolveOptions,
|
|
101
90
|
scopeChartIds,
|
|
102
|
-
scrollFitFor,
|
|
103
91
|
sessionViewStateProvider,
|
|
104
92
|
straddleSampleGrid,
|
|
105
93
|
straddles,
|
|
106
94
|
stripEsmExports,
|
|
107
95
|
stripJsComments,
|
|
108
|
-
svgInkReach,
|
|
109
96
|
syncMemberSelection,
|
|
110
97
|
toRGBA,
|
|
111
98
|
toSourceRows,
|
|
112
|
-
unpinScrolledAxis,
|
|
113
99
|
valuePlacementFlag
|
|
114
|
-
} from "./chunk-
|
|
100
|
+
} from "./chunk-CDWNZHIN.mjs";
|
|
115
101
|
import {
|
|
116
102
|
LASSO_AXES_SLOT,
|
|
117
103
|
LASSO_COUNT_CLASS,
|
|
@@ -173,6 +159,32 @@ import {
|
|
|
173
159
|
trackPress,
|
|
174
160
|
validRowSet
|
|
175
161
|
} from "./chunk-IURROJT7.mjs";
|
|
162
|
+
import {
|
|
163
|
+
AXIS_PIN_BAND_PAD_PX,
|
|
164
|
+
AXIS_PIN_MAX_BAND_FRACTION,
|
|
165
|
+
AXIS_PIN_MIN_LABELS,
|
|
166
|
+
AXIS_PIN_TRACK_REACH_PX,
|
|
167
|
+
FIT_CONTENT_SELECTOR,
|
|
168
|
+
MAX_FRAME_GROW_FACTOR,
|
|
169
|
+
PHANTOM_FRACTION,
|
|
170
|
+
SCROLL_SLACK_PX,
|
|
171
|
+
axisPinPlacement,
|
|
172
|
+
contentExtentOf,
|
|
173
|
+
ctmScaleOf,
|
|
174
|
+
fitReadingFor,
|
|
175
|
+
fitRenderedChart,
|
|
176
|
+
isHorizontalLabelRow,
|
|
177
|
+
isPhantomBox,
|
|
178
|
+
labelBand,
|
|
179
|
+
measureContainerBoxes,
|
|
180
|
+
needsScroll,
|
|
181
|
+
pinScrolledAxis,
|
|
182
|
+
planAxisPin,
|
|
183
|
+
planFrameGrow,
|
|
184
|
+
scrollFitFor,
|
|
185
|
+
svgInkReach,
|
|
186
|
+
unpinScrolledAxis
|
|
187
|
+
} from "./chunk-BQFG5HWZ.mjs";
|
|
176
188
|
import "./chunk-A2GMXZP7.mjs";
|
|
177
189
|
import {
|
|
178
190
|
ACTIVE_TICK_CLASS,
|
|
@@ -601,256 +613,6 @@ function histogramSource(idx, name) {
|
|
|
601
613
|
}`;
|
|
602
614
|
}
|
|
603
615
|
|
|
604
|
-
// src/snapshot.ts
|
|
605
|
-
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
606
|
-
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
607
|
-
var XHTML_NS = "http://www.w3.org/1999/xhtml";
|
|
608
|
-
function toBase64(str) {
|
|
609
|
-
const bytes = new TextEncoder().encode(str);
|
|
610
|
-
let binary = "";
|
|
611
|
-
for (let i2 = 0; i2 < bytes.length; i2++) binary += String.fromCharCode(bytes[i2]);
|
|
612
|
-
return btoa(binary);
|
|
613
|
-
}
|
|
614
|
-
function viewOf(el) {
|
|
615
|
-
return el.ownerDocument && el.ownerDocument.defaultView || globalThis;
|
|
616
|
-
}
|
|
617
|
-
var MAX_STYLED_ELEMENTS = 4e3;
|
|
618
|
-
var TEXT_FONT_PROPS = ["font-family", "font-size", "font-weight", "font-style"];
|
|
619
|
-
function inlineTextFonts(original, clone) {
|
|
620
|
-
const view = viewOf(original);
|
|
621
|
-
if (!view || typeof view.getComputedStyle !== "function") return;
|
|
622
|
-
const src = original.querySelectorAll("text");
|
|
623
|
-
const dst = clone.querySelectorAll("text");
|
|
624
|
-
if (src.length !== dst.length) return;
|
|
625
|
-
const n2 = Math.min(src.length, MAX_STYLED_ELEMENTS);
|
|
626
|
-
for (let i2 = 0; i2 < n2; i2++) {
|
|
627
|
-
let cs;
|
|
628
|
-
try {
|
|
629
|
-
cs = view.getComputedStyle(src[i2]);
|
|
630
|
-
} catch {
|
|
631
|
-
continue;
|
|
632
|
-
}
|
|
633
|
-
const style = dst[i2].style;
|
|
634
|
-
if (!style) continue;
|
|
635
|
-
for (const prop of TEXT_FONT_PROPS) {
|
|
636
|
-
const v2 = cs.getPropertyValue(prop);
|
|
637
|
-
if (v2 && !style.getPropertyValue(prop)) style.setProperty(prop, v2);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
function svgToDataUrl(svg) {
|
|
642
|
-
const cloned = svg.cloneNode(true);
|
|
643
|
-
const w2 = svg.clientWidth || svg.getBoundingClientRect && svg.getBoundingClientRect().width || 600;
|
|
644
|
-
const h2 = svg.clientHeight || svg.getBoundingClientRect && svg.getBoundingClientRect().height || 400;
|
|
645
|
-
cloned.setAttribute("width", String(Math.round(w2)));
|
|
646
|
-
cloned.setAttribute("height", String(Math.round(h2)));
|
|
647
|
-
if (!cloned.getAttribute("xmlns")) cloned.setAttribute("xmlns", SVG_NS);
|
|
648
|
-
try {
|
|
649
|
-
inlineTextFonts(svg, cloned);
|
|
650
|
-
} catch {
|
|
651
|
-
}
|
|
652
|
-
return "data:image/svg+xml;base64," + toBase64(new XMLSerializer().serializeToString(cloned));
|
|
653
|
-
}
|
|
654
|
-
function tableHostOf(root) {
|
|
655
|
-
if (!root || typeof root.querySelector !== "function") return null;
|
|
656
|
-
if (root.namespaceURI === SVG_NS) return null;
|
|
657
|
-
const firstSvg = root.querySelector("svg");
|
|
658
|
-
for (const sel of [".lch-table-frame", "table"]) {
|
|
659
|
-
const host = root.querySelector(sel);
|
|
660
|
-
if (host && (!firstSvg || host.contains(firstSvg))) return host;
|
|
661
|
-
}
|
|
662
|
-
return null;
|
|
663
|
-
}
|
|
664
|
-
function chartSvgOf(root) {
|
|
665
|
-
if (!root || typeof root.querySelector !== "function") return null;
|
|
666
|
-
return root.querySelector(`svg[${SNAPSHOT_SVG_ATTR}]`) ?? root.querySelector("svg");
|
|
667
|
-
}
|
|
668
|
-
function inlinePx(v2) {
|
|
669
|
-
const m2 = /^\s*([0-9.]+)px\s*$/.exec(v2 || "");
|
|
670
|
-
return m2 ? parseFloat(m2[1]) : 0;
|
|
671
|
-
}
|
|
672
|
-
function htmlNaturalSize(el) {
|
|
673
|
-
const r2 = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
|
|
674
|
-
const w2 = el.clientWidth || r2 && r2.width || inlinePx(el.style && el.style.width) || 600;
|
|
675
|
-
const h2 = el.clientHeight || r2 && r2.height || inlinePx(el.style && el.style.height) || 400;
|
|
676
|
-
return { width: Math.round(w2), height: Math.round(h2) };
|
|
677
|
-
}
|
|
678
|
-
var HTML_STYLE_PROPS = [
|
|
679
|
-
"display",
|
|
680
|
-
"box-sizing",
|
|
681
|
-
"width",
|
|
682
|
-
"height",
|
|
683
|
-
"margin-top",
|
|
684
|
-
"margin-right",
|
|
685
|
-
"margin-bottom",
|
|
686
|
-
"margin-left",
|
|
687
|
-
"padding-top",
|
|
688
|
-
"padding-right",
|
|
689
|
-
"padding-bottom",
|
|
690
|
-
"padding-left",
|
|
691
|
-
"border-top-width",
|
|
692
|
-
"border-right-width",
|
|
693
|
-
"border-bottom-width",
|
|
694
|
-
"border-left-width",
|
|
695
|
-
"border-top-style",
|
|
696
|
-
"border-right-style",
|
|
697
|
-
"border-bottom-style",
|
|
698
|
-
"border-left-style",
|
|
699
|
-
"border-top-color",
|
|
700
|
-
"border-right-color",
|
|
701
|
-
"border-bottom-color",
|
|
702
|
-
"border-left-color",
|
|
703
|
-
"border-collapse",
|
|
704
|
-
"border-spacing",
|
|
705
|
-
"background-color",
|
|
706
|
-
"color",
|
|
707
|
-
"opacity",
|
|
708
|
-
"font-family",
|
|
709
|
-
"font-size",
|
|
710
|
-
"font-weight",
|
|
711
|
-
"font-style",
|
|
712
|
-
"line-height",
|
|
713
|
-
"letter-spacing",
|
|
714
|
-
"text-align",
|
|
715
|
-
"vertical-align",
|
|
716
|
-
"white-space",
|
|
717
|
-
"text-overflow",
|
|
718
|
-
"text-decoration-line",
|
|
719
|
-
"flex-direction",
|
|
720
|
-
"flex-wrap",
|
|
721
|
-
"flex-grow",
|
|
722
|
-
"flex-shrink",
|
|
723
|
-
"flex-basis",
|
|
724
|
-
"align-items",
|
|
725
|
-
"justify-content",
|
|
726
|
-
"position",
|
|
727
|
-
"top",
|
|
728
|
-
"right",
|
|
729
|
-
"bottom",
|
|
730
|
-
"left"
|
|
731
|
-
];
|
|
732
|
-
function inlineHtmlStyles(original, clone) {
|
|
733
|
-
const view = viewOf(original);
|
|
734
|
-
if (!view || typeof view.getComputedStyle !== "function") return;
|
|
735
|
-
const src = [original, ...Array.from(original.querySelectorAll("*"))];
|
|
736
|
-
const dst = [clone, ...Array.from(clone.querySelectorAll("*"))];
|
|
737
|
-
if (src.length !== dst.length) return;
|
|
738
|
-
let styled = 0;
|
|
739
|
-
for (let i2 = 0; i2 < src.length && styled < MAX_STYLED_ELEMENTS; i2++) {
|
|
740
|
-
const o3 = src[i2], c2 = dst[i2];
|
|
741
|
-
if (o3.namespaceURI !== XHTML_NS || !c2.style) continue;
|
|
742
|
-
let cs;
|
|
743
|
-
try {
|
|
744
|
-
cs = view.getComputedStyle(o3);
|
|
745
|
-
} catch {
|
|
746
|
-
continue;
|
|
747
|
-
}
|
|
748
|
-
for (const prop of HTML_STYLE_PROPS) {
|
|
749
|
-
const v2 = cs.getPropertyValue(prop);
|
|
750
|
-
if (v2) c2.style.setProperty(prop, v2);
|
|
751
|
-
}
|
|
752
|
-
c2.style.setProperty("overflow", "hidden");
|
|
753
|
-
styled++;
|
|
754
|
-
}
|
|
755
|
-
try {
|
|
756
|
-
inlineTextFonts(original, clone);
|
|
757
|
-
} catch {
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
function htmlToSvgDataUrl(el) {
|
|
761
|
-
const { width, height } = htmlNaturalSize(el);
|
|
762
|
-
const clone = el.cloneNode(true);
|
|
763
|
-
inlineHtmlStyles(el, clone);
|
|
764
|
-
clone.style.setProperty("width", `${width}px`);
|
|
765
|
-
clone.style.setProperty("height", `${height}px`);
|
|
766
|
-
clone.style.setProperty("margin", "0");
|
|
767
|
-
let xhtml = new XMLSerializer().serializeToString(clone);
|
|
768
|
-
if (!/^<[^>]*\sxmlns=/.test(xhtml)) xhtml = xhtml.replace(/^<([a-zA-Z0-9-]+)/, `<$1 xmlns="${XHTML_NS}"`);
|
|
769
|
-
const svg = `<svg xmlns="${SVG_NS}" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><foreignObject x="0" y="0" width="${width}" height="${height}">${xhtml}</foreignObject></svg>`;
|
|
770
|
-
return { url: "data:image/svg+xml;base64," + toBase64(svg), width, height };
|
|
771
|
-
}
|
|
772
|
-
function svgNaturalSize(svg) {
|
|
773
|
-
const w2 = svg.clientWidth || svg.getBoundingClientRect && svg.getBoundingClientRect().width || 600;
|
|
774
|
-
const h2 = svg.clientHeight || svg.getBoundingClientRect && svg.getBoundingClientRect().height || 400;
|
|
775
|
-
return { width: Math.round(w2), height: Math.round(h2) };
|
|
776
|
-
}
|
|
777
|
-
function rasterizeSvgToPngDataUrl(svgDataUrl, width, height, opts = {}) {
|
|
778
|
-
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
779
|
-
return new Promise((resolve) => {
|
|
780
|
-
let img;
|
|
781
|
-
try {
|
|
782
|
-
img = new Image();
|
|
783
|
-
} catch (e2) {
|
|
784
|
-
opts.onWarn?.("snapshot-no-image", e2 instanceof Error ? e2.message : String(e2));
|
|
785
|
-
resolve(null);
|
|
786
|
-
return;
|
|
787
|
-
}
|
|
788
|
-
const timer = setTimeout(() => {
|
|
789
|
-
opts.onWarn?.("snapshot-raster-timeout", { width, height, timeoutMs });
|
|
790
|
-
resolve(null);
|
|
791
|
-
}, timeoutMs);
|
|
792
|
-
img.onload = () => {
|
|
793
|
-
clearTimeout(timer);
|
|
794
|
-
try {
|
|
795
|
-
const cap = opts.maxSide && opts.maxSide > 0 ? opts.maxSide : Math.max(width, height);
|
|
796
|
-
const scale = Math.min(1, cap / Math.max(width, height));
|
|
797
|
-
const cw = Math.max(1, Math.round(width * scale));
|
|
798
|
-
const ch = Math.max(1, Math.round(height * scale));
|
|
799
|
-
const canvas = document.createElement("canvas");
|
|
800
|
-
canvas.width = cw;
|
|
801
|
-
canvas.height = ch;
|
|
802
|
-
const c2d = canvas.getContext("2d");
|
|
803
|
-
if (!c2d) {
|
|
804
|
-
resolve(null);
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
c2d.fillStyle = "white";
|
|
808
|
-
c2d.fillRect(0, 0, cw, ch);
|
|
809
|
-
c2d.drawImage(img, 0, 0, cw, ch);
|
|
810
|
-
resolve(canvas.toDataURL("image/png"));
|
|
811
|
-
} catch (e2) {
|
|
812
|
-
opts.onWarn?.("snapshot-raster-draw", e2 instanceof Error ? e2.message : String(e2));
|
|
813
|
-
resolve(null);
|
|
814
|
-
}
|
|
815
|
-
};
|
|
816
|
-
img.onerror = () => {
|
|
817
|
-
clearTimeout(timer);
|
|
818
|
-
opts.onWarn?.("snapshot-raster-decode", { width, height });
|
|
819
|
-
resolve(null);
|
|
820
|
-
};
|
|
821
|
-
img.src = svgDataUrl;
|
|
822
|
-
});
|
|
823
|
-
}
|
|
824
|
-
async function captureSvgSnapshot(root, opts = {}) {
|
|
825
|
-
if (!root) return null;
|
|
826
|
-
const table = root instanceof SVGSVGElement ? null : tableHostOf(root);
|
|
827
|
-
const svg = table ? null : root instanceof SVGSVGElement ? root : chartSvgOf(root);
|
|
828
|
-
if (!table && !svg) return null;
|
|
829
|
-
let svgUrl;
|
|
830
|
-
let width, height;
|
|
831
|
-
try {
|
|
832
|
-
if (table) {
|
|
833
|
-
const r2 = htmlToSvgDataUrl(table);
|
|
834
|
-
svgUrl = r2.url;
|
|
835
|
-
width = r2.width;
|
|
836
|
-
height = r2.height;
|
|
837
|
-
} else {
|
|
838
|
-
svgUrl = svgToDataUrl(svg);
|
|
839
|
-
({ width, height } = svgNaturalSize(svg));
|
|
840
|
-
}
|
|
841
|
-
} catch (e2) {
|
|
842
|
-
opts.onWarn?.("snapshot-serialize", e2 instanceof Error ? e2.message : String(e2));
|
|
843
|
-
return null;
|
|
844
|
-
}
|
|
845
|
-
try {
|
|
846
|
-
const png = await rasterizeSvgToPngDataUrl(svgUrl, width, height, opts);
|
|
847
|
-
return png || svgUrl;
|
|
848
|
-
} catch (e2) {
|
|
849
|
-
opts.onWarn?.("snapshot-raster-unexpected", e2 instanceof Error ? e2.message : String(e2));
|
|
850
|
-
return svgUrl;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
616
|
// src/review.ts
|
|
855
617
|
function shouldReview(g2) {
|
|
856
618
|
if (!g2.enabled) return false;
|