@bicharts/chart-host 0.6.84 → 0.6.86

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.
@@ -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-2HKO6GEK.mjs";
100
+ } from "./chunk-MXL5SK7X.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;