@node-projects/excelforge 2.4.0 → 3.1.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.
Files changed (74) hide show
  1. package/.github/FUNDING.yml +4 -0
  2. package/FEATURES.md +294 -0
  3. package/README.md +628 -12
  4. package/dist/core/SharedStrings.js +6 -2
  5. package/dist/core/SharedStrings.js.map +1 -1
  6. package/dist/core/Workbook.d.ts +43 -1
  7. package/dist/core/Workbook.js +881 -58
  8. package/dist/core/Workbook.js.map +1 -1
  9. package/dist/core/WorkbookReader.d.ts +18 -4
  10. package/dist/core/WorkbookReader.js +1386 -20
  11. package/dist/core/WorkbookReader.js.map +1 -1
  12. package/dist/core/Worksheet.d.ts +136 -2
  13. package/dist/core/Worksheet.js +828 -63
  14. package/dist/core/Worksheet.js.map +1 -1
  15. package/dist/core/types.d.ts +311 -5
  16. package/dist/core/types.js +12 -1
  17. package/dist/core/types.js.map +1 -1
  18. package/dist/features/ChartBuilder.d.ts +9 -1
  19. package/dist/features/ChartBuilder.js +140 -14
  20. package/dist/features/ChartBuilder.js.map +1 -1
  21. package/dist/features/CsvModule.d.ts +11 -0
  22. package/dist/features/CsvModule.js +137 -0
  23. package/dist/features/CsvModule.js.map +1 -0
  24. package/dist/features/Encryption.d.ts +6 -0
  25. package/dist/features/Encryption.js +806 -0
  26. package/dist/features/Encryption.js.map +1 -0
  27. package/dist/features/FormControlBuilder.d.ts +6 -0
  28. package/dist/features/FormControlBuilder.js +135 -0
  29. package/dist/features/FormControlBuilder.js.map +1 -0
  30. package/dist/features/FormulaEngine.d.ts +22 -0
  31. package/dist/features/FormulaEngine.js +498 -0
  32. package/dist/features/FormulaEngine.js.map +1 -0
  33. package/dist/features/HtmlModule.d.ts +22 -0
  34. package/dist/features/HtmlModule.js +1441 -0
  35. package/dist/features/HtmlModule.js.map +1 -0
  36. package/dist/features/JsonModule.d.ts +10 -0
  37. package/dist/features/JsonModule.js +76 -0
  38. package/dist/features/JsonModule.js.map +1 -0
  39. package/dist/features/PdfModule.d.ts +30 -0
  40. package/dist/features/PdfModule.js +1567 -0
  41. package/dist/features/PdfModule.js.map +1 -0
  42. package/dist/features/PivotTableBuilder.d.ts +7 -0
  43. package/dist/features/PivotTableBuilder.js +170 -0
  44. package/dist/features/PivotTableBuilder.js.map +1 -0
  45. package/dist/features/Signing.d.ts +12 -0
  46. package/dist/features/Signing.js +326 -0
  47. package/dist/features/Signing.js.map +1 -0
  48. package/dist/features/TableBuilder.js +2 -2
  49. package/dist/features/TableBuilder.js.map +1 -1
  50. package/dist/index-min.js +609 -147
  51. package/dist/index.d.ts +19 -1
  52. package/dist/index.js +11 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/styles/StyleRegistry.d.ts +14 -0
  55. package/dist/styles/StyleRegistry.js +95 -30
  56. package/dist/styles/StyleRegistry.js.map +1 -1
  57. package/dist/utils/helpers.d.ts +4 -0
  58. package/dist/utils/helpers.js +64 -14
  59. package/dist/utils/helpers.js.map +1 -1
  60. package/dist/utils/zip.js +145 -73
  61. package/dist/utils/zip.js.map +1 -1
  62. package/dist/vba/VbaProject.d.ts +31 -0
  63. package/dist/vba/VbaProject.js +576 -0
  64. package/dist/vba/VbaProject.js.map +1 -0
  65. package/dist/vba/cfb.d.ts +7 -0
  66. package/dist/vba/cfb.js +352 -0
  67. package/dist/vba/cfb.js.map +1 -0
  68. package/dist/vba/ovba.d.ts +2 -0
  69. package/dist/vba/ovba.js +137 -0
  70. package/dist/vba/ovba.js.map +1 -0
  71. package/package.json +4 -3
  72. package/validator.cs +0 -155
  73. package/validatorEpplus.cs +0 -27
  74. package/validatorReadData.cs +0 -111
@@ -0,0 +1,1441 @@
1
+ import { escapeXml, colIndexToLetter } from '../utils/helpers.js';
2
+ import { FormulaEngine } from './FormulaEngine.js';
3
+ const THEME_COLORS = [
4
+ '#000000', '#FFFFFF', '#44546A', '#E7E6E6', '#4472C4', '#ED7D31',
5
+ '#A5A5A5', '#FFC000', '#5B9BD5', '#70AD47',
6
+ ];
7
+ function colorToCSS(c) {
8
+ if (!c)
9
+ return '';
10
+ if (c.startsWith('#'))
11
+ return c;
12
+ if (c.startsWith('theme:')) {
13
+ const idx = parseInt(c.slice(6), 10);
14
+ return THEME_COLORS[idx] ?? '#000';
15
+ }
16
+ if (c.length === 8 && !c.startsWith('#'))
17
+ return '#' + c.slice(2);
18
+ return '#' + c;
19
+ }
20
+ function parseColor(c) {
21
+ const hex = c.replace('#', '');
22
+ return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
23
+ }
24
+ function interpolateColor(c1, c2, t) {
25
+ const [r1, g1, b1] = parseColor(colorToCSS(c1) || '#FFFFFF');
26
+ const [r2, g2, b2] = parseColor(colorToCSS(c2) || '#000000');
27
+ const r = Math.round(r1 + (r2 - r1) * t);
28
+ const g = Math.round(g1 + (g2 - g1) * t);
29
+ const b = Math.round(b1 + (b2 - b1) * t);
30
+ return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
31
+ }
32
+ function fontToCSS(f) {
33
+ const parts = [];
34
+ if (f.bold)
35
+ parts.push('font-weight:bold');
36
+ if (f.italic)
37
+ parts.push('font-style:italic');
38
+ const decs = [];
39
+ if (f.underline && f.underline !== 'none')
40
+ decs.push('underline');
41
+ if (f.strike)
42
+ decs.push('line-through');
43
+ if (decs.length)
44
+ parts.push(`text-decoration:${decs.join(' ')}`);
45
+ if (f.size)
46
+ parts.push(`font-size:${f.size}pt`);
47
+ if (f.color)
48
+ parts.push(`color:${colorToCSS(f.color)}`);
49
+ if (f.name)
50
+ parts.push(`font-family:'${f.name}',sans-serif`);
51
+ if (f.vertAlign === 'superscript')
52
+ parts.push('vertical-align:super;font-size:smaller');
53
+ else if (f.vertAlign === 'subscript')
54
+ parts.push('vertical-align:sub;font-size:smaller');
55
+ return parts.join(';');
56
+ }
57
+ function fillToCSS(fill) {
58
+ if (fill.type === 'pattern') {
59
+ const pf = fill;
60
+ if (pf.pattern === 'solid' && pf.fgColor)
61
+ return `background-color:${colorToCSS(pf.fgColor)}`;
62
+ }
63
+ if (fill.type === 'gradient') {
64
+ const gf = fill;
65
+ if (gf.stops && gf.stops.length >= 2) {
66
+ const stops = gf.stops.map(s => `${colorToCSS(s.color)} ${Math.round(s.position * 100)}%`).join(',');
67
+ const deg = gf.degree ?? 0;
68
+ return `background:linear-gradient(${deg}deg,${stops})`;
69
+ }
70
+ }
71
+ return '';
72
+ }
73
+ function borderSideCSS(side) {
74
+ if (!side || !side.style)
75
+ return '';
76
+ const widthMap = {
77
+ thin: '1px', medium: '2px', thick: '3px', dashed: '1px', dotted: '1px',
78
+ double: '3px', hair: '1px', mediumDashed: '2px', dashDot: '1px',
79
+ mediumDashDot: '2px', dashDotDot: '1px', mediumDashDotDot: '2px', slantDashDot: '2px',
80
+ };
81
+ const styleMap = {
82
+ thin: 'solid', medium: 'solid', thick: 'solid', dashed: 'dashed', dotted: 'dotted',
83
+ double: 'double', hair: 'solid', mediumDashed: 'dashed', dashDot: 'dashed',
84
+ mediumDashDot: 'dashed', dashDotDot: 'dotted', mediumDashDotDot: 'dotted', slantDashDot: 'dashed',
85
+ };
86
+ const w = widthMap[side.style] ?? '1px';
87
+ const s = styleMap[side.style] ?? 'solid';
88
+ const c = side.color ? colorToCSS(side.color) : '#000';
89
+ return `${w} ${s} ${c}`;
90
+ }
91
+ function alignmentCSS(a) {
92
+ const parts = [];
93
+ if (a.horizontal) {
94
+ const hMap = { left: 'left', center: 'center', right: 'right', fill: 'justify', justify: 'justify', distributed: 'justify' };
95
+ parts.push(`text-align:${hMap[a.horizontal] ?? a.horizontal}`);
96
+ }
97
+ if (a.vertical) {
98
+ const vMap = { top: 'top', center: 'middle', bottom: 'bottom', distributed: 'middle' };
99
+ parts.push(`vertical-align:${vMap[a.vertical] ?? 'bottom'}`);
100
+ }
101
+ if (a.wrapText)
102
+ parts.push('white-space:normal;word-wrap:break-word');
103
+ if (a.textRotation)
104
+ parts.push(`transform:rotate(-${a.textRotation}deg)`);
105
+ if (a.indent)
106
+ parts.push(`padding-left:${a.indent * 8}px`);
107
+ return parts.join(';');
108
+ }
109
+ function styleToCSS(s) {
110
+ const parts = [];
111
+ if (s.font)
112
+ parts.push(fontToCSS(s.font));
113
+ if (s.fill)
114
+ parts.push(fillToCSS(s.fill));
115
+ if (s.alignment)
116
+ parts.push(alignmentCSS(s.alignment));
117
+ if (s.border) {
118
+ if (s.border.top)
119
+ parts.push(`border-top:${borderSideCSS(s.border.top)}`);
120
+ if (s.border.bottom)
121
+ parts.push(`border-bottom:${borderSideCSS(s.border.bottom)}`);
122
+ if (s.border.left)
123
+ parts.push(`border-left:${borderSideCSS(s.border.left)}`);
124
+ if (s.border.right)
125
+ parts.push(`border-right:${borderSideCSS(s.border.right)}`);
126
+ }
127
+ return parts.filter(Boolean).join(';');
128
+ }
129
+ function formatNumber(value, fmt) {
130
+ if (value == null)
131
+ return '';
132
+ if (!fmt || fmt === 'General')
133
+ return String(value);
134
+ const num = typeof value === 'number' ? value : parseFloat(String(value));
135
+ if (isNaN(num))
136
+ return String(value);
137
+ if (fmt.includes('%')) {
138
+ const decimals = (fmt.match(/0\.(0+)%/) ?? [])[1]?.length ?? 0;
139
+ return (num * 100).toFixed(decimals) + '%';
140
+ }
141
+ const currMatch = fmt.match(/[$€£¥]|"CHF"/);
142
+ if (currMatch) {
143
+ const sym = currMatch[0].replace(/"/g, '');
144
+ const decimals = (fmt.match(/\.(0+)/) ?? [])[1]?.length ?? 2;
145
+ const formatted = Math.abs(num).toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
146
+ if (fmt.indexOf(currMatch[0]) < fmt.indexOf('0')) {
147
+ return (num < 0 ? '-' : '') + sym + formatted;
148
+ }
149
+ return (num < 0 ? '-' : '') + formatted + ' ' + sym;
150
+ }
151
+ if (fmt.includes('#,##0') || fmt.includes('#,###')) {
152
+ const decimals = (fmt.match(/\.(0+)/) ?? [])[1]?.length ?? 0;
153
+ return num.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
154
+ }
155
+ const fixedMatch = fmt.match(/^0\.(0+)$/);
156
+ if (fixedMatch)
157
+ return num.toFixed(fixedMatch[1].length);
158
+ if (/[ymdh]/i.test(fmt))
159
+ return formatDate(num, fmt);
160
+ if (fmt.includes('?/?') || fmt.includes('??/??'))
161
+ return formatFraction(num);
162
+ if (/0\.0+E\+0+/i.test(fmt)) {
163
+ const decimals = (fmt.match(/0\.(0+)/) ?? [])[1]?.length ?? 2;
164
+ return num.toExponential(decimals).toUpperCase();
165
+ }
166
+ return String(value);
167
+ }
168
+ function formatDate(serial, fmt) {
169
+ const epoch = new Date(1899, 11, 30);
170
+ const d = new Date(epoch.getTime() + serial * 86400000);
171
+ const Y = d.getFullYear(), M = d.getMonth() + 1, D = d.getDate();
172
+ const h = d.getHours(), m = d.getMinutes(), s = d.getSeconds();
173
+ return fmt
174
+ .replace(/yyyy/gi, String(Y))
175
+ .replace(/yy/gi, String(Y).slice(-2))
176
+ .replace(/mmmm/gi, d.toLocaleDateString('en', { month: 'long' }))
177
+ .replace(/mmm/gi, d.toLocaleDateString('en', { month: 'short' }))
178
+ .replace(/mm/gi, String(M).padStart(2, '0'))
179
+ .replace(/m/gi, String(M))
180
+ .replace(/dd/gi, String(D).padStart(2, '0'))
181
+ .replace(/d/gi, String(D))
182
+ .replace(/hh/gi, String(h).padStart(2, '0'))
183
+ .replace(/h/gi, String(h))
184
+ .replace(/ss/gi, String(s).padStart(2, '0'))
185
+ .replace(/nn|MM/g, String(m).padStart(2, '0'));
186
+ }
187
+ function formatFraction(num) {
188
+ const whole = Math.floor(Math.abs(num));
189
+ const frac = Math.abs(num) - whole;
190
+ if (frac < 0.0001)
191
+ return String(num < 0 ? -whole : whole);
192
+ let bestN = 0, bestD = 1, bestErr = 1;
193
+ for (let d = 1; d <= 100; d++) {
194
+ const n = Math.round(frac * d);
195
+ const err = Math.abs(frac - n / d);
196
+ if (err < bestErr) {
197
+ bestN = n;
198
+ bestD = d;
199
+ bestErr = err;
200
+ }
201
+ if (err < 0.0001)
202
+ break;
203
+ }
204
+ const sign = num < 0 ? '-' : '';
205
+ return whole > 0 ? `${sign}${whole} ${bestN}/${bestD}` : `${sign}${bestN}/${bestD}`;
206
+ }
207
+ function evaluateConditionalFormats(cf, value, allValues) {
208
+ if (cf.colorScale) {
209
+ const sorted = [...allValues].sort((a, b) => a - b);
210
+ const min = sorted[0], max = sorted[sorted.length - 1];
211
+ const range = max - min || 1;
212
+ const t = (value - min) / range;
213
+ const cs = cf.colorScale;
214
+ if (cs.color.length === 2)
215
+ return `background-color:${interpolateColor(cs.color[0], cs.color[1], t)}`;
216
+ if (cs.color.length >= 3) {
217
+ if (t <= 0.5)
218
+ return `background-color:${interpolateColor(cs.color[0], cs.color[1], t * 2)}`;
219
+ return `background-color:${interpolateColor(cs.color[1], cs.color[2], (t - 0.5) * 2)}`;
220
+ }
221
+ }
222
+ if (cf.dataBar) {
223
+ const sorted = [...allValues].sort((a, b) => a - b);
224
+ const min = sorted[0], max = sorted[sorted.length - 1];
225
+ const pct = Math.round(((value - min) / (max - min || 1)) * 100);
226
+ const color = colorToCSS(cf.dataBar.color) || '#638EC6';
227
+ return `background:linear-gradient(90deg,${color} ${pct}%,transparent ${pct}%)`;
228
+ }
229
+ if (cf.iconSet) {
230
+ const sorted = [...allValues].sort((a, b) => a - b);
231
+ const min = sorted[0], max = sorted[sorted.length - 1];
232
+ const t = (value - min) / (max - min || 1);
233
+ const ICON_MAP = {
234
+ '3Arrows': ['↓', '→', '↑'], '3ArrowsGray': ['⇩', '⇨', '⇧'],
235
+ '3TrafficLights1': ['🔴', '🟡', '🟢'], '3TrafficLights2': ['🔴', '🟡', '🟢'],
236
+ '3Signs': ['⛔', '⚠️', '✅'], '3Symbols': ['✖', '!', '✔'],
237
+ '3Symbols2': ['✖', '!', '✔'], '3Flags': ['🏴', '🏳', '🏁'],
238
+ '3Stars': ['☆', '★', '★'], '4Arrows': ['↓', '↘', '↗', '↑'],
239
+ '4ArrowsGray': ['⇩', '⇘', '⇗', '⇧'], '4Rating': ['◔', '◑', '◕', '●'],
240
+ '4RedToBlack': ['⬤', '⬤', '⬤', '⬤'], '4TrafficLights': ['⬤', '⬤', '⬤', '⬤'],
241
+ '5Arrows': ['↓', '↘', '→', '↗', '↑'], '5ArrowsGray': ['⇩', '⇘', '⇨', '⇗', '⇧'],
242
+ '5Quarters': ['○', '◔', '◑', '◕', '●'], '5Rating': ['◔', '◔', '◑', '◕', '●'],
243
+ };
244
+ const icons = ICON_MAP[cf.iconSet.iconSet ?? '3TrafficLights1'] ?? ['🔴', '🟡', '🟢'];
245
+ const idx = Math.min(Math.floor(t * icons.length), icons.length - 1);
246
+ return `data-icon="${icons[idx]}"`;
247
+ }
248
+ return '';
249
+ }
250
+ function sparklineToSvg(sparkline, values) {
251
+ if (!values.length)
252
+ return '';
253
+ const W = 100, H = 20;
254
+ const min = Math.min(...values), max = Math.max(...values);
255
+ const range = max - min || 1;
256
+ const color = colorToCSS(sparkline.color) || '#4472C4';
257
+ const maxIdx = values.indexOf(Math.max(...values));
258
+ const minIdx = values.indexOf(Math.min(...values));
259
+ if (sparkline.type === 'bar' || sparkline.type === 'stacked') {
260
+ const bw = W / values.length;
261
+ const bars = values.map((v, i) => {
262
+ const barH = sparkline.type === 'stacked'
263
+ ? (v >= 0 ? H / 2 : H / 2)
264
+ : Math.max(1, ((v - min) / range) * H);
265
+ const y = sparkline.type === 'stacked'
266
+ ? (v >= 0 ? H / 2 - barH : H / 2)
267
+ : H - barH;
268
+ let fill = color;
269
+ if (v < 0 && sparkline.negativeColor)
270
+ fill = colorToCSS(sparkline.negativeColor);
271
+ else if (sparkline.showHigh && i === maxIdx && sparkline.highColor)
272
+ fill = colorToCSS(sparkline.highColor);
273
+ else if (sparkline.showLow && i === minIdx && sparkline.lowColor)
274
+ fill = colorToCSS(sparkline.lowColor);
275
+ else if (sparkline.showFirst && i === 0 && sparkline.firstColor)
276
+ fill = colorToCSS(sparkline.firstColor);
277
+ else if (sparkline.showLast && i === values.length - 1 && sparkline.lastColor)
278
+ fill = colorToCSS(sparkline.lastColor);
279
+ return `<rect x="${i * bw + bw * 0.1}" y="${y}" width="${bw * 0.8}" height="${barH}" fill="${fill}" rx="1"/>`;
280
+ }).join('');
281
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" style="display:inline-block;vertical-align:middle">${bars}</svg>`;
282
+ }
283
+ const strokeW = sparkline.lineWidth ?? 1.5;
284
+ const pts = values.map((v, i) => `${(i / (values.length - 1 || 1)) * W},${H - ((v - min) / range) * H}`).join(' ');
285
+ let markers = '';
286
+ if (sparkline.showMarkers) {
287
+ markers = values.map((v, i) => {
288
+ const x = (i / (values.length - 1 || 1)) * W;
289
+ const y = H - ((v - min) / range) * H;
290
+ return `<circle cx="${x}" cy="${y}" r="1.5" fill="${colorToCSS(sparkline.markersColor) || color}"/>`;
291
+ }).join('');
292
+ }
293
+ const specialMarkers = [];
294
+ const addMarker = (idx, clr) => {
295
+ const x = (idx / (values.length - 1 || 1)) * W;
296
+ const y = H - ((values[idx] - min) / range) * H;
297
+ specialMarkers.push(`<circle cx="${x}" cy="${y}" r="2.5" fill="${clr}" stroke="white" stroke-width="0.5"/>`);
298
+ };
299
+ if (sparkline.showHigh && sparkline.highColor)
300
+ addMarker(maxIdx, colorToCSS(sparkline.highColor));
301
+ if (sparkline.showLow && sparkline.lowColor)
302
+ addMarker(minIdx, colorToCSS(sparkline.lowColor));
303
+ if (sparkline.showFirst && sparkline.firstColor)
304
+ addMarker(0, colorToCSS(sparkline.firstColor));
305
+ if (sparkline.showLast && sparkline.lastColor)
306
+ addMarker(values.length - 1, colorToCSS(sparkline.lastColor));
307
+ if (sparkline.showNegative && sparkline.negativeColor) {
308
+ values.forEach((v, i) => {
309
+ if (v < 0) {
310
+ const x = (i / (values.length - 1 || 1)) * W;
311
+ const y = H - ((v - min) / range) * H;
312
+ specialMarkers.push(`<circle cx="${x}" cy="${y}" r="2" fill="${colorToCSS(sparkline.negativeColor)}"/>`);
313
+ }
314
+ });
315
+ }
316
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" style="display:inline-block;vertical-align:middle"><polyline points="${pts}" fill="none" stroke="${color}" stroke-width="${strokeW}"/>${markers}${specialMarkers.join('')}</svg>`;
317
+ }
318
+ const CHART_PALETTE = ['#4472C4', '#ED7D31', '#A5A5A5', '#FFC000', '#5B9BD5', '#70AD47', '#264478', '#9B57A0', '#636363', '#EB7E3A'];
319
+ function resolveChartSeriesData(ws, ref) {
320
+ const vals = [];
321
+ const part = ref.includes('!') ? ref.split('!')[1] : ref;
322
+ const clean = part.replace(/\$/g, '');
323
+ const m = clean.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
324
+ if (!m)
325
+ return vals;
326
+ const c1 = colLetterToIdx(m[1]), r1 = parseInt(m[2], 10);
327
+ const c2 = colLetterToIdx(m[3]), r2 = parseInt(m[4], 10);
328
+ for (let r = r1; r <= r2; r++) {
329
+ for (let c = c1; c <= c2; c++) {
330
+ const cell = ws.getCell(r, c);
331
+ vals.push(typeof cell.value === 'number' ? cell.value : null);
332
+ }
333
+ }
334
+ return vals;
335
+ }
336
+ function resolveChartCategories(ws, ref) {
337
+ const cats = [];
338
+ const part = ref.includes('!') ? ref.split('!')[1] : ref;
339
+ const clean = part.replace(/\$/g, '');
340
+ const m = clean.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
341
+ if (!m)
342
+ return cats;
343
+ const c1 = colLetterToIdx(m[1]), r1 = parseInt(m[2], 10);
344
+ const c2 = colLetterToIdx(m[3]), r2 = parseInt(m[4], 10);
345
+ for (let r = r1; r <= r2; r++) {
346
+ for (let c = c1; c <= c2; c++) {
347
+ const cell = ws.getCell(r, c);
348
+ cats.push(cell.value != null ? String(cell.value) : '');
349
+ }
350
+ }
351
+ return cats;
352
+ }
353
+ function chartToSvg(chart, ws) {
354
+ const W = 520, H = 340;
355
+ const PAD_T = 45, PAD_B = 55, PAD_L = 65, PAD_R = 20;
356
+ const plotW = W - PAD_L - PAD_R, plotH = H - PAD_T - PAD_B;
357
+ const title = chart.title ?? '';
358
+ const type = chart.type;
359
+ const allSeries = [];
360
+ let categories = [];
361
+ for (let si = 0; si < chart.series.length; si++) {
362
+ const s = chart.series[si];
363
+ const rawVals = resolveChartSeriesData(ws, s.values);
364
+ const vals = rawVals.map(v => v ?? 0);
365
+ if (s.categories && !categories.length)
366
+ categories = resolveChartCategories(ws, s.categories);
367
+ allSeries.push({
368
+ name: s.name ?? `Series ${si + 1}`,
369
+ values: vals,
370
+ color: s.color ? colorToCSS(s.color) : CHART_PALETTE[si % CHART_PALETTE.length],
371
+ });
372
+ }
373
+ if (!allSeries.length || !allSeries[0].values.length) {
374
+ return `<div style="display:inline-block;width:${W}px;height:${H}px;border:1px solid #ccc;background:#f9f9f9;text-align:center;line-height:${H}px;color:#666;font-size:14px" data-chart-type="${type}">[Chart: ${escapeXml(title || type)} — no data]</div>`;
375
+ }
376
+ const numCats = Math.max(...allSeries.map(s => s.values.length));
377
+ if (!categories.length)
378
+ categories = Array.from({ length: numCats }, (_, i) => String(i + 1));
379
+ if (type === 'pie' || type === 'doughnut')
380
+ return pieChartSvg(chart, allSeries, categories, W, H);
381
+ if (type === 'radar' || type === 'radarFilled')
382
+ return radarChartSvg(chart, allSeries, categories, W, H);
383
+ if (type === 'scatter' || type === 'scatterSmooth' || type === 'bubble')
384
+ return scatterChartSvg(chart, allSeries, ws, W, H);
385
+ const allVals = allSeries.flatMap(s => s.values);
386
+ let yMin = chart.yAxis?.min ?? Math.min(0, ...allVals);
387
+ let yMax = chart.yAxis?.max ?? Math.max(...allVals);
388
+ if (yMax === yMin)
389
+ yMax = yMin + 1;
390
+ const yRange = yMax - yMin;
391
+ const numTicks = 5;
392
+ const gridLines = [];
393
+ const yLabels = [];
394
+ for (let i = 0; i <= numTicks; i++) {
395
+ const val = yMin + (yRange * i / numTicks);
396
+ const y = PAD_T + plotH - (plotH * (val - yMin) / yRange);
397
+ gridLines.push(`<line x1="${PAD_L}" y1="${y}" x2="${W - PAD_R}" y2="${y}" stroke="#e0e0e0" stroke-width="1"/>`);
398
+ const label = Math.abs(val) >= 1000 ? (val / 1000).toFixed(1) + 'k' : Number.isInteger(val) ? String(val) : val.toFixed(1);
399
+ yLabels.push(`<text x="${PAD_L - 8}" y="${y + 4}" text-anchor="end" font-size="10" fill="#666">${label}</text>`);
400
+ }
401
+ const zeroY = PAD_T + plotH - (plotH * (0 - yMin) / yRange);
402
+ let dataSvg = '';
403
+ const isBar = type === 'bar' || type === 'barStacked' || type === 'barStacked100';
404
+ const isColumn = type === 'column' || type === 'columnStacked' || type === 'columnStacked100';
405
+ const isArea = type === 'area' || type === 'areaStacked';
406
+ const isLine = type === 'line' || type === 'lineStacked' || type === 'lineMarker';
407
+ const isStacked = type.includes('Stacked');
408
+ const is100 = type.includes('100');
409
+ if (isBar) {
410
+ const barGroupH = plotH / numCats;
411
+ const barH = isStacked ? barGroupH * 0.7 : (barGroupH * 0.7) / allSeries.length;
412
+ for (let ci = 0; ci < numCats; ci++) {
413
+ if (isStacked) {
414
+ let xAcc = 0;
415
+ const total = is100 ? allSeries.reduce((s, ser) => s + Math.abs(ser.values[ci] ?? 0), 0) || 1 : 1;
416
+ for (let si = 0; si < allSeries.length; si++) {
417
+ let val = allSeries[si].values[ci] ?? 0;
418
+ if (is100)
419
+ val = (val / total) * (yMax - yMin);
420
+ const barW = (Math.abs(val) / yRange) * plotW;
421
+ const x = PAD_L + (xAcc / yRange) * plotW;
422
+ const y = PAD_T + ci * barGroupH + barGroupH * 0.15;
423
+ dataSvg += `<rect x="${x}" y="${y}" width="${barW}" height="${barH}" fill="${allSeries[si].color}" rx="2"><title>${allSeries[si].name}: ${allSeries[si].values[ci] ?? 0}</title></rect>`;
424
+ xAcc += Math.abs(val);
425
+ }
426
+ }
427
+ else {
428
+ for (let si = 0; si < allSeries.length; si++) {
429
+ const val = allSeries[si].values[ci] ?? 0;
430
+ const barW = (Math.abs(val - yMin) / yRange) * plotW;
431
+ const y = PAD_T + ci * barGroupH + barGroupH * 0.15 + si * barH;
432
+ dataSvg += `<rect x="${PAD_L}" y="${y}" width="${barW}" height="${barH}" fill="${allSeries[si].color}" rx="2"><title>${allSeries[si].name}: ${val}</title></rect>`;
433
+ }
434
+ }
435
+ }
436
+ for (let ci = 0; ci < numCats; ci++) {
437
+ const y = PAD_T + ci * barGroupH + barGroupH / 2 + 4;
438
+ yLabels.push(`<text x="${PAD_L - 8}" y="${y}" text-anchor="end" font-size="10" fill="#666">${escapeXml(categories[ci] ?? '')}</text>`);
439
+ }
440
+ }
441
+ else if (isColumn || (!isLine && !isArea)) {
442
+ const groupW = plotW / numCats;
443
+ const barW = isStacked ? groupW * 0.6 : (groupW * 0.6) / allSeries.length;
444
+ for (let ci = 0; ci < numCats; ci++) {
445
+ if (isStacked) {
446
+ let yAcc = 0;
447
+ const total = is100 ? allSeries.reduce((s, ser) => s + Math.abs(ser.values[ci] ?? 0), 0) || 1 : 1;
448
+ for (let si = 0; si < allSeries.length; si++) {
449
+ let val = allSeries[si].values[ci] ?? 0;
450
+ if (is100)
451
+ val = (val / total) * yRange;
452
+ const bh = (Math.abs(val) / yRange) * plotH;
453
+ const x = PAD_L + ci * groupW + groupW * 0.2;
454
+ const y = zeroY - yAcc - bh;
455
+ dataSvg += `<rect x="${x}" y="${y}" width="${barW}" height="${bh}" fill="${allSeries[si].color}" rx="2"><title>${allSeries[si].name}: ${allSeries[si].values[ci] ?? 0}</title></rect>`;
456
+ yAcc += bh;
457
+ }
458
+ }
459
+ else {
460
+ for (let si = 0; si < allSeries.length; si++) {
461
+ const val = allSeries[si].values[ci] ?? 0;
462
+ const bh = (Math.abs(val - yMin) / yRange) * plotH;
463
+ const x = PAD_L + ci * groupW + groupW * 0.2 + si * barW;
464
+ const y = PAD_T + plotH - bh;
465
+ dataSvg += `<rect x="${x}" y="${y}" width="${barW}" height="${bh}" fill="${allSeries[si].color}" rx="2"><title>${allSeries[si].name}: ${val}</title></rect>`;
466
+ }
467
+ }
468
+ }
469
+ }
470
+ if (isArea) {
471
+ for (let si = 0; si < allSeries.length; si++) {
472
+ const pts = [];
473
+ for (let ci = 0; ci < numCats; ci++) {
474
+ const val = allSeries[si].values[ci] ?? 0;
475
+ const x = PAD_L + (ci / (numCats - 1 || 1)) * plotW;
476
+ const y = PAD_T + plotH - ((val - yMin) / yRange) * plotH;
477
+ pts.push(`${x},${y}`);
478
+ }
479
+ const firstX = PAD_L, lastX = PAD_L + ((numCats - 1) / (numCats - 1 || 1)) * plotW;
480
+ const areaPath = `M${firstX},${zeroY} L${pts.join(' L')} L${lastX},${zeroY} Z`;
481
+ dataSvg += `<path d="${areaPath}" fill="${allSeries[si].color}" fill-opacity="0.3"/>`;
482
+ dataSvg += `<polyline points="${pts.join(' ')}" fill="none" stroke="${allSeries[si].color}" stroke-width="2"/>`;
483
+ }
484
+ }
485
+ if (isLine) {
486
+ for (let si = 0; si < allSeries.length; si++) {
487
+ const pts = [];
488
+ for (let ci = 0; ci < numCats; ci++) {
489
+ const val = allSeries[si].values[ci] ?? 0;
490
+ const x = PAD_L + (ci / (numCats - 1 || 1)) * plotW;
491
+ const y = PAD_T + plotH - ((val - yMin) / yRange) * plotH;
492
+ pts.push(`${x},${y}`);
493
+ }
494
+ dataSvg += `<polyline points="${pts.join(' ')}" fill="none" stroke="${allSeries[si].color}" stroke-width="2.5"/>`;
495
+ if (type === 'lineMarker' || allSeries[si].values.length <= 20) {
496
+ for (let ci = 0; ci < numCats; ci++) {
497
+ const val = allSeries[si].values[ci] ?? 0;
498
+ const x = PAD_L + (ci / (numCats - 1 || 1)) * plotW;
499
+ const y = PAD_T + plotH - ((val - yMin) / yRange) * plotH;
500
+ dataSvg += `<circle cx="${x}" cy="${y}" r="3.5" fill="${allSeries[si].color}" stroke="white" stroke-width="1.5"><title>${allSeries[si].name}: ${val}</title></circle>`;
501
+ }
502
+ }
503
+ }
504
+ }
505
+ let catLabels = '';
506
+ if (!isBar) {
507
+ const step = numCats > 20 ? Math.ceil(numCats / 15) : 1;
508
+ for (let ci = 0; ci < numCats; ci += step) {
509
+ const x = isColumn || (!isLine && !isArea)
510
+ ? PAD_L + ci * (plotW / numCats) + (plotW / numCats) / 2
511
+ : PAD_L + (ci / (numCats - 1 || 1)) * plotW;
512
+ catLabels += `<text x="${x}" y="${PAD_T + plotH + 18}" text-anchor="middle" font-size="10" fill="#666" transform="rotate(-30 ${x} ${PAD_T + plotH + 18})">${escapeXml((categories[ci] ?? '').slice(0, 12))}</text>`;
513
+ }
514
+ }
515
+ let legendSvg = '';
516
+ if (chart.legend !== false && allSeries.length > 1) {
517
+ const ly = H - 12;
518
+ const totalWidth = allSeries.reduce((s, ser) => s + ser.name.length * 7 + 25, 0);
519
+ let lx = (W - totalWidth) / 2;
520
+ for (const ser of allSeries) {
521
+ legendSvg += `<rect x="${lx}" y="${ly - 8}" width="12" height="12" rx="2" fill="${ser.color}"/>`;
522
+ legendSvg += `<text x="${lx + 16}" y="${ly + 2}" font-size="10" fill="#444">${escapeXml(ser.name)}</text>`;
523
+ lx += ser.name.length * 7 + 25;
524
+ }
525
+ }
526
+ let axisTitles = '';
527
+ if (chart.xAxis?.title) {
528
+ axisTitles += `<text x="${W / 2}" y="${H - 2}" text-anchor="middle" font-size="11" fill="#444">${escapeXml(chart.xAxis.title)}</text>`;
529
+ }
530
+ if (chart.yAxis?.title) {
531
+ axisTitles += `<text x="14" y="${PAD_T + plotH / 2}" text-anchor="middle" font-size="11" fill="#444" transform="rotate(-90 14 ${PAD_T + plotH / 2})">${escapeXml(chart.yAxis.title)}</text>`;
532
+ }
533
+ const titleSvg = title ? `<text x="${W / 2}" y="22" text-anchor="middle" font-size="14" font-weight="600" fill="#333">${escapeXml(title)}</text>` : '';
534
+ const plotBorder = `<rect x="${PAD_L}" y="${PAD_T}" width="${plotW}" height="${plotH}" fill="none" stroke="#ccc" stroke-width="0.5"/>`;
535
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" style="background:white;border:1px solid #e0e0e0;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.08);margin:4px">
536
+ ${titleSvg}
537
+ ${gridLines.join('\n')}
538
+ ${plotBorder}
539
+ ${dataSvg}
540
+ ${catLabels}
541
+ ${yLabels.join('\n')}
542
+ ${legendSvg}
543
+ ${axisTitles}
544
+ </svg>`;
545
+ }
546
+ function pieChartSvg(chart, allSeries, categories, W, H) {
547
+ const cx = W / 2, cy = H / 2 + 10;
548
+ const outerR = Math.min(W, H) / 2 - 40;
549
+ const innerR = chart.type === 'doughnut' ? outerR * 0.5 : 0;
550
+ const vals = allSeries[0].values;
551
+ const total = vals.reduce((s, v) => s + Math.abs(v), 0) || 1;
552
+ const title = chart.title ?? '';
553
+ let angle = -Math.PI / 2;
554
+ const slices = [];
555
+ const labels = [];
556
+ for (let i = 0; i < vals.length; i++) {
557
+ const pct = Math.abs(vals[i]) / total;
558
+ const sweep = pct * Math.PI * 2;
559
+ const midAngle = angle + sweep / 2;
560
+ const large = sweep > Math.PI ? 1 : 0;
561
+ const x1o = cx + outerR * Math.cos(angle);
562
+ const y1o = cy + outerR * Math.sin(angle);
563
+ const x2o = cx + outerR * Math.cos(angle + sweep);
564
+ const y2o = cy + outerR * Math.sin(angle + sweep);
565
+ let path;
566
+ if (innerR > 0) {
567
+ const x1i = cx + innerR * Math.cos(angle);
568
+ const y1i = cy + innerR * Math.sin(angle);
569
+ const x2i = cx + innerR * Math.cos(angle + sweep);
570
+ const y2i = cy + innerR * Math.sin(angle + sweep);
571
+ path = `M${x1o},${y1o} A${outerR},${outerR} 0 ${large} 1 ${x2o},${y2o} L${x2i},${y2i} A${innerR},${innerR} 0 ${large} 0 ${x1i},${y1i} Z`;
572
+ }
573
+ else {
574
+ path = vals.length === 1
575
+ ? `M${cx - outerR},${cy} A${outerR},${outerR} 0 1 1 ${cx + outerR},${cy} A${outerR},${outerR} 0 1 1 ${cx - outerR},${cy} Z`
576
+ : `M${cx},${cy} L${x1o},${y1o} A${outerR},${outerR} 0 ${large} 1 ${x2o},${y2o} Z`;
577
+ }
578
+ const color = CHART_PALETTE[i % CHART_PALETTE.length];
579
+ slices.push(`<path d="${path}" fill="${color}" stroke="white" stroke-width="1.5"><title>${escapeXml(categories[i] ?? '')}: ${vals[i]} (${(pct * 100).toFixed(1)}%)</title></path>`);
580
+ if (pct > 0.04) {
581
+ const lr = outerR + 16;
582
+ const lx = cx + lr * Math.cos(midAngle);
583
+ const ly = cy + lr * Math.sin(midAngle);
584
+ const anchor = midAngle > Math.PI / 2 && midAngle < Math.PI * 1.5 ? 'end' : 'start';
585
+ labels.push(`<text x="${lx}" y="${ly + 4}" text-anchor="${anchor}" font-size="10" fill="#444">${escapeXml((categories[i] ?? '').slice(0, 10))} ${(pct * 100).toFixed(0)}%</text>`);
586
+ }
587
+ angle += sweep;
588
+ }
589
+ let legendSvg = '';
590
+ if (chart.legend !== false) {
591
+ const lx = W - 10;
592
+ for (let i = 0; i < Math.min(vals.length, 10); i++) {
593
+ const ly = 40 + i * 18;
594
+ legendSvg += `<rect x="${lx - 80}" y="${ly - 8}" width="10" height="10" rx="2" fill="${CHART_PALETTE[i % CHART_PALETTE.length]}"/>`;
595
+ legendSvg += `<text x="${lx - 65}" y="${ly + 2}" font-size="10" fill="#444">${escapeXml((categories[i] ?? '').slice(0, 10))}</text>`;
596
+ }
597
+ }
598
+ const titleSvg = title ? `<text x="${W / 2}" y="22" text-anchor="middle" font-size="14" font-weight="600" fill="#333">${escapeXml(title)}</text>` : '';
599
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" style="background:white;border:1px solid #e0e0e0;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.08);margin:4px">
600
+ ${titleSvg}
601
+ ${slices.join('\n')}
602
+ ${labels.join('\n')}
603
+ ${legendSvg}
604
+ </svg>`;
605
+ }
606
+ function radarChartSvg(chart, allSeries, categories, W, H) {
607
+ const cx = W / 2, cy = H / 2 + 10;
608
+ const R = Math.min(W, H) / 2 - 50;
609
+ const n = categories.length || 1;
610
+ const isFilled = chart.type === 'radarFilled';
611
+ const allVals = allSeries.flatMap(s => s.values);
612
+ const maxVal = Math.max(...allVals, 1);
613
+ const rings = [];
614
+ for (let ring = 1; ring <= 4; ring++) {
615
+ const r = R * ring / 4;
616
+ const pts = Array.from({ length: n }, (_, i) => {
617
+ const a = -Math.PI / 2 + (2 * Math.PI * i / n);
618
+ return `${cx + r * Math.cos(a)},${cy + r * Math.sin(a)}`;
619
+ });
620
+ rings.push(`<polygon points="${pts.join(' ')}" fill="none" stroke="#e0e0e0" stroke-width="0.5"/>`);
621
+ }
622
+ const axes = [];
623
+ for (let i = 0; i < n; i++) {
624
+ const a = -Math.PI / 2 + (2 * Math.PI * i / n);
625
+ const x = cx + R * Math.cos(a);
626
+ const y = cy + R * Math.sin(a);
627
+ axes.push(`<line x1="${cx}" y1="${cy}" x2="${x}" y2="${y}" stroke="#ccc" stroke-width="0.5"/>`);
628
+ const lx = cx + (R + 14) * Math.cos(a);
629
+ const ly = cy + (R + 14) * Math.sin(a);
630
+ axes.push(`<text x="${lx}" y="${ly + 4}" text-anchor="middle" font-size="9" fill="#666">${escapeXml((categories[i] ?? '').slice(0, 8))}</text>`);
631
+ }
632
+ const seriesSvg = [];
633
+ for (const ser of allSeries) {
634
+ const pts = ser.values.map((v, i) => {
635
+ const a = -Math.PI / 2 + (2 * Math.PI * i / n);
636
+ const r = (v / maxVal) * R;
637
+ return `${cx + r * Math.cos(a)},${cy + r * Math.sin(a)}`;
638
+ });
639
+ if (isFilled) {
640
+ seriesSvg.push(`<polygon points="${pts.join(' ')}" fill="${ser.color}" fill-opacity="0.2" stroke="${ser.color}" stroke-width="2"/>`);
641
+ }
642
+ else {
643
+ seriesSvg.push(`<polygon points="${pts.join(' ')}" fill="none" stroke="${ser.color}" stroke-width="2"/>`);
644
+ }
645
+ ser.values.forEach((v, i) => {
646
+ const a = -Math.PI / 2 + (2 * Math.PI * i / n);
647
+ const r = (v / maxVal) * R;
648
+ seriesSvg.push(`<circle cx="${cx + r * Math.cos(a)}" cy="${cy + r * Math.sin(a)}" r="3" fill="${ser.color}" stroke="white" stroke-width="1"><title>${ser.name}: ${v}</title></circle>`);
649
+ });
650
+ }
651
+ const titleSvg = chart.title ? `<text x="${W / 2}" y="20" text-anchor="middle" font-size="14" font-weight="600" fill="#333">${escapeXml(chart.title)}</text>` : '';
652
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" style="background:white;border:1px solid #e0e0e0;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.08);margin:4px">
653
+ ${titleSvg}
654
+ ${rings.join('\n')}
655
+ ${axes.join('\n')}
656
+ ${seriesSvg.join('\n')}
657
+ </svg>`;
658
+ }
659
+ function scatterChartSvg(chart, allSeries, ws, W, H) {
660
+ const PAD_T = 45, PAD_B = 40, PAD_L = 60, PAD_R = 20;
661
+ const plotW = W - PAD_L - PAD_R, plotH = H - PAD_T - PAD_B;
662
+ const catSeries = chart.series[0]?.categories ? resolveChartSeriesData(ws, chart.series[0].categories) : [];
663
+ const points = [];
664
+ for (const ser of allSeries) {
665
+ for (let i = 0; i < ser.values.length; i++) {
666
+ points.push({ x: catSeries[i] ?? i, y: ser.values[i], name: ser.name, color: ser.color });
667
+ }
668
+ }
669
+ if (!points.length)
670
+ return '';
671
+ const xMin = Math.min(...points.map(p => p.x)), xMax = Math.max(...points.map(p => p.x));
672
+ const yMin = Math.min(0, ...points.map(p => p.y)), yMax = Math.max(...points.map(p => p.y));
673
+ const xRange = xMax - xMin || 1, yRange = yMax - yMin || 1;
674
+ const gridLines = [];
675
+ for (let i = 0; i <= 4; i++) {
676
+ const y = PAD_T + plotH - (plotH * i / 4);
677
+ gridLines.push(`<line x1="${PAD_L}" y1="${y}" x2="${W - PAD_R}" y2="${y}" stroke="#e0e0e0" stroke-width="0.5"/>`);
678
+ const val = yMin + yRange * i / 4;
679
+ gridLines.push(`<text x="${PAD_L - 8}" y="${y + 4}" text-anchor="end" font-size="10" fill="#666">${val.toFixed(0)}</text>`);
680
+ }
681
+ const dotsSvg = points.map(p => {
682
+ const x = PAD_L + ((p.x - xMin) / xRange) * plotW;
683
+ const y = PAD_T + plotH - ((p.y - yMin) / yRange) * plotH;
684
+ const r = chart.type === 'bubble' ? Math.max(4, Math.min(20, Math.sqrt(Math.abs(p.y)) * 2)) : 4;
685
+ return `<circle cx="${x}" cy="${y}" r="${r}" fill="${p.color}" fill-opacity="${chart.type === 'bubble' ? '0.6' : '1'}" stroke="white" stroke-width="1"><title>${p.name}: (${p.x}, ${p.y})</title></circle>`;
686
+ }).join('\n');
687
+ let lineSvg = '';
688
+ if (chart.type === 'scatterSmooth') {
689
+ for (const ser of allSeries) {
690
+ const pts = ser.values.map((v, i) => {
691
+ const xVal = catSeries[i] ?? i;
692
+ const x = PAD_L + ((xVal - xMin) / xRange) * plotW;
693
+ const y = PAD_T + plotH - ((v - yMin) / yRange) * plotH;
694
+ return `${x},${y}`;
695
+ });
696
+ lineSvg += `<polyline points="${pts.join(' ')}" fill="none" stroke="${ser.color}" stroke-width="2"/>`;
697
+ }
698
+ }
699
+ const titleSvg = chart.title ? `<text x="${W / 2}" y="22" text-anchor="middle" font-size="14" font-weight="600" fill="#333">${escapeXml(chart.title)}</text>` : '';
700
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" style="background:white;border:1px solid #e0e0e0;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.08);margin:4px">
701
+ ${titleSvg}
702
+ ${gridLines.join('\n')}
703
+ <rect x="${PAD_L}" y="${PAD_T}" width="${plotW}" height="${plotH}" fill="none" stroke="#ccc" stroke-width="0.5"/>
704
+ ${lineSvg}
705
+ ${dotsSvg}
706
+ </svg>`;
707
+ }
708
+ function chartToHtml(chart, ws) {
709
+ return `<div class="xl-chart" data-from-col="${chart.from.col}" data-from-row="${chart.from.row}" data-to-col="${chart.to.col}" data-to-row="${chart.to.row}" style="position:absolute;z-index:4">${chartToSvg(chart, ws)}</div>`;
710
+ }
711
+ function shapeSvgPath(type, w, h) {
712
+ switch (type) {
713
+ case 'rect': return `<rect x="0" y="0" width="${w}" height="${h}"/>`;
714
+ case 'roundRect': return `<rect x="0" y="0" width="${w}" height="${h}" rx="${Math.min(w, h) * 0.15}"/>`;
715
+ case 'ellipse': return `<ellipse cx="${w / 2}" cy="${h / 2}" rx="${w / 2}" ry="${h / 2}"/>`;
716
+ case 'triangle': return `<polygon points="${w / 2},0 ${w},${h} 0,${h}"/>`;
717
+ case 'diamond': return `<polygon points="${w / 2},0 ${w},${h / 2} ${w / 2},${h} 0,${h / 2}"/>`;
718
+ case 'pentagon': {
719
+ const pts = Array.from({ length: 5 }, (_, i) => {
720
+ const a = -Math.PI / 2 + (2 * Math.PI * i / 5);
721
+ return `${w / 2 + w / 2 * Math.cos(a)},${h / 2 + h / 2 * Math.sin(a)}`;
722
+ });
723
+ return `<polygon points="${pts.join(' ')}"/>`;
724
+ }
725
+ case 'hexagon': {
726
+ const pts = Array.from({ length: 6 }, (_, i) => {
727
+ const a = -Math.PI / 6 + (2 * Math.PI * i / 6);
728
+ return `${w / 2 + w / 2 * Math.cos(a)},${h / 2 + h / 2 * Math.sin(a)}`;
729
+ });
730
+ return `<polygon points="${pts.join(' ')}"/>`;
731
+ }
732
+ case 'octagon': {
733
+ const pts = Array.from({ length: 8 }, (_, i) => {
734
+ const a = -Math.PI / 8 + (2 * Math.PI * i / 8);
735
+ return `${w / 2 + w / 2 * Math.cos(a)},${h / 2 + h / 2 * Math.sin(a)}`;
736
+ });
737
+ return `<polygon points="${pts.join(' ')}"/>`;
738
+ }
739
+ case 'star5':
740
+ case 'star6': {
741
+ const n = type === 'star5' ? 5 : 6;
742
+ const pts = [];
743
+ for (let i = 0; i < n * 2; i++) {
744
+ const a = -Math.PI / 2 + (Math.PI * i / n);
745
+ const r = i % 2 === 0 ? Math.min(w, h) / 2 : Math.min(w, h) / 4.5;
746
+ pts.push(`${w / 2 + r * Math.cos(a)},${h / 2 + r * Math.sin(a)}`);
747
+ }
748
+ return `<polygon points="${pts.join(' ')}"/>`;
749
+ }
750
+ case 'rightArrow':
751
+ return `<polygon points="0,${h * 0.25} ${w * 0.65},${h * 0.25} ${w * 0.65},0 ${w},${h / 2} ${w * 0.65},${h} ${w * 0.65},${h * 0.75} 0,${h * 0.75}"/>`;
752
+ case 'leftArrow':
753
+ return `<polygon points="${w},${h * 0.25} ${w * 0.35},${h * 0.25} ${w * 0.35},0 0,${h / 2} ${w * 0.35},${h} ${w * 0.35},${h * 0.75} ${w},${h * 0.75}"/>`;
754
+ case 'upArrow':
755
+ return `<polygon points="${w * 0.25},${h} ${w * 0.25},${h * 0.35} 0,${h * 0.35} ${w / 2},0 ${w},${h * 0.35} ${w * 0.75},${h * 0.35} ${w * 0.75},${h}"/>`;
756
+ case 'downArrow':
757
+ return `<polygon points="${w * 0.25},0 ${w * 0.75},0 ${w * 0.75},${h * 0.65} ${w},${h * 0.65} ${w / 2},${h} 0,${h * 0.65} ${w * 0.25},${h * 0.65}"/>`;
758
+ case 'heart': {
759
+ const hw = w / 2, hh = h;
760
+ return `<path d="M${hw},${hh * 0.35} C${hw},${hh * 0.15} ${hw * 0.5},0 ${hw * 0.25},0 C0,0 0,${hh * 0.35} 0,${hh * 0.35} C0,${hh * 0.6} ${hw * 0.5},${hh * 0.8} ${hw},${hh} C${hw * 1.5},${hh * 0.8} ${w},${hh * 0.6} ${w},${hh * 0.35} C${w},${hh * 0.35} ${w},0 ${hw * 1.75},0 C${hw * 1.5},0 ${hw},${hh * 0.15} ${hw},${hh * 0.35} Z"/>`;
761
+ }
762
+ case 'lightningBolt':
763
+ return `<polygon points="${w * 0.55},0 ${w * 0.2},${h * 0.45} ${w * 0.45},${h * 0.45} ${w * 0.15},${h} ${w * 0.8},${h * 0.4} ${w * 0.55},${h * 0.4}"/>`;
764
+ case 'sun': return `<circle cx="${w / 2}" cy="${h / 2}" r="${Math.min(w, h) * 0.3}"/>`;
765
+ case 'moon':
766
+ return `<path d="M${w * 0.6},${h * 0.1} A${w * 0.4},${h * 0.4} 0 1 0 ${w * 0.6},${h * 0.9} A${w * 0.3},${h * 0.35} 0 1 1 ${w * 0.6},${h * 0.1} Z"/>`;
767
+ case 'smileyFace':
768
+ return `<circle cx="${w / 2}" cy="${h / 2}" r="${Math.min(w, h) * 0.45}"/>`
769
+ + `<circle cx="${w * 0.35}" cy="${h * 0.38}" r="${Math.min(w, h) * 0.04}" fill="white"/>`
770
+ + `<circle cx="${w * 0.65}" cy="${h * 0.38}" r="${Math.min(w, h) * 0.04}" fill="white"/>`
771
+ + `<path d="M${w * 0.3},${h * 0.58} Q${w / 2},${h * 0.78} ${w * 0.7},${h * 0.58}" fill="none" stroke="white" stroke-width="2"/>`;
772
+ case 'cloud':
773
+ return `<ellipse cx="${w * 0.35}" cy="${h * 0.55}" rx="${w * 0.25}" ry="${h * 0.25}"/>`
774
+ + `<ellipse cx="${w * 0.55}" cy="${h * 0.35}" rx="${w * 0.22}" ry="${h * 0.22}"/>`
775
+ + `<ellipse cx="${w * 0.7}" cy="${h * 0.5}" rx="${w * 0.2}" ry="${h * 0.2}"/>`
776
+ + `<rect x="${w * 0.15}" y="${h * 0.5}" width="${w * 0.7}" height="${h * 0.25}" rx="4"/>`;
777
+ case 'callout1':
778
+ return `<path d="M0,0 L${w},0 L${w},${h * 0.7} L${w * 0.4},${h * 0.7} L${w * 0.25},${h} L${w * 0.3},${h * 0.7} L0,${h * 0.7} Z"/>`;
779
+ case 'callout2':
780
+ return `<path d="M${w * 0.1},0 L${w * 0.9},0 Q${w},0 ${w},${h * 0.1} L${w},${h * 0.6} Q${w},${h * 0.7} ${w * 0.9},${h * 0.7} L${w * 0.4},${h * 0.7} L${w * 0.25},${h} L${w * 0.3},${h * 0.7} L${w * 0.1},${h * 0.7} Q0,${h * 0.7} 0,${h * 0.6} L0,${h * 0.1} Q0,0 ${w * 0.1},0 Z"/>`;
781
+ case 'flowChartProcess':
782
+ return `<rect x="0" y="0" width="${w}" height="${h}" rx="2"/>`;
783
+ case 'flowChartDecision':
784
+ return `<polygon points="${w / 2},0 ${w},${h / 2} ${w / 2},${h} 0,${h / 2}"/>`;
785
+ case 'flowChartTerminator':
786
+ return `<rect x="0" y="0" width="${w}" height="${h}" rx="${h / 2}"/>`;
787
+ case 'flowChartDocument':
788
+ return `<path d="M0,0 L${w},0 L${w},${h * 0.8} Q${w * 0.75},${h * 0.65} ${w * 0.5},${h * 0.8} Q${w * 0.25},${h * 0.95} 0,${h * 0.8} Z"/>`;
789
+ case 'line':
790
+ return `<line x1="0" y1="${h / 2}" x2="${w}" y2="${h / 2}" stroke-width="2"/>`;
791
+ case 'curvedConnector3':
792
+ return `<path d="M0,${h / 2} C${w * 0.3},${h * 0.1} ${w * 0.7},${h * 0.9} ${w},${h / 2}" fill="none" stroke-width="2"/>`;
793
+ default:
794
+ return `<rect x="0" y="0" width="${w}" height="${h}" rx="4"/>`;
795
+ }
796
+ }
797
+ function shapeToHtml(shape) {
798
+ const toHex = (c) => { let h = c.replace(/^#/, ''); if (h.length === 8)
799
+ h = h.substring(2); return '#' + h; };
800
+ const bg = shape.fillColor ? toHex(shape.fillColor) : '#4472C4';
801
+ const border = shape.lineColor ? toHex(shape.lineColor) : '#2F5496';
802
+ const lw = shape.lineWidth ?? 2;
803
+ const w = 160, h = 80;
804
+ const rotation = shape.rotation ? ` transform="rotate(${shape.rotation} ${w / 2} ${h / 2})"` : '';
805
+ const textEl = shape.text ? `<text x="${w / 2}" y="${h / 2 + 5}" text-anchor="middle" fill="white" font-size="13" font-weight="600"${shape.font?.name ? ` font-family="'${escapeXml(shape.font.name)}'"` : ''}>${escapeXml(shape.text)}</text>` : '';
806
+ const isLine = shape.type === 'line' || shape.type === 'curvedConnector3';
807
+ const fillAttr = isLine ? `fill="none" stroke="${bg}"` : `fill="${bg}" stroke="${border}" stroke-width="${lw}"`;
808
+ return `<div class="xl-shape" data-from-col="${shape.from.col}" data-from-row="${shape.from.row}" data-to-col="${shape.to.col}" data-to-row="${shape.to.row}" style="position:absolute;z-index:2">
809
+ <svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"${rotation}>
810
+ <g ${fillAttr}>${shapeSvgPath(shape.type, w, h)}</g>
811
+ ${textEl}
812
+ </svg></div>`;
813
+ }
814
+ function wordArtToHtml(wa) {
815
+ const toHex = (c) => { let h = c.replace(/^#/, ''); if (h.length === 8)
816
+ h = h.substring(2); return '#' + h; };
817
+ const color = wa.fillColor ? toHex(wa.fillColor) : '#333';
818
+ const outline = wa.outlineColor ? toHex(wa.outlineColor) : '';
819
+ const family = wa.font?.name ?? 'Impact';
820
+ const size = wa.font?.size ?? 36;
821
+ const bold = wa.font?.bold !== false ? 'font-weight:bold;' : '';
822
+ const italic = wa.font?.italic ? 'font-style:italic;' : '';
823
+ const textStroke = outline ? `-webkit-text-stroke:1px ${outline};paint-order:stroke fill;` : '';
824
+ const presetStyle = wordArtPresetCSS(wa.preset);
825
+ return `<div class="xl-wordart" data-from-col="${wa.from.col}" data-from-row="${wa.from.row}" data-to-col="${wa.to.col}" data-to-row="${wa.to.row}" style="position:absolute;z-index:2;font-family:'${escapeXml(family)}',sans-serif;font-size:${size}px;${bold}${italic}color:${color};${textStroke}text-shadow:2px 2px 4px rgba(0,0,0,.3);${presetStyle}padding:8px 16px;white-space:nowrap;line-height:1.2">${escapeXml(wa.text)}</div>`;
826
+ }
827
+ function wordArtPresetCSS(preset) {
828
+ if (!preset || preset === 'textPlain')
829
+ return '';
830
+ const presets = {
831
+ textArchUp: 'letter-spacing:4px;',
832
+ textArchDown: 'letter-spacing:4px;transform:scaleY(-1);',
833
+ textCircle: 'letter-spacing:6px;',
834
+ textWave1: 'letter-spacing:2px;font-style:italic;transform:skewX(-5deg);',
835
+ textWave2: 'letter-spacing:2px;font-style:italic;transform:skewX(5deg);',
836
+ textInflate: 'letter-spacing:3px;transform:scaleY(1.3);',
837
+ textDeflate: 'letter-spacing:1px;transform:scaleY(0.7);',
838
+ textSlantUp: 'transform:perspective(300px) rotateY(-8deg) rotateX(3deg);',
839
+ textSlantDown: 'transform:perspective(300px) rotateY(8deg) rotateX(-3deg);',
840
+ textFadeUp: 'transform:perspective(200px) rotateX(-8deg);',
841
+ textFadeDown: 'transform:perspective(200px) rotateX(8deg);',
842
+ textFadeLeft: 'transform:perspective(200px) rotateY(8deg);',
843
+ textFadeRight: 'transform:perspective(200px) rotateY(-8deg);',
844
+ textCascadeUp: 'letter-spacing:3px;transform:rotate(-5deg) scaleX(1.1);',
845
+ textCascadeDown: 'letter-spacing:3px;transform:rotate(5deg) scaleX(1.1);',
846
+ textChevron: 'letter-spacing:5px;transform:scaleX(1.15);',
847
+ textRingInside: 'letter-spacing:8px;',
848
+ textRingOutside: 'letter-spacing:6px;',
849
+ textStop: 'letter-spacing:2px;transform:scaleY(0.85) scaleX(1.1);',
850
+ };
851
+ return presets[preset] ?? '';
852
+ }
853
+ function toBase64(bytes) {
854
+ let b64 = '';
855
+ for (let i = 0; i < bytes.length; i += 3) {
856
+ const b0 = bytes[i], b1 = bytes[i + 1] ?? 0, b2 = bytes[i + 2] ?? 0;
857
+ const n = (b0 << 16) | (b1 << 8) | b2;
858
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
859
+ b64 += chars[(n >> 18) & 63] + chars[(n >> 12) & 63];
860
+ b64 += i + 1 < bytes.length ? chars[(n >> 6) & 63] : '=';
861
+ b64 += i + 2 < bytes.length ? chars[n & 63] : '=';
862
+ }
863
+ return b64;
864
+ }
865
+ function imageDataUri(data, format) {
866
+ const mime = formatToMime(format);
867
+ const b64 = typeof data === 'string' ? data : toBase64(data);
868
+ return `data:${mime};base64,${b64}`;
869
+ }
870
+ function imageToPositionedHtml(img) {
871
+ const src = imageDataUri(img.data, img.format);
872
+ const alt = img.altText ? ` alt="${escapeXml(img.altText)}"` : '';
873
+ const w = img.width ? `width:${img.width}px;` : 'max-width:400px;';
874
+ const h = img.height ? `height:${img.height}px;` : 'max-height:300px;';
875
+ const fromCol = img.from?.col ?? 0;
876
+ const fromRow = img.from?.row ?? 0;
877
+ return `<img src="${src}"${alt} class="xl-img" data-from-col="${fromCol}" data-from-row="${fromRow}" style="${w}${h}border:1px solid #ddd;border-radius:4px"/>`;
878
+ }
879
+ function cellImageToHtml(ci) {
880
+ const src = imageDataUri(ci.data, ci.format);
881
+ const alt = ci.altText ? ` alt="${escapeXml(ci.altText)}"` : '';
882
+ return `<img src="${src}"${alt} style="max-width:100%;max-height:100%;object-fit:contain"/>`;
883
+ }
884
+ function formatToMime(fmt) {
885
+ switch (fmt) {
886
+ case 'jpeg':
887
+ case 'jpg': return 'image/jpeg';
888
+ case 'gif': return 'image/gif';
889
+ case 'svg': return 'image/svg+xml';
890
+ case 'webp': return 'image/webp';
891
+ case 'bmp': return 'image/bmp';
892
+ default: return 'image/png';
893
+ }
894
+ }
895
+ function mathElementToMathML(el) {
896
+ switch (el.type) {
897
+ case 'text': {
898
+ const text = escapeXml(el.text ?? '');
899
+ if (el.font === 'normal' || /^[a-zA-Z]{2,}$/.test(el.text ?? ''))
900
+ return `<mi mathvariant="normal">${text}</mi>`;
901
+ if (/^[0-9.]+$/.test(el.text ?? ''))
902
+ return `<mn>${text}</mn>`;
903
+ if (el.text && el.text.length === 1 && /[+\-*/=<>±×÷≤≥≠∞∈∉∪∩⊂⊃∧∨¬→←↔∀∃∑∏∫]/.test(el.text))
904
+ return `<mo>${text}</mo>`;
905
+ return `<mi>${text}</mi>`;
906
+ }
907
+ case 'frac':
908
+ return `<mfrac><mrow>${(el.base ?? []).map(mathElementToMathML).join('')}</mrow><mrow>${(el.argument ?? []).map(mathElementToMathML).join('')}</mrow></mfrac>`;
909
+ case 'sup':
910
+ return `<msup><mrow>${(el.base ?? []).map(mathElementToMathML).join('')}</mrow><mrow>${(el.argument ?? []).map(mathElementToMathML).join('')}</mrow></msup>`;
911
+ case 'sub':
912
+ return `<msub><mrow>${(el.base ?? []).map(mathElementToMathML).join('')}</mrow><mrow>${(el.argument ?? []).map(mathElementToMathML).join('')}</mrow></msub>`;
913
+ case 'subSup':
914
+ return `<msubsup><mrow>${(el.base ?? []).map(mathElementToMathML).join('')}</mrow><mrow>${(el.subscript ?? []).map(mathElementToMathML).join('')}</mrow><mrow>${(el.superscript ?? []).map(mathElementToMathML).join('')}</mrow></msubsup>`;
915
+ case 'nary':
916
+ return `<munderover><mo>${escapeXml(el.operator ?? '∑')}</mo><mrow>${(el.lower ?? []).map(mathElementToMathML).join('')}</mrow><mrow>${(el.upper ?? []).map(mathElementToMathML).join('')}</mrow></munderover><mrow>${(el.body ?? []).map(mathElementToMathML).join('')}</mrow>`;
917
+ case 'rad':
918
+ if (!el.hideDegree && el.degree?.length)
919
+ return `<mroot><mrow>${(el.body ?? []).map(mathElementToMathML).join('')}</mrow><mrow>${el.degree.map(mathElementToMathML).join('')}</mrow></mroot>`;
920
+ return `<msqrt><mrow>${(el.body ?? []).map(mathElementToMathML).join('')}</mrow></msqrt>`;
921
+ case 'delim':
922
+ return `<mrow><mo>${escapeXml(el.open ?? '(')}</mo>${(el.body ?? []).map(mathElementToMathML).join('')}<mo>${escapeXml(el.close ?? ')')}</mo></mrow>`;
923
+ case 'func':
924
+ return `<mrow><mi mathvariant="normal">${(el.base ?? []).map(e => escapeXml(e.text ?? '')).join('')}</mi><mo>&#x2061;</mo>${(el.argument ?? []).map(mathElementToMathML).join('')}</mrow>`;
925
+ case 'groupChar':
926
+ return `<munder><mrow>${(el.base ?? []).map(mathElementToMathML).join('')}</mrow><mo>${escapeXml(el.operator ?? '⏟')}</mo></munder>`;
927
+ case 'accent':
928
+ return `<mover accent="true"><mrow>${(el.base ?? []).map(mathElementToMathML).join('')}</mrow><mo>${escapeXml(el.operator ?? '̂')}</mo></mover>`;
929
+ case 'bar':
930
+ return `<mover><mrow>${(el.base ?? []).map(mathElementToMathML).join('')}</mrow><mo>¯</mo></mover>`;
931
+ case 'limLow':
932
+ return `<munder><mrow>${(el.base ?? []).map(mathElementToMathML).join('')}</mrow><mrow>${(el.argument ?? []).map(mathElementToMathML).join('')}</mrow></munder>`;
933
+ case 'limUpp':
934
+ return `<mover><mrow>${(el.base ?? []).map(mathElementToMathML).join('')}</mrow><mrow>${(el.argument ?? []).map(mathElementToMathML).join('')}</mrow></mover>`;
935
+ case 'eqArr':
936
+ return `<mtable>${(el.rows ?? []).map(row => `<mtr><mtd>${row.map(mathElementToMathML).join('')}</mtd></mtr>`).join('')}</mtable>`;
937
+ case 'matrix':
938
+ return `<mrow><mo>(</mo><mtable>${(el.rows ?? []).map(row => `<mtr>${row.map(c => `<mtd>${mathElementToMathML(c)}</mtd>`).join('')}</mtr>`).join('')}</mtable><mo>)</mo></mrow>`;
939
+ default:
940
+ return el.text ? `<mi>${escapeXml(el.text)}</mi>` : '<mrow></mrow>';
941
+ }
942
+ }
943
+ function mathEquationToMathML(eq) {
944
+ const size = eq.fontSize ?? 11;
945
+ const font = eq.fontName ?? 'Cambria Math';
946
+ return `<div class="xl-math" data-from-col="${eq.from.col}" data-from-row="${eq.from.row}" style="position:absolute;z-index:2;font-family:'${escapeXml(font)}',serif;font-size:${size}pt;padding:4px;background:white"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mrow>${eq.elements.map(mathElementToMathML).join('')}</mrow></math></div>`;
947
+ }
948
+ function formControlToPositionedHtml(fc) {
949
+ const fromCol = fc.from.col;
950
+ const fromRow = fc.from.row;
951
+ const toCol = fc.to?.col;
952
+ const toRow = fc.to?.row;
953
+ const toAttrs = toCol != null && toRow != null ? ` data-to-col="${toCol}" data-to-row="${toRow}"` : '';
954
+ const linked = fc.linkedCell ? ` data-linked-cell="${escapeXml(fc.linkedCell)}"` : '';
955
+ const inputRange = fc.inputRange ? ` data-input-range="${escapeXml(fc.inputRange)}"` : '';
956
+ const macro = fc.macro ? ` data-macro="${escapeXml(fc.macro)}"` : '';
957
+ let inner = '';
958
+ switch (fc.type) {
959
+ case 'button':
960
+ case 'dialog':
961
+ inner = `<button style="width:100%;height:100%;padding:4px 12px;font-size:13px;border:1px outset #ccc;background:linear-gradient(180deg,#f8f8f8,#e0e0e0);cursor:pointer;border-radius:3px;white-space:nowrap"${macro}>${escapeXml(fc.text ?? 'Button')}</button>`;
962
+ break;
963
+ case 'checkBox': {
964
+ const checked = fc.checked === 'checked' ? ' checked' : '';
965
+ const indeterminate = fc.checked === 'mixed' ? ' data-indeterminate="true"' : '';
966
+ inner = `<label style="font-size:13px;display:inline-flex;align-items:center;gap:4px;width:100%;height:100%;padding:2px 4px;cursor:pointer"><input type="checkbox"${checked}${indeterminate}${linked}/> ${escapeXml(fc.text ?? 'Checkbox')}</label>`;
967
+ break;
968
+ }
969
+ case 'optionButton': {
970
+ const checked = fc.checked === 'checked' ? ' checked' : '';
971
+ inner = `<label style="font-size:13px;display:inline-flex;align-items:center;gap:4px;width:100%;height:100%;padding:2px 4px;cursor:pointer"><input type="radio" name="group"${checked}${linked}/> ${escapeXml(fc.text ?? 'Option')}</label>`;
972
+ break;
973
+ }
974
+ case 'comboBox': {
975
+ const lines = fc.dropLines ?? 8;
976
+ inner = `<select style="width:100%;height:100%;padding:2px 4px;font-size:13px;border:1px solid #aaa;background:white"${linked}${inputRange} size="1" data-drop-lines="${lines}"><option>${escapeXml(fc.text ?? 'Select...')}</option></select>`;
977
+ break;
978
+ }
979
+ case 'listBox': {
980
+ const size = fc.dropLines ?? 5;
981
+ const sel = fc.selType ?? 'single';
982
+ const multi = sel === 'multi' || sel === 'extend' ? ' multiple' : '';
983
+ inner = `<select style="width:100%;height:100%;padding:2px;font-size:13px;border:1px solid #aaa;background:white"${linked}${inputRange} size="${size}"${multi}><option>${escapeXml(fc.text ?? 'Item')}</option></select>`;
984
+ break;
985
+ }
986
+ case 'spinner': {
987
+ const min = fc.min ?? 0;
988
+ const max = fc.max ?? 100;
989
+ const step = fc.inc ?? 1;
990
+ const val = fc.val ?? min;
991
+ inner = `<input type="number" value="${val}" min="${min}" max="${max}" step="${step}" style="width:100%;height:100%;padding:2px 4px;font-size:13px;border:1px solid #aaa"${linked}/>`;
992
+ break;
993
+ }
994
+ case 'scrollBar': {
995
+ const min = fc.min ?? 0;
996
+ const max = fc.max ?? 100;
997
+ const step = fc.inc ?? 1;
998
+ const val = fc.val ?? min;
999
+ inner = `<input type="range" value="${val}" min="${min}" max="${max}" step="${step}" style="width:100%;height:100%"${linked}/>`;
1000
+ break;
1001
+ }
1002
+ case 'label':
1003
+ inner = `<span style="font-size:13px;display:flex;align-items:center;width:100%;height:100%;padding:2px 4px">${escapeXml(fc.text ?? 'Label')}</span>`;
1004
+ break;
1005
+ case 'groupBox':
1006
+ inner = `<fieldset style="width:100%;height:100%;padding:8px;border:1px solid #999;font-size:13px;margin:0;box-sizing:border-box"><legend>${escapeXml(fc.text ?? 'Group')}</legend></fieldset>`;
1007
+ break;
1008
+ default:
1009
+ inner = `<span style="font-size:13px">[${escapeXml(fc.type)}]</span>`;
1010
+ }
1011
+ return `<div class="xl-fc" data-from-col="${fromCol}" data-from-row="${fromRow}"${toAttrs} style="position:absolute;overflow:hidden">${inner}</div>`;
1012
+ }
1013
+ function colLetterToIdx(letter) {
1014
+ let idx = 0;
1015
+ for (let i = 0; i < letter.length; i++) {
1016
+ idx = idx * 26 + (letter.charCodeAt(i) - 64);
1017
+ }
1018
+ return idx;
1019
+ }
1020
+ function resolveSparklineData(ws, dataRange) {
1021
+ const vals = [];
1022
+ const ref = dataRange.includes('!') ? dataRange.split('!')[1] : dataRange;
1023
+ const m = ref.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
1024
+ if (m) {
1025
+ const c1 = colLetterToIdx(m[1]), r1 = parseInt(m[2], 10);
1026
+ const c2 = colLetterToIdx(m[3]), r2 = parseInt(m[4], 10);
1027
+ for (let r = r1; r <= r2; r++) {
1028
+ for (let c = c1; c <= c2; c++) {
1029
+ const cell = ws.getCell(r, c);
1030
+ if (typeof cell.value === 'number')
1031
+ vals.push(cell.value);
1032
+ }
1033
+ }
1034
+ }
1035
+ return vals;
1036
+ }
1037
+ export function worksheetToHtml(ws, options = {}) {
1038
+ if (options.evaluateFormulas) {
1039
+ new FormulaEngine().calculateSheet(ws);
1040
+ }
1041
+ const range = ws.getUsedRange();
1042
+ if (!range) {
1043
+ return options.fullDocument !== false
1044
+ ? `<!DOCTYPE html><html><head><title>${escapeXml(options.title ?? '')}</title></head><body><p>Empty worksheet</p></body></html>`
1045
+ : '<table></table>';
1046
+ }
1047
+ let { startRow, startCol, endRow, endCol } = range;
1048
+ if (options.printAreaOnly && ws.printArea) {
1049
+ const pa = ws.printArea;
1050
+ const m = pa.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
1051
+ if (m) {
1052
+ startCol = colLetterToIdx(m[1]);
1053
+ startRow = parseInt(m[2], 10);
1054
+ endCol = colLetterToIdx(m[3]);
1055
+ endRow = parseInt(m[4], 10);
1056
+ }
1057
+ }
1058
+ const merges = ws.getMerges();
1059
+ const conditionalFormats = options.includeConditionalFormatting !== false ? ws.getConditionalFormats() : [];
1060
+ const sparklines = options.includeSparklines !== false ? ws.getSparklines() : [];
1061
+ const sparklineMap = new Map();
1062
+ for (const sp of sparklines) {
1063
+ const m = sp.location.match(/^([A-Z]+)(\d+)$/);
1064
+ if (m)
1065
+ sparklineMap.set(`${parseInt(m[2], 10)},${colLetterToIdx(m[1])}`, sp);
1066
+ }
1067
+ const cfValueMap = new Map();
1068
+ for (const cf of conditionalFormats) {
1069
+ if (!cf.colorScale && !cf.dataBar && !cf.iconSet)
1070
+ continue;
1071
+ const vals = [];
1072
+ const refs = cf.sqref.split(' ');
1073
+ for (const ref of refs) {
1074
+ const rm = ref.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
1075
+ if (rm) {
1076
+ for (let r = parseInt(rm[2], 10); r <= parseInt(rm[4], 10); r++) {
1077
+ for (let c = colLetterToIdx(rm[1]); c <= colLetterToIdx(rm[3]); c++) {
1078
+ const cell = ws.getCell(r, c);
1079
+ if (typeof cell.value === 'number')
1080
+ vals.push(cell.value);
1081
+ }
1082
+ }
1083
+ }
1084
+ }
1085
+ cfValueMap.set(cf, vals);
1086
+ }
1087
+ const mergeMap = new Map();
1088
+ for (const m of merges) {
1089
+ const rs = m.endRow - m.startRow + 1;
1090
+ const cs = m.endCol - m.startCol + 1;
1091
+ mergeMap.set(`${m.startRow},${m.startCol}`, { rowSpan: rs, colSpan: cs });
1092
+ for (let r = m.startRow; r <= m.endRow; r++) {
1093
+ for (let c = m.startCol; c <= m.endCol; c++) {
1094
+ if (r !== m.startRow || c !== m.startCol)
1095
+ mergeMap.set(`${r},${c}`, 'skip');
1096
+ }
1097
+ }
1098
+ }
1099
+ const colWidths = [];
1100
+ if (options.includeColumnWidths !== false) {
1101
+ for (let c = startCol; c <= endCol; c++) {
1102
+ const def = ws.getColumn(c);
1103
+ if (options.skipHidden && def?.hidden)
1104
+ continue;
1105
+ const w = def?.width ? Math.round(def.width * 7.5) : undefined;
1106
+ colWidths.push(w ? `<col style="width:${w}px">` : '<col>');
1107
+ }
1108
+ }
1109
+ const cellImageMap = new Map();
1110
+ const cellImages = ws.getCellImages?.();
1111
+ if (cellImages) {
1112
+ for (const ci of cellImages)
1113
+ cellImageMap.set(ci.cell, ci);
1114
+ }
1115
+ const rows = [];
1116
+ for (let r = startRow; r <= endRow; r++) {
1117
+ const rowDef = ws.getRow(r);
1118
+ if (options.skipHidden && rowDef?.hidden)
1119
+ continue;
1120
+ const rowStyle = rowDef?.height ? ` style="height:${rowDef.height}px"` : '';
1121
+ const cells = [];
1122
+ for (let c = startCol; c <= endCol; c++) {
1123
+ const colDef = ws.getColumn(c);
1124
+ if (options.skipHidden && colDef?.hidden)
1125
+ continue;
1126
+ const key = `${r},${c}`;
1127
+ const merge = mergeMap.get(key);
1128
+ if (merge === 'skip')
1129
+ continue;
1130
+ const cell = ws.getCell(r, c);
1131
+ let val = '';
1132
+ const cellRef = `${colIndexToLetter(c)}${r}`;
1133
+ const ci = cellImageMap.get(cellRef);
1134
+ if (ci) {
1135
+ val = cellImageToHtml(ci);
1136
+ }
1137
+ else if (cell.richText) {
1138
+ val = cell.richText.map(run => {
1139
+ const s = run.font ? fontToCSS(run.font) : '';
1140
+ return s ? `<span style="${s}">${escapeXml(run.text)}</span>` : escapeXml(run.text);
1141
+ }).join('');
1142
+ }
1143
+ else if (cell.value != null) {
1144
+ const formatted = cell.style?.numberFormat
1145
+ ? formatNumber(cell.value, cell.style.numberFormat.formatCode)
1146
+ : String(cell.value);
1147
+ val = escapeXml(formatted);
1148
+ }
1149
+ if (cell.hyperlink) {
1150
+ const href = escapeXml(cell.hyperlink.href ?? '');
1151
+ const tip = cell.hyperlink.tooltip ? ` title="${escapeXml(cell.hyperlink.tooltip)}"` : '';
1152
+ val = `<a href="${href}"${tip} style="color:#0563C1;text-decoration:underline">${val}</a>`;
1153
+ }
1154
+ if (cell.comment) {
1155
+ const commentText = cell.comment.richText
1156
+ ? cell.comment.richText.map(run => run.text).join('')
1157
+ : cell.comment.text;
1158
+ val = `<span title="${escapeXml(commentText)}" style="cursor:help">${val}</span>`;
1159
+ }
1160
+ const attrs = [];
1161
+ if (merge && typeof merge !== 'string') {
1162
+ if (merge.rowSpan > 1)
1163
+ attrs.push(`rowspan="${merge.rowSpan}"`);
1164
+ if (merge.colSpan > 1)
1165
+ attrs.push(`colspan="${merge.colSpan}"`);
1166
+ }
1167
+ const cssParts = [];
1168
+ if (options.includeStyles !== false && cell.style)
1169
+ cssParts.push(styleToCSS(cell.style));
1170
+ let iconAttr = '';
1171
+ if (typeof cell.value === 'number') {
1172
+ for (const cf of conditionalFormats) {
1173
+ const allVals = cfValueMap.get(cf);
1174
+ if (!allVals)
1175
+ continue;
1176
+ const result = evaluateConditionalFormats(cf, cell.value, allVals);
1177
+ if (result.startsWith('data-icon=')) {
1178
+ iconAttr = ` ${result}`;
1179
+ }
1180
+ else if (result) {
1181
+ cssParts.push(result);
1182
+ }
1183
+ }
1184
+ }
1185
+ const css = cssParts.filter(Boolean).join(';');
1186
+ if (css)
1187
+ attrs.push(`style="${css}"`);
1188
+ attrs.push(`data-cell="${colIndexToLetter(c)}${r}"`);
1189
+ const sp = sparklineMap.get(key);
1190
+ if (sp)
1191
+ val += sparklineToSvg(sp, resolveSparklineData(ws, sp.dataRange));
1192
+ const attrStr = attrs.length ? ' ' + attrs.join(' ') : '';
1193
+ cells.push(`<td${attrStr}${iconAttr}>${val}</td>`);
1194
+ }
1195
+ rows.push(`<tr${rowStyle}>${cells.join('')}</tr>`);
1196
+ }
1197
+ const colGroup = colWidths.length ? `<colgroup>${colWidths.join('')}</colgroup>` : '';
1198
+ const tableHtml = `<div class="xl-sheet-wrapper" style="position:relative;display:inline-block"><table border="0" cellpadding="4" cellspacing="0">\n${colGroup}\n${rows.join('\n')}\n</table>`;
1199
+ let chartsHtml = '';
1200
+ if (options.includeCharts !== false) {
1201
+ const charts = ws.getCharts();
1202
+ if (charts.length)
1203
+ chartsHtml = '\n<div class="xl-charts">' + charts.map(ch => chartToHtml(ch, ws)).join('\n') + '</div>';
1204
+ }
1205
+ let imagesHtml = '';
1206
+ const images = ws.getImages?.();
1207
+ if (images?.length)
1208
+ imagesHtml = '\n<div class="xl-images">' + images.map(imageToPositionedHtml).join('\n') + '</div>';
1209
+ let shapesHtml = '';
1210
+ const shapes = ws.getShapes?.();
1211
+ if (shapes?.length)
1212
+ shapesHtml = '\n<div class="xl-shapes">' + shapes.map(shapeToHtml).join('\n') + '</div>';
1213
+ let wordArtHtml = '';
1214
+ const wordArts = ws.getWordArt?.();
1215
+ if (wordArts?.length)
1216
+ wordArtHtml = '\n<div class="xl-wordarts">' + wordArts.map(wordArtToHtml).join('\n') + '</div>';
1217
+ let mathHtml = '';
1218
+ const mathEqs = ws.getMathEquations?.();
1219
+ if (mathEqs?.length)
1220
+ mathHtml = '\n<div class="xl-math-equations">' + mathEqs.map(mathEquationToMathML).join('\n') + '</div>';
1221
+ let formControlsHtml = '';
1222
+ const fcs = ws.getFormControls?.();
1223
+ if (fcs?.length)
1224
+ formControlsHtml = '\n<div class="xl-form-controls">' + fcs.map(formControlToPositionedHtml).join('\n') + '</div>';
1225
+ const extraHtml = chartsHtml + imagesHtml + shapesHtml + wordArtHtml + mathHtml + formControlsHtml;
1226
+ const wrapperClose = '</div>';
1227
+ if (options.fullDocument === false)
1228
+ return tableHtml + extraHtml + wrapperClose;
1229
+ const title = escapeXml(options.title ?? options.sheetName ?? 'Export');
1230
+ const css = `<style>
1231
+ * { box-sizing: border-box; }
1232
+ body { font-family: 'Segoe UI', Calibri, sans-serif; margin: 20px; background: #f5f6fa; }
1233
+ .xl-sheet-wrapper { position: relative; display: inline-block; }
1234
+ table { border-collapse: collapse; background: white; box-shadow: 0 1px 4px rgba(0,0,0,.1); }
1235
+ td { padding: 4px 8px; border: 1px solid #d4d4d4; vertical-align: bottom; }
1236
+ td[data-icon]::before { content: attr(data-icon); margin-right: 4px; }
1237
+ .xl-images { position: absolute; top: 0; left: 0; pointer-events: none; }
1238
+ .xl-images .xl-img { pointer-events: auto; position: absolute; z-index: 2; }
1239
+ .xl-charts { position: absolute; top: 0; left: 0; pointer-events: none; }
1240
+ .xl-charts .xl-chart { pointer-events: auto; }
1241
+ .xl-shapes { position: absolute; top: 0; left: 0; pointer-events: none; }
1242
+ .xl-shapes .xl-shape { pointer-events: auto; }
1243
+ .xl-wordarts { position: absolute; top: 0; left: 0; pointer-events: none; }
1244
+ .xl-wordarts .xl-wordart { pointer-events: auto; }
1245
+ .xl-math-equations { position: absolute; top: 0; left: 0; pointer-events: none; }
1246
+ .xl-math-equations .xl-math { pointer-events: auto; }
1247
+ .xl-form-controls { position: absolute; top: 0; left: 0; pointer-events: none; }
1248
+ .xl-form-controls .xl-fc { pointer-events: auto; z-index: 3; }
1249
+ a { color: #0563C1; }
1250
+ </style>`;
1251
+ const positionScript = `<script>
1252
+ (function(){
1253
+ document.querySelectorAll('.xl-sheet-wrapper').forEach(function(wrapper){
1254
+ var table = wrapper.querySelector('table');
1255
+ if (!table) return;
1256
+ var startRow = parseInt((table.rows[0] && table.rows[0].cells[0] || {}).getAttribute && table.rows[0].cells[0].getAttribute('data-cell') ? table.rows[0].cells[0].getAttribute('data-cell').replace(/[A-Z]+/,'') : '1', 10) || 1;
1257
+ var startCol = 1;
1258
+ var firstCell = table.rows[0] && table.rows[0].cells[0] ? table.rows[0].cells[0].getAttribute('data-cell') : null;
1259
+ if (firstCell) {
1260
+ var colStr = firstCell.replace(/[0-9]+/g,'');
1261
+ startCol = 0;
1262
+ for (var ci = 0; ci < colStr.length; ci++) startCol = startCol * 26 + (colStr.charCodeAt(ci) - 64);
1263
+ }
1264
+ function cellRect(sheetRow, sheetCol) {
1265
+ var ri = sheetRow - startRow;
1266
+ var colIdx = sheetCol - startCol;
1267
+ if (ri < 0) ri = 0;
1268
+ if (colIdx < 0) colIdx = 0;
1269
+ var tr = table.rows[ri];
1270
+ if (!tr) tr = table.rows[table.rows.length - 1] || table.rows[0];
1271
+ if (!tr) return {x:0, y:0, w:0, h:0};
1272
+ var td = tr.cells[colIdx];
1273
+ if (!td) td = tr.cells[tr.cells.length - 1] || tr.cells[0];
1274
+ if (!td) return {x:0, y:0, w:0, h:0};
1275
+ return {x: td.offsetLeft, y: td.offsetTop, w: td.offsetWidth, h: td.offsetHeight};
1276
+ }
1277
+ wrapper.querySelectorAll('[data-from-col][data-from-row]').forEach(function(el){
1278
+ var fc = parseInt(el.getAttribute('data-from-col'),10);
1279
+ var fr = parseInt(el.getAttribute('data-from-row'),10);
1280
+ var from = cellRect(fr, fc);
1281
+ el.style.left = from.x + 'px';
1282
+ el.style.top = from.y + 'px';
1283
+ var tc = el.getAttribute('data-to-col');
1284
+ var tr2 = el.getAttribute('data-to-row');
1285
+ if (tc !== null && tr2 !== null) {
1286
+ var to = cellRect(parseInt(tr2,10), parseInt(tc,10));
1287
+ var w = to.x + to.w - from.x;
1288
+ var h = to.y + to.h - from.y;
1289
+ if (w > 0) el.style.width = w + 'px';
1290
+ if (h > 0) el.style.height = h + 'px';
1291
+ }
1292
+ });
1293
+ });
1294
+ })();
1295
+ </script>`;
1296
+ return `<!DOCTYPE html>
1297
+ <html lang="en">
1298
+ <head>
1299
+ <meta charset="UTF-8">
1300
+ <title>${title}</title>
1301
+ ${css}
1302
+ </head>
1303
+ <body>
1304
+ ${tableHtml}${extraHtml}${wrapperClose}
1305
+ ${positionScript}
1306
+ </body>
1307
+ </html>`;
1308
+ }
1309
+ export function workbookToHtml(wb, options = {}) {
1310
+ const sheets = wb.getSheets();
1311
+ const names = wb.getSheetNames();
1312
+ const selected = options.sheets ?? names;
1313
+ const includeTabs = options.includeTabs !== false;
1314
+ if (options.evaluateFormulas) {
1315
+ new FormulaEngine().calculateWorkbook(wb);
1316
+ }
1317
+ const sheetHtmls = [];
1318
+ for (let i = 0; i < sheets.length; i++) {
1319
+ if (!selected.includes(names[i]))
1320
+ continue;
1321
+ if (sheets[i]._isChartSheet) {
1322
+ const charts = sheets[i].getCharts();
1323
+ if (charts.length) {
1324
+ const chartHtml = chartToSvg(charts[0], sheets[i]);
1325
+ sheetHtmls.push({ name: names[i], html: `<div class="xl-sheet-wrapper" style="position:relative;display:inline-block">${chartHtml}</div>` });
1326
+ }
1327
+ continue;
1328
+ }
1329
+ if (sheets[i]._isDialogSheet) {
1330
+ const fcs = sheets[i].getFormControls?.() ?? [];
1331
+ if (fcs.length) {
1332
+ const fcHtml = fcs.map(formControlToPositionedHtml).join('\n');
1333
+ sheetHtmls.push({ name: names[i], html: `<div class="xl-sheet-wrapper" style="position:relative;display:inline-block;min-width:400px;min-height:300px"><div class="xl-form-controls">${fcHtml}</div></div>` });
1334
+ }
1335
+ else {
1336
+ sheetHtmls.push({ name: names[i], html: '<div class="xl-sheet-wrapper"><p>Dialog Sheet</p></div>' });
1337
+ }
1338
+ continue;
1339
+ }
1340
+ const html = worksheetToHtml(sheets[i], { ...options, fullDocument: false, sheetName: names[i] });
1341
+ sheetHtmls.push({ name: names[i], html });
1342
+ }
1343
+ if (sheetHtmls.length === 1 && !includeTabs) {
1344
+ return worksheetToHtml(sheets[0], options);
1345
+ }
1346
+ const title = escapeXml(options.title ?? 'Workbook Export');
1347
+ const tabs = sheetHtmls.map((s, i) => `<button class="tab${i === 0 ? ' active' : ''}" onclick="switchTab(${i})">${escapeXml(s.name)}</button>`).join('');
1348
+ const panels = sheetHtmls.map((s, i) => `<div class="panel${i === 0 ? ' active' : ''}" id="panel-${i}">${s.html}</div>`).join('\n');
1349
+ return `<!DOCTYPE html>
1350
+ <html lang="en">
1351
+ <head>
1352
+ <meta charset="UTF-8">
1353
+ <title>${title}</title>
1354
+ <style>
1355
+ * { box-sizing: border-box; }
1356
+ body { font-family: 'Segoe UI', Calibri, sans-serif; margin: 0; background: #f5f6fa; }
1357
+ .tab-bar { display: flex; background: #2b579a; padding: 0 16px; gap: 2px; position: sticky; top: 0; z-index: 10; }
1358
+ .tab { padding: 8px 20px; border: none; background: rgba(255,255,255,.15); color: white; cursor: pointer;
1359
+ font-size: 13px; border-radius: 4px 4px 0 0; margin-top: 4px; transition: background .15s; }
1360
+ .tab:hover { background: rgba(255,255,255,.3); }
1361
+ .tab.active { background: white; color: #2b579a; font-weight: 600; }
1362
+ .panel { display: none; padding: 20px; overflow: auto; }
1363
+ .panel.active { display: block; }
1364
+ .xl-sheet-wrapper { position: relative; display: inline-block; }
1365
+ table { border-collapse: collapse; background: white; box-shadow: 0 1px 4px rgba(0,0,0,.1); }
1366
+ td { padding: 4px 8px; border: 1px solid #d4d4d4; vertical-align: bottom; }
1367
+ td[data-icon]::before { content: attr(data-icon); margin-right: 4px; }
1368
+ .xl-images { position: absolute; top: 0; left: 0; pointer-events: none; }
1369
+ .xl-images .xl-img { pointer-events: auto; position: absolute; z-index: 2; }
1370
+ .xl-charts { position: absolute; top: 0; left: 0; pointer-events: none; }
1371
+ .xl-charts .xl-chart { pointer-events: auto; }
1372
+ .xl-shapes { position: absolute; top: 0; left: 0; pointer-events: none; }
1373
+ .xl-shapes .xl-shape { pointer-events: auto; }
1374
+ .xl-wordarts { position: absolute; top: 0; left: 0; pointer-events: none; }
1375
+ .xl-wordarts .xl-wordart { pointer-events: auto; }
1376
+ .xl-math-equations { position: absolute; top: 0; left: 0; pointer-events: none; }
1377
+ .xl-math-equations .xl-math { pointer-events: auto; }
1378
+ .xl-form-controls { position: absolute; top: 0; left: 0; pointer-events: none; }
1379
+ .xl-form-controls .xl-fc { pointer-events: auto; z-index: 3; }
1380
+ a { color: #0563C1; }
1381
+ </style>
1382
+ </head>
1383
+ <body>
1384
+ ${includeTabs ? `<div class="tab-bar">${tabs}</div>` : ''}
1385
+ ${panels}
1386
+ <script>
1387
+ function switchTab(idx) {
1388
+ document.querySelectorAll('.tab').forEach((t,i) => t.classList.toggle('active', i===idx));
1389
+ document.querySelectorAll('.panel').forEach((p,i) => p.classList.toggle('active', i===idx));
1390
+ // Re-run positioning after tab switch since hidden panels may have 0 dimensions
1391
+ setTimeout(positionOverlays, 50);
1392
+ }
1393
+ function positionOverlays() {
1394
+ document.querySelectorAll('.xl-sheet-wrapper').forEach(function(wrapper){
1395
+ var table = wrapper.querySelector('table');
1396
+ if (!table) return;
1397
+ var startRow = 1, startCol = 1;
1398
+ var firstCell = table.rows[0] && table.rows[0].cells[0] ? table.rows[0].cells[0].getAttribute('data-cell') : null;
1399
+ if (firstCell) {
1400
+ startRow = parseInt(firstCell.replace(/[A-Z]+/,''), 10) || 1;
1401
+ var colStr = firstCell.replace(/[0-9]+/g,'');
1402
+ startCol = 0;
1403
+ for (var ci = 0; ci < colStr.length; ci++) startCol = startCol * 26 + (colStr.charCodeAt(ci) - 64);
1404
+ }
1405
+ function cellRect(sheetRow, sheetCol) {
1406
+ var ri = sheetRow - startRow;
1407
+ var colIdx = sheetCol - startCol;
1408
+ if (ri < 0) ri = 0;
1409
+ if (colIdx < 0) colIdx = 0;
1410
+ var tr = table.rows[ri];
1411
+ if (!tr) tr = table.rows[table.rows.length - 1] || table.rows[0];
1412
+ if (!tr) return {x:0, y:0, w:0, h:0};
1413
+ var td = tr.cells[colIdx];
1414
+ if (!td) td = tr.cells[tr.cells.length - 1] || tr.cells[0];
1415
+ if (!td) return {x:0, y:0, w:0, h:0};
1416
+ return {x: td.offsetLeft, y: td.offsetTop, w: td.offsetWidth, h: td.offsetHeight};
1417
+ }
1418
+ wrapper.querySelectorAll('[data-from-col][data-from-row]').forEach(function(el){
1419
+ var fc = parseInt(el.getAttribute('data-from-col'),10);
1420
+ var fr = parseInt(el.getAttribute('data-from-row'),10);
1421
+ var from = cellRect(fr, fc);
1422
+ el.style.left = from.x + 'px';
1423
+ el.style.top = from.y + 'px';
1424
+ var tc = el.getAttribute('data-to-col');
1425
+ var tr2 = el.getAttribute('data-to-row');
1426
+ if (tc !== null && tr2 !== null) {
1427
+ var to = cellRect(parseInt(tr2,10), parseInt(tc,10));
1428
+ var w = to.x + to.w - from.x;
1429
+ var h = to.y + to.h - from.y;
1430
+ if (w > 0) el.style.width = w + 'px';
1431
+ if (h > 0) el.style.height = h + 'px';
1432
+ }
1433
+ });
1434
+ });
1435
+ }
1436
+ positionOverlays();
1437
+ </script>
1438
+ </body>
1439
+ </html>`;
1440
+ }
1441
+ //# sourceMappingURL=HtmlModule.js.map