wysiwygido 1.3.0 → 1.4.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 (41) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +4 -2
  3. package/dist/adapter.js +1 -1
  4. package/dist/{chart-D61cP2Zp.js → chart-CEPd_jSz.js} +2 -139
  5. package/dist/chart-CEPd_jSz.js.map +1 -0
  6. package/dist/crop-oLrgb-A3.js +140 -0
  7. package/dist/crop-oLrgb-A3.js.map +1 -0
  8. package/dist/{data-table-CgmN4xc4.js → data-table-C4bo747f.js} +19 -8
  9. package/dist/{data-table-CgmN4xc4.js.map → data-table-C4bo747f.js.map} +1 -1
  10. package/dist/{data-table-app-B2_xsug1.js → data-table-app-BCG8N37n.js} +2 -2
  11. package/dist/{data-table-app-B2_xsug1.js.map → data-table-app-BCG8N37n.js.map} +1 -1
  12. package/dist/data-table.d.ts +13 -0
  13. package/dist/data-table.js +2 -2
  14. package/dist/docx.js +3 -2
  15. package/dist/docx.js.map +1 -1
  16. package/dist/{grid-formulas-CFGWbz2E.js → grid-formulas-DcdcPnK7.js} +21 -1
  17. package/dist/grid-formulas-DcdcPnK7.js.map +1 -0
  18. package/dist/grid-formulas.d.ts +2 -0
  19. package/dist/{office-chrome-BmnDYfog.js → office-chrome-BTBP-K4h.js} +4 -1
  20. package/dist/office-chrome-BTBP-K4h.js.map +1 -0
  21. package/dist/presentation-app.d.ts +11 -2
  22. package/dist/presentation-pptx.d.ts +15 -0
  23. package/dist/presentation.js +1496 -5
  24. package/dist/presentation.js.map +1 -1
  25. package/dist/react.js +1 -1
  26. package/dist/spreadsheet-app.d.ts +17 -0
  27. package/dist/spreadsheet-dialog.d.ts +39 -0
  28. package/dist/spreadsheet-features.d.ts +112 -0
  29. package/dist/spreadsheet-model.d.ts +40 -0
  30. package/dist/spreadsheet-view.d.ts +10 -0
  31. package/dist/spreadsheet.js +1736 -131
  32. package/dist/spreadsheet.js.map +1 -1
  33. package/dist/{src-D4NV1D9Y.js → src-CYQYge8P.js} +6 -5
  34. package/dist/{src-D4NV1D9Y.js.map → src-CYQYge8P.js.map} +1 -1
  35. package/dist/style.css +26 -1
  36. package/dist/vue.js +1 -1
  37. package/dist/wysiwygido.js +6 -5
  38. package/package.json +1 -1
  39. package/dist/chart-D61cP2Zp.js.map +0 -1
  40. package/dist/grid-formulas-CFGWbz2E.js.map +0 -1
  41. package/dist/office-chrome-BmnDYfog.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses [Semantic Versioning](https://semver.org/). Before 1.0.0 minor versions could contain breaking changes. From 1.0.0 on, only a major version may break the public API (see "Stability and versioning" in the README); changes are listed under "Changed" or "Removed".
4
4
 
5
+ ## 1.4.0 - 2026-10-25
6
+
7
+ - **Presentation: PowerPoint (.pptx) import and export.** File menu entries "Open .pptx…" and "Download as PowerPoint (.pptx)"; `toPptx()` and `loadPptx(file)`; an `import` event (`{ slides, skipped }`). Strict zip and XML reader (size, entry count and compression limits, no DOCTYPE or entities, path checks); everything read goes through `cleanDeck`.
8
+
9
+ - **Spreadsheet: merged cells, conditional formats and charts.** Merge, merge across and unmerge (the selection, the arrow keys, the editor and printing treat a merge as one cell); conditional formatting with highlight rules, top and bottom, above and below average, duplicates, blanks, errors, data bars, colour scales and formula rules, and a rules list; column, line and pie charts you can move, resize, edit and delete. All saved in the workbook, followed through inserted and deleted rows and columns, undoable, limited in number (2,000 merges, 100 rules and 50 charts per sheet) and checked when loaded. New API: `merge`, `unmerge`, `addConditionalFormat`, `clearConditionalFormats`, `addChart`, `removeChart`.
10
+
5
11
  ## 1.3.0 - 2026-10-25
6
12
 
7
13
  - **Security audit, second part**: pasted HTML can no longer carry tracked-change authors, mention ids or comment ids; an embed from the page's own origin never gets `allow-same-origin`; `encryptedDraftStore`, `deriveDraftKey` and `IndexedDBDraftStore.clear()` for drafts on shared computers; a regex replacement keeps lookbehind and anchors (`$1` is expanded from the match as found); adding a comment finds its words again across bold or italic runs; rejected pictures and unreadable files are reported (`image-rejected`, `error`, `import` events, no more silent failures); pictures over 25 MB are refused; optional peers `katex >=0.18.2`, `mermaid >=12.1.0`, `vue >=3.5.0`; `publint` and `attw` are pinned in `check:package`.
package/README.md CHANGED
@@ -623,7 +623,9 @@ sheet.getValue('C1'); // 14400 + 1200
623
623
  sheet.toCSV(); sheet.getData(); sheet.setMenu('floating');
624
624
  ```
625
625
 
626
- The two **menus** share the same commands. The classic ribbon has tabs with labelled groups; the floating menu is a card with a header (document name, save status, your own `end` element for avatars or a Share button), a menu bar with drop-downs, and a one-row toolbar whose last items fold into "⋯" when the window is narrow. `menu` also takes `'menubar'` (the card with only its drop-down menus), `'formula'` (only the formula editor) and `'none'`; `setMenu(mode)` changes it while running and `setFormulaBar(false)` hides the formula editor. Options: `theme` (`'light'`, `'dark'`, `'auto'` or any colour theme of the editor), `readOnly`, `height`, `formulaBar`, `sheetTabs`, `statusBar`, `gridlines`, `headings`, `currency`, `locale`, `onChange`. Not included: merged cells, charts, conditional formats, defined names in the UI, `LAMBDA` and its helpers, complex-number, bond, cube and web functions (those give `#NAME?`).
626
+ The two **menus** share the same commands. The classic ribbon has tabs with labelled groups; the floating menu is a card with a header (document name, save status, your own `end` element for avatars or a Share button), a menu bar with drop-downs, and a one-row toolbar whose last items fold into "⋯" when the window is narrow. `menu` also takes `'menubar'` (the card with only its drop-down menus), `'formula'` (only the formula editor) and `'none'`; `setMenu(mode)` changes it while running and `setFormulaBar(false)` hides the formula editor. Options: `theme` (`'light'`, `'dark'`, `'auto'` or any colour theme of the editor), `readOnly`, `height`, `formulaBar`, `sheetTabs`, `statusBar`, `gridlines`, `headings`, `currency`, `locale`, `onChange`. Not included: defined names in the UI, `LAMBDA` and its helpers, complex-number, bond, cube and web functions (those give `#NAME?`), and pivot tables.
627
+
628
+ **Merged cells, conditional formats and charts.** Merge cells from the alignment group (Merge cells, Merge across, Unmerge); the top left value stays and the rest is emptied; selections and arrow keys treat a merge as one cell, and rows or columns inserted or deleted move it. Conditional formatting (Home, Styles) has highlight rules (greater than, less than, between, equal to, text that contains, duplicate or unique values, blanks, errors), top and bottom N or N percent, above and below average, data bars, two- and three-colour scales and formula rules, with a list to manage them. Charts (Insert, Chart) are columns, lines or pies drawn from a range (first row names the series, first column the labels); drag to move, drag the corner to resize, double-click to edit, Delete to remove. Everything is saved with the workbook, follows inserted rows and columns, is checked on load, and prints (charts after the table). From code: `sheet.merge('A1:C1')`, `sheet.addConditionalFormat('B2:B20', { type: 'cell', op: '>', a: '100', style: { bg: '#fecaca' } })`, `sheet.addChart({ range: 'A1:C13', type: 'line', title: 'Sales' })`. Copy and paste does not carry merges, rules or charts to another place.
627
629
 
628
630
  ## Presentation
629
631
 
@@ -644,7 +646,7 @@ deck.present(0); // deck.present(0, true) opens the presente
644
646
  const page = deck.toHTML(); // everything in one file
645
647
  ```
646
648
 
647
- Everything that comes from outside (a loaded file, pasted text, a dropped picture) is cleaned: text keeps a short list of tags and styles, links keep `http(s)`, `mailto` and `tel`, pictures must be `data:` images or `http(s)`, so a deck from someone else cannot run code. `PowerPoint (.pptx)` files are not read or written.
649
+ Everything that comes from outside (a loaded file, pasted text, a dropped picture) is cleaned: text keeps a short list of tags and styles, links keep `http(s)`, `mailto` and `tel`, pictures must be `data:` images or `http(s)`, so a deck from someone else cannot run code. **PowerPoint files** open and save (File, Open .pptx / Download as PowerPoint, or `deck.loadPptx(file)` and `deck.toPptx()`): text boxes and shapes with their text formatting, bullets, links, pictures (png, jpeg, gif, webp on import; svg is drawn to png on export), notes, backgrounds, hidden slides and fade, slide and zoom transitions. Tables, charts, SmartArt, video, audio and animations are skipped and listed in `skipped` (and the `import` event). Gradients become their first colour, master text styles are not read, and a file is opened in a strict reader: no more than 2,000 parts or 150 MB unpacked, no entity declarations, and links only to `http(s)`, `mailto` and `tel`. Files written here read back faithfully and open in a strict parser (python-pptx); they have not been opened in PowerPoint, Keynote or Google Slides.
648
650
 
649
651
  ## Themes
650
652
 
package/dist/adapter.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as createEditor } from "./src-D4NV1D9Y.js";
1
+ import { t as createEditor } from "./src-CYQYge8P.js";
2
2
  // src/adapter.ts
3
3
  /**
4
4
  * Framework-neutral binding for anything that needs a "value in, value out" control: Angular's `ControlValueAccessor`, Svelte, Solid,
@@ -1,140 +1,3 @@
1
- // src/url.ts
2
- /** Where a link may point: http(s), mailto, tel, an in-page anchor or a same-site path. `//host` (protocol-relative) is not allowed. */
3
- var HREF = /^(?:https?:|mailto:|tel:|#|\/(?![\/\\\t\r\n]))/i;
4
- /** Where an image may come from: http(s), a same-site path, or a base64 raster data URL (not SVG). */
5
- var SRC = /^(?:https?:|data:image\/(?:png|jpe?g|gif|webp|bmp|avif);base64,|\/(?![\/\\\t\r\n]))/i;
6
- var isSafeHref = (u) => typeof u === "string" && HREF.test(u);
7
- var isSafeSrc = (u) => typeof u === "string" && SRC.test(u);
8
- /** Make a URL safe to place inside Markdown `(...)`: characters that end or split the destination are percent-encoded. */
9
- var markdownUrl = (u) => u.replace(/[()\s<>\\]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
10
- // src/crop.ts
11
- /** The smallest visible share of the image on either axis (so a crop can never collapse to nothing). */
12
- var MIN_VISIBLE = .05;
13
- var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
14
- var round = (v) => Math.round(v * 1e4) / 1e4;
15
- /** Clamp a crop into a valid range; null means "no crop". Junk values are treated as 0. */
16
- function normalizeCrop(input) {
17
- if (!input || typeof input !== "object") return null;
18
- const n = (v) => typeof v === "number" && Number.isFinite(v) ? clamp(v, 0, .95) : 0;
19
- let { left, top, right, bottom } = {
20
- left: n(input.left),
21
- top: n(input.top),
22
- right: n(input.right),
23
- bottom: n(input.bottom)
24
- };
25
- const fit = (a, b) => {
26
- const excess = a + b - .95;
27
- if (excess <= 0) return [a, b];
28
- return a >= b ? [a - excess, b] : [a, b - excess];
29
- };
30
- [left, right] = fit(left, right);
31
- [top, bottom] = fit(top, bottom);
32
- const c = {
33
- left: round(left),
34
- top: round(top),
35
- right: round(right),
36
- bottom: round(bottom)
37
- };
38
- return c.left || c.top || c.right || c.bottom ? c : null;
39
- }
40
- /** Natural size of the visible region (what a crop leaves), in natural px. */
41
- function visibleSize(crop, nat) {
42
- const c = crop ?? {
43
- left: 0,
44
- top: 0,
45
- right: 0,
46
- bottom: 0
47
- };
48
- return {
49
- w: nat.w * (1 - c.left - c.right),
50
- h: nat.h * (1 - c.top - c.bottom)
51
- };
52
- }
53
- /** Lay out a crop so the visible box is `displayWidth` px wide. */
54
- function cropLayout(crop, nat, displayWidth) {
55
- const vw = 1 - crop.left - crop.right;
56
- const vh = 1 - crop.top - crop.bottom;
57
- const width = displayWidth;
58
- const height = width * (vh * nat.h) / (vw * nat.w);
59
- const imgWidth = width / vw;
60
- const imgHeight = height / vh;
61
- return {
62
- width,
63
- height,
64
- imgWidth,
65
- imgHeight,
66
- offsetX: -crop.left * imgWidth,
67
- offsetY: -crop.top * imgHeight
68
- };
69
- }
70
- /** Serialize as "left,top,right,bottom" for a data attribute. */
71
- var cropToString = (c) => `${c.left},${c.top},${c.right},${c.bottom}`;
72
- function cropFromString(s) {
73
- if (!s) return null;
74
- const p = s.split(",").map(Number);
75
- if (p.length !== 4 || p.some((x) => !Number.isFinite(x) || x < 0 || x >= 1)) return null;
76
- return normalizeCrop({
77
- left: p[0],
78
- top: p[1],
79
- right: p[2],
80
- bottom: p[3]
81
- });
82
- }
83
- /**
84
- * Apply a drag of (dx, dy) fractions of the full image to a crop rectangle. Edges move independently,
85
- * `move` slides the whole rectangle without resizing it. Result is always valid.
86
- */
87
- function dragCrop(start, handle, dx, dy) {
88
- let { left, top, right, bottom } = start;
89
- if (handle === "move") {
90
- const w = 1 - left - right;
91
- const h = 1 - top - bottom;
92
- left = clamp(left + dx, 0, 1 - w);
93
- top = clamp(top + dy, 0, 1 - h);
94
- right = 1 - w - left;
95
- bottom = 1 - h - top;
96
- } else {
97
- if (handle.includes("w")) left = clamp(left + dx, 0, 1 - right - MIN_VISIBLE);
98
- if (handle.includes("e")) right = clamp(right - dx, 0, 1 - left - MIN_VISIBLE);
99
- if (handle.includes("n")) top = clamp(top + dy, 0, 1 - bottom - MIN_VISIBLE);
100
- if (handle.includes("s")) bottom = clamp(bottom - dy, 0, 1 - top - MIN_VISIBLE);
101
- }
102
- return {
103
- left: round(left),
104
- top: round(top),
105
- right: round(right),
106
- bottom: round(bottom)
107
- };
108
- }
109
- /**
110
- * Render the cropped region of an image to PNG bytes (browser only: needs canvas). Returns null when canvas
111
- * is unavailable, so callers can fall back to the full image.
112
- */
113
- async function cropBytes(data, crop) {
114
- try {
115
- if (typeof createImageBitmap !== "function") return null;
116
- const bmp = await createImageBitmap(new Blob([data]));
117
- const sx = Math.round(bmp.width * crop.left);
118
- const sy = Math.round(bmp.height * crop.top);
119
- const sw = Math.max(1, Math.round(bmp.width * (1 - crop.left - crop.right)));
120
- const sh = Math.max(1, Math.round(bmp.height * (1 - crop.top - crop.bottom)));
121
- const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(sw, sh) : Object.assign(document.createElement("canvas"), {
122
- width: sw,
123
- height: sh
124
- });
125
- const ctx = canvas.getContext("2d");
126
- if (!ctx) return null;
127
- ctx.drawImage(bmp, sx, sy, sw, sh, 0, 0, sw, sh);
128
- const blob = "convertToBlob" in canvas ? await canvas.convertToBlob({ type: "image/png" }) : await new Promise((r) => canvas.toBlob(r, "image/png"));
129
- return blob ? {
130
- data: new Uint8Array(await blob.arrayBuffer()),
131
- width: sw,
132
- height: sh
133
- } : null;
134
- } catch {
135
- return null;
136
- }
137
- }
138
1
  // src/chart.ts
139
2
  var TYPES = [
140
3
  "bar",
@@ -282,6 +145,6 @@ function chartSVG(spec, width = 560, height = 320) {
282
145
  if (legendH) parts.push(legend(spec.series.map((s) => s.name)));
283
146
  return `${head}${parts.join("")}</svg>`;
284
147
  }
285
- export { MIN_VISIBLE as a, cropLayout as c, normalizeCrop as d, visibleSize as f, markdownUrl as h, specFromRows as i, cropToString as l, isSafeSrc as m, chartSVG as n, cropBytes as o, isSafeHref as p, cleanChart as r, cropFromString as s, CHART_COLORS as t, dragCrop as u };
148
+ export { specFromRows as a, parseNumber as i, chartSVG as n, cleanChart as r, CHART_COLORS as t };
286
149
 
287
- //# sourceMappingURL=chart-D61cP2Zp.js.map
150
+ //# sourceMappingURL=chart-CEPd_jSz.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chart-CEPd_jSz.js","names":[],"sources":["../src/chart.ts"],"sourcesContent":["export type ChartType = 'bar' | 'line' | 'pie';\nexport interface ChartSpec { type: ChartType; title: string; labels: string[]; series: { name: string; values: number[] }[] }\n\nconst TYPES: ChartType[] = ['bar', 'line', 'pie'];\nexport const CHART_COLORS = ['#2563eb', '#ea580c', '#16a34a', '#9333ea', '#dc2626', '#0891b2', '#ca8a04', '#db2777'];\nconst esc = (s: string) => s.replace(/[&<>\"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;' })[c]!);\nconst str = (v: unknown, max: number) => (typeof v === 'string' ? v.replace(/[\\u0000-\\u001f]/g, ' ').slice(0, max) : '');\n\n/** A well-formed spec from untrusted input (JSON text or an object), or null when there is nothing to draw. */\nexport function cleanChart(input: unknown): ChartSpec | null {\n let o: any = input;\n if (typeof input === 'string') { try { o = JSON.parse(input); } catch { return null; } }\n if (!o || typeof o !== 'object') return null;\n const labels = (Array.isArray(o.labels) ? o.labels : []).slice(0, 50).map((l: unknown) => str(l, 40));\n const series = (Array.isArray(o.series) ? o.series : []).slice(0, 8).map((s: any) => ({\n name: str(s?.name, 40),\n values: labels.map((_: string, i: number) => { const n = Number(s?.values?.[i]); return Number.isFinite(n) ? Math.max(-1e12, Math.min(1e12, n)) : 0; }),\n }));\n if (!labels.length || !series.length) return null;\n return { type: TYPES.includes(o.type) ? o.type : 'bar', title: str(o.title, 120), labels, series };\n}\n\n/** Parse \"1,234.5\", \"12%\", \"$40\" … ; NaN when it is not a number. */\nexport function parseNumber(text: string): number {\n const t = text.trim().replace(/[\\s$€£¥%]/g, '').replace(/,(?=\\d{3}(\\D|$))/g, '');\n return t && /^-?\\d*\\.?\\d+(e[+-]?\\d+)?$/i.test(t) ? Number(t) : NaN;\n}\n\n/** Chart data from a table: first row = series names, first column = labels, the rest numbers (a first row of numbers means no header). */\nexport function specFromRows(rows: string[][], type: ChartType = 'bar'): ChartSpec | null {\n if (rows.length < 2 || rows[0].length < 2) return null;\n const header = rows[0];\n const body = rows.slice(1);\n const cols = header.length - 1;\n const series = Array.from({ length: cols }, (_, c) => ({ name: header[c + 1] || `Series ${c + 1}`, values: body.map((r) => { const n = parseNumber(r[c + 1] ?? ''); return Number.isNaN(n) ? 0 : n; }) }));\n if (!series.some((s) => s.values.some((v) => v !== 0))) return null;\n return cleanChart({ type, title: '', labels: body.map((r) => r[0] ?? ''), series });\n}\n\nconst nice = (max: number) => { if (max <= 0) return 1; const p = 10 ** Math.floor(Math.log10(max)); const f = max / p; return (f <= 1 ? 1 : f <= 2 ? 2 : f <= 5 ? 5 : 10) * p; };\nconst fmt = (n: number) => (Math.abs(n) >= 1e6 ? `${+(n / 1e6).toFixed(1)}M` : Math.abs(n) >= 1e4 ? `${+(n / 1e3).toFixed(1)}k` : String(+n.toFixed(2)));\n\n/** Self-contained SVG (text in `currentColor`, fixed series colours) with an accessible title and description. */\nexport function chartSVG(spec: ChartSpec, width = 560, height = 320): string {\n const W = width, H = height;\n const head = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 ${W} ${H}\" width=\"${W}\" height=\"${H}\" role=\"img\" aria-label=\"${esc(spec.title || `${spec.type} chart`)}\" font-family=\"system-ui, sans-serif\" font-size=\"12\">`;\n const parts: string[] = [`<title>${esc(spec.title || `${spec.type} chart`)}</title>`];\n const top = spec.title ? 34 : 14;\n if (spec.title) parts.push(`<text x=\"${W / 2}\" y=\"22\" text-anchor=\"middle\" font-size=\"15\" font-weight=\"700\" fill=\"currentColor\">${esc(spec.title)}</text>`);\n const legendH = spec.type === 'pie' || spec.series.length > 1 ? 24 : 0;\n const color = (i: number) => CHART_COLORS[i % CHART_COLORS.length];\n const legend = (items: string[]) => {\n let x = 12;\n return items.map((n, i) => { const g = `<rect x=\"${x}\" y=\"${H - 18}\" width=\"10\" height=\"10\" rx=\"2\" fill=\"${color(i)}\"/><text x=\"${x + 14}\" y=\"${H - 9}\" fill=\"currentColor\">${esc(n)}</text>`; x += 26 + n.length * 7; return g; }).join('');\n };\n if (spec.type === 'pie') {\n const vals = spec.series[0].values.map((v) => Math.max(0, v));\n const total = vals.reduce((a, b) => a + b, 0) || 1;\n const cx = W / 2, cy = top + (H - top - legendH - 8) / 2, r = Math.min(W / 2 - 20, (H - top - legendH - 16) / 2);\n let a0 = -Math.PI / 2;\n vals.forEach((v, i) => {\n if (v <= 0) return;\n const a1 = a0 + (v / total) * Math.PI * 2;\n const big = a1 - a0 > Math.PI ? 1 : 0;\n const p = (a: number) => `${(cx + r * Math.cos(a)).toFixed(2)} ${(cy + r * Math.sin(a)).toFixed(2)}`;\n parts.push(v / total >= 0.9999 ? `<circle cx=\"${cx}\" cy=\"${cy}\" r=\"${r}\" fill=\"${color(i)}\"/>` : `<path d=\"M${cx} ${cy} L${p(a0)} A${r} ${r} 0 ${big} 1 ${p(a1)} Z\" fill=\"${color(i)}\" stroke=\"#fff\" stroke-width=\"1.5\"/>`);\n if (v / total > 0.06) { const am = (a0 + a1) / 2; parts.push(`<text x=\"${(cx + r * 0.62 * Math.cos(am)).toFixed(1)}\" y=\"${(cy + r * 0.62 * Math.sin(am) + 4).toFixed(1)}\" text-anchor=\"middle\" fill=\"#fff\" font-weight=\"700\">${Math.round((v / total) * 100)}%</text>`); }\n a0 = a1;\n });\n parts.push(legend(spec.labels));\n return `${head}${parts.join('')}</svg>`;\n }\n const left = 46, right = 14, bottom = 28 + legendH;\n const pw = W - left - right, ph = H - top - bottom;\n const all = spec.series.flatMap((s) => s.values);\n const max = nice(Math.max(0, ...all)), min = Math.min(0, ...all) < 0 ? -nice(-Math.min(...all)) : 0;\n const y = (v: number) => top + ph - ((v - min) / (max - min)) * ph;\n for (let i = 0; i <= 4; i++) {\n const v = min + ((max - min) * i) / 4, yy = y(v);\n parts.push(`<line x1=\"${left}\" x2=\"${W - right}\" y1=\"${yy.toFixed(1)}\" y2=\"${yy.toFixed(1)}\" stroke=\"currentColor\" stroke-opacity=\".15\"/><text x=\"${left - 6}\" y=\"${(yy + 4).toFixed(1)}\" text-anchor=\"end\" fill=\"currentColor\" fill-opacity=\".7\">${fmt(v)}</text>`);\n }\n const n = spec.labels.length, slot = pw / n;\n spec.labels.forEach((l, i) => { if (n <= 12 || i % Math.ceil(n / 12) === 0) parts.push(`<text x=\"${(left + slot * (i + 0.5)).toFixed(1)}\" y=\"${top + ph + 16}\" text-anchor=\"middle\" fill=\"currentColor\" fill-opacity=\".8\">${esc(l.length > 12 ? `${l.slice(0, 11)}…` : l)}</text>`); });\n if (spec.type === 'bar') {\n const bw = Math.min(40, (slot * 0.76) / spec.series.length);\n spec.series.forEach((s, si) => s.values.forEach((v, i) => {\n const x = left + slot * (i + 0.5) - (bw * spec.series.length) / 2 + bw * si;\n parts.push(`<rect x=\"${x.toFixed(1)}\" y=\"${Math.min(y(v), y(0)).toFixed(1)}\" width=\"${bw.toFixed(1)}\" height=\"${Math.abs(y(v) - y(0)).toFixed(1)}\" rx=\"2\" fill=\"${color(si)}\"><title>${esc(s.name)} · ${esc(spec.labels[i])}: ${v}</title></rect>`);\n }));\n } else {\n spec.series.forEach((s, si) => {\n const pts = s.values.map((v, i) => `${(left + slot * (i + 0.5)).toFixed(1)},${y(v).toFixed(1)}`);\n parts.push(`<polyline points=\"${pts.join(' ')}\" fill=\"none\" stroke=\"${color(si)}\" stroke-width=\"2.5\" stroke-linejoin=\"round\"/>`);\n s.values.forEach((v, i) => parts.push(`<circle cx=\"${(left + slot * (i + 0.5)).toFixed(1)}\" cy=\"${y(v).toFixed(1)}\" r=\"3.5\" fill=\"${color(si)}\"><title>${esc(s.name)} · ${esc(spec.labels[i])}: ${v}</title></circle>`));\n });\n }\n parts.push(`<line x1=\"${left}\" x2=\"${W - right}\" y1=\"${y(0).toFixed(1)}\" y2=\"${y(0).toFixed(1)}\" stroke=\"currentColor\" stroke-opacity=\".5\"/>`);\n if (legendH) parts.push(legend(spec.series.map((s) => s.name)));\n return `${head}${parts.join('')}</svg>`;\n}\n"],"mappings":"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"}
@@ -0,0 +1,140 @@
1
+ // src/url.ts
2
+ /** Where a link may point: http(s), mailto, tel, an in-page anchor or a same-site path. `//host` (protocol-relative) is not allowed. */
3
+ var HREF = /^(?:https?:|mailto:|tel:|#|\/(?![\/\\\t\r\n]))/i;
4
+ /** Where an image may come from: http(s), a same-site path, or a base64 raster data URL (not SVG). */
5
+ var SRC = /^(?:https?:|data:image\/(?:png|jpe?g|gif|webp|bmp|avif);base64,|\/(?![\/\\\t\r\n]))/i;
6
+ var isSafeHref = (u) => typeof u === "string" && HREF.test(u);
7
+ var isSafeSrc = (u) => typeof u === "string" && SRC.test(u);
8
+ /** Make a URL safe to place inside Markdown `(...)`: characters that end or split the destination are percent-encoded. */
9
+ var markdownUrl = (u) => u.replace(/[()\s<>\\]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`);
10
+ // src/crop.ts
11
+ /** The smallest visible share of the image on either axis (so a crop can never collapse to nothing). */
12
+ var MIN_VISIBLE = .05;
13
+ var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
14
+ var round = (v) => Math.round(v * 1e4) / 1e4;
15
+ /** Clamp a crop into a valid range; null means "no crop". Junk values are treated as 0. */
16
+ function normalizeCrop(input) {
17
+ if (!input || typeof input !== "object") return null;
18
+ const n = (v) => typeof v === "number" && Number.isFinite(v) ? clamp(v, 0, .95) : 0;
19
+ let { left, top, right, bottom } = {
20
+ left: n(input.left),
21
+ top: n(input.top),
22
+ right: n(input.right),
23
+ bottom: n(input.bottom)
24
+ };
25
+ const fit = (a, b) => {
26
+ const excess = a + b - .95;
27
+ if (excess <= 0) return [a, b];
28
+ return a >= b ? [a - excess, b] : [a, b - excess];
29
+ };
30
+ [left, right] = fit(left, right);
31
+ [top, bottom] = fit(top, bottom);
32
+ const c = {
33
+ left: round(left),
34
+ top: round(top),
35
+ right: round(right),
36
+ bottom: round(bottom)
37
+ };
38
+ return c.left || c.top || c.right || c.bottom ? c : null;
39
+ }
40
+ /** Natural size of the visible region (what a crop leaves), in natural px. */
41
+ function visibleSize(crop, nat) {
42
+ const c = crop ?? {
43
+ left: 0,
44
+ top: 0,
45
+ right: 0,
46
+ bottom: 0
47
+ };
48
+ return {
49
+ w: nat.w * (1 - c.left - c.right),
50
+ h: nat.h * (1 - c.top - c.bottom)
51
+ };
52
+ }
53
+ /** Lay out a crop so the visible box is `displayWidth` px wide. */
54
+ function cropLayout(crop, nat, displayWidth) {
55
+ const vw = 1 - crop.left - crop.right;
56
+ const vh = 1 - crop.top - crop.bottom;
57
+ const width = displayWidth;
58
+ const height = width * (vh * nat.h) / (vw * nat.w);
59
+ const imgWidth = width / vw;
60
+ const imgHeight = height / vh;
61
+ return {
62
+ width,
63
+ height,
64
+ imgWidth,
65
+ imgHeight,
66
+ offsetX: -crop.left * imgWidth,
67
+ offsetY: -crop.top * imgHeight
68
+ };
69
+ }
70
+ /** Serialize as "left,top,right,bottom" for a data attribute. */
71
+ var cropToString = (c) => `${c.left},${c.top},${c.right},${c.bottom}`;
72
+ function cropFromString(s) {
73
+ if (!s) return null;
74
+ const p = s.split(",").map(Number);
75
+ if (p.length !== 4 || p.some((x) => !Number.isFinite(x) || x < 0 || x >= 1)) return null;
76
+ return normalizeCrop({
77
+ left: p[0],
78
+ top: p[1],
79
+ right: p[2],
80
+ bottom: p[3]
81
+ });
82
+ }
83
+ /**
84
+ * Apply a drag of (dx, dy) fractions of the full image to a crop rectangle. Edges move independently,
85
+ * `move` slides the whole rectangle without resizing it. Result is always valid.
86
+ */
87
+ function dragCrop(start, handle, dx, dy) {
88
+ let { left, top, right, bottom } = start;
89
+ if (handle === "move") {
90
+ const w = 1 - left - right;
91
+ const h = 1 - top - bottom;
92
+ left = clamp(left + dx, 0, 1 - w);
93
+ top = clamp(top + dy, 0, 1 - h);
94
+ right = 1 - w - left;
95
+ bottom = 1 - h - top;
96
+ } else {
97
+ if (handle.includes("w")) left = clamp(left + dx, 0, 1 - right - MIN_VISIBLE);
98
+ if (handle.includes("e")) right = clamp(right - dx, 0, 1 - left - MIN_VISIBLE);
99
+ if (handle.includes("n")) top = clamp(top + dy, 0, 1 - bottom - MIN_VISIBLE);
100
+ if (handle.includes("s")) bottom = clamp(bottom - dy, 0, 1 - top - MIN_VISIBLE);
101
+ }
102
+ return {
103
+ left: round(left),
104
+ top: round(top),
105
+ right: round(right),
106
+ bottom: round(bottom)
107
+ };
108
+ }
109
+ /**
110
+ * Render the cropped region of an image to PNG bytes (browser only: needs canvas). Returns null when canvas
111
+ * is unavailable, so callers can fall back to the full image.
112
+ */
113
+ async function cropBytes(data, crop) {
114
+ try {
115
+ if (typeof createImageBitmap !== "function") return null;
116
+ const bmp = await createImageBitmap(new Blob([data]));
117
+ const sx = Math.round(bmp.width * crop.left);
118
+ const sy = Math.round(bmp.height * crop.top);
119
+ const sw = Math.max(1, Math.round(bmp.width * (1 - crop.left - crop.right)));
120
+ const sh = Math.max(1, Math.round(bmp.height * (1 - crop.top - crop.bottom)));
121
+ const canvas = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(sw, sh) : Object.assign(document.createElement("canvas"), {
122
+ width: sw,
123
+ height: sh
124
+ });
125
+ const ctx = canvas.getContext("2d");
126
+ if (!ctx) return null;
127
+ ctx.drawImage(bmp, sx, sy, sw, sh, 0, 0, sw, sh);
128
+ const blob = "convertToBlob" in canvas ? await canvas.convertToBlob({ type: "image/png" }) : await new Promise((r) => canvas.toBlob(r, "image/png"));
129
+ return blob ? {
130
+ data: new Uint8Array(await blob.arrayBuffer()),
131
+ width: sw,
132
+ height: sh
133
+ } : null;
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+ export { cropToString as a, visibleSize as c, markdownUrl as d, cropLayout as i, isSafeHref as l, cropBytes as n, dragCrop as o, cropFromString as r, normalizeCrop as s, MIN_VISIBLE as t, isSafeSrc as u };
139
+
140
+ //# sourceMappingURL=crop-oLrgb-A3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crop-oLrgb-A3.js","names":[],"sources":["../src/url.ts","../src/crop.ts"],"sourcesContent":["/** Where a link may point: http(s), mailto, tel, an in-page anchor or a same-site path. `//host` (protocol-relative) is not allowed. */\nconst HREF = /^(?:https?:|mailto:|tel:|#|\\/(?![\\/\\\\\\t\\r\\n]))/i;\n/** Where an image may come from: http(s), a same-site path, or a base64 raster data URL (not SVG). */\nconst SRC = /^(?:https?:|data:image\\/(?:png|jpe?g|gif|webp|bmp|avif);base64,|\\/(?![\\/\\\\\\t\\r\\n]))/i;\n\nexport const isSafeHref = (u: unknown): u is string => typeof u === 'string' && HREF.test(u);\nexport const isSafeSrc = (u: unknown): u is string => typeof u === 'string' && SRC.test(u);\n\n/** Make a URL safe to place inside Markdown `(...)`: characters that end or split the destination are percent-encoded. */\nexport const markdownUrl = (u: string): string => u.replace(/[()\\s<>\\\\]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0')}`);\n","/**\n * Image cropping math. A crop is the fraction of the *natural* image removed from each side, so it survives\n * resizing and is independent of how large the image is shown.\n */\nexport interface Crop { left: number; top: number; right: number; bottom: number }\nexport interface Natural { w: number; h: number }\n\n/** The smallest visible share of the image on either axis (so a crop can never collapse to nothing). */\nexport const MIN_VISIBLE = 0.05;\n\nconst clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v));\nconst round = (v: number) => Math.round(v * 10000) / 10000;\n\n/** Clamp a crop into a valid range; null means \"no crop\". Junk values are treated as 0. */\nexport function normalizeCrop(input: Partial<Crop> | null | undefined): Crop | null {\n if (!input || typeof input !== 'object') return null;\n const n = (v: unknown) => (typeof v === 'number' && Number.isFinite(v) ? clamp(v, 0, 1 - MIN_VISIBLE) : 0);\n let { left, top, right, bottom } = { left: n(input.left), top: n(input.top), right: n(input.right), bottom: n(input.bottom) };\n // Keep at least MIN_VISIBLE of each axis: shrink the larger side first.\n const fit = (a: number, b: number): [number, number] => {\n const excess = a + b - (1 - MIN_VISIBLE);\n if (excess <= 0) return [a, b];\n return a >= b ? [a - excess, b] : [a, b - excess];\n };\n [left, right] = fit(left, right);\n [top, bottom] = fit(top, bottom);\n const c = { left: round(left), top: round(top), right: round(right), bottom: round(bottom) };\n return c.left || c.top || c.right || c.bottom ? c : null;\n}\n\n/** Natural size of the visible region (what a crop leaves), in natural px. */\nexport function visibleSize(crop: Crop | null, nat: Natural): Natural {\n const c = crop ?? { left: 0, top: 0, right: 0, bottom: 0 };\n return { w: nat.w * (1 - c.left - c.right), h: nat.h * (1 - c.top - c.bottom) };\n}\n\nexport interface CropLayout {\n /** Size of the visible box. */\n width: number;\n height: number;\n /** Size and offset of the full image inside the box. */\n imgWidth: number;\n imgHeight: number;\n offsetX: number;\n offsetY: number;\n}\n\n/** Lay out a crop so the visible box is `displayWidth` px wide. */\nexport function cropLayout(crop: Crop, nat: Natural, displayWidth: number): CropLayout {\n const vw = 1 - crop.left - crop.right;\n const vh = 1 - crop.top - crop.bottom;\n const width = displayWidth;\n const height = (width * (vh * nat.h)) / (vw * nat.w);\n const imgWidth = width / vw;\n const imgHeight = height / vh;\n return { width, height, imgWidth, imgHeight, offsetX: -crop.left * imgWidth, offsetY: -crop.top * imgHeight };\n}\n\n/** Serialize as \"left,top,right,bottom\" for a data attribute. */\nexport const cropToString = (c: Crop) => `${c.left},${c.top},${c.right},${c.bottom}`;\n\nexport function cropFromString(s: string | null): Crop | null {\n if (!s) return null;\n const p = s.split(',').map(Number);\n // Data from HTML is untrusted: out-of-range values mean the markup is bogus, so reject it instead of clamping.\n if (p.length !== 4 || p.some((x) => !Number.isFinite(x) || x < 0 || x >= 1)) return null;\n return normalizeCrop({ left: p[0], top: p[1], right: p[2], bottom: p[3] });\n}\n\nexport type Handle = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw' | 'move';\n\n/**\n * Apply a drag of (dx, dy) fractions of the full image to a crop rectangle. Edges move independently,\n * `move` slides the whole rectangle without resizing it. Result is always valid.\n */\nexport function dragCrop(start: Crop, handle: Handle, dx: number, dy: number): Crop {\n let { left, top, right, bottom } = start;\n if (handle === 'move') {\n const w = 1 - left - right;\n const h = 1 - top - bottom;\n left = clamp(left + dx, 0, 1 - w);\n top = clamp(top + dy, 0, 1 - h);\n right = 1 - w - left;\n bottom = 1 - h - top;\n } else {\n if (handle.includes('w')) left = clamp(left + dx, 0, 1 - right - MIN_VISIBLE);\n if (handle.includes('e')) right = clamp(right - dx, 0, 1 - left - MIN_VISIBLE);\n if (handle.includes('n')) top = clamp(top + dy, 0, 1 - bottom - MIN_VISIBLE);\n if (handle.includes('s')) bottom = clamp(bottom - dy, 0, 1 - top - MIN_VISIBLE);\n }\n return { left: round(left), top: round(top), right: round(right), bottom: round(bottom) };\n}\n\n/**\n * Render the cropped region of an image to PNG bytes (browser only: needs canvas). Returns null when canvas\n * is unavailable, so callers can fall back to the full image.\n */\nexport async function cropBytes(data: Uint8Array, crop: Crop): Promise<{ data: Uint8Array; width: number; height: number } | null> {\n try {\n if (typeof createImageBitmap !== 'function') return null;\n const bmp = await createImageBitmap(new Blob([data as Uint8Array<ArrayBuffer>]));\n const sx = Math.round(bmp.width * crop.left);\n const sy = Math.round(bmp.height * crop.top);\n const sw = Math.max(1, Math.round(bmp.width * (1 - crop.left - crop.right)));\n const sh = Math.max(1, Math.round(bmp.height * (1 - crop.top - crop.bottom)));\n const canvas = typeof OffscreenCanvas !== 'undefined' ? new OffscreenCanvas(sw, sh) : Object.assign(document.createElement('canvas'), { width: sw, height: sh });\n const ctx = canvas.getContext('2d') as CanvasRenderingContext2D | null;\n if (!ctx) return null;\n ctx.drawImage(bmp, sx, sy, sw, sh, 0, 0, sw, sh);\n const blob: Blob | null = 'convertToBlob' in canvas ? await canvas.convertToBlob({ type: 'image/png' }) : await new Promise((r) => (canvas as HTMLCanvasElement).toBlob(r, 'image/png'));\n return blob ? { data: new Uint8Array(await blob.arrayBuffer()), width: sw, height: sh } : null;\n } catch {\n return null;\n }\n}\n"],"mappings":"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"}
@@ -1284,8 +1284,7 @@ async function inflateRaw(data, limit) {
1284
1284
  }
1285
1285
  return out;
1286
1286
  }
1287
- /** Reads the entries of a zip archive. Entries are unpacked on demand, and each one is size-checked. */
1288
- function openZip(bytes) {
1287
+ function openZip(bytes, strict) {
1289
1288
  const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1290
1289
  let end = -1;
1291
1290
  for (let i = bytes.length - 22; i >= Math.max(0, bytes.length - 22 - 65535); i--) if (v.getUint32(i, true) === 101010256) {
@@ -1294,6 +1293,7 @@ function openZip(bytes) {
1294
1293
  }
1295
1294
  if (end < 0) throw new Error("This is not a spreadsheet file.");
1296
1295
  const count = v.getUint16(end + 10, true);
1296
+ if (strict && count > strict.maxEntries) throw new Error(strict.message);
1297
1297
  let at = v.getUint32(end + 16, true);
1298
1298
  const entries = /* @__PURE__ */ new Map();
1299
1299
  let total = 0;
@@ -1303,7 +1303,12 @@ function openZip(bytes) {
1303
1303
  const nl = v.getUint16(at + 28, true), el = v.getUint16(at + 30, true), cl = v.getUint16(at + 32, true), offset = v.getUint32(at + 42, true);
1304
1304
  const name = new TextDecoder().decode(bytes.subarray(at + 46, at + 46 + nl));
1305
1305
  total += usize;
1306
- if (usize > MAX_UNZIPPED || total > MAX_UNZIPPED * 2) throw new Error("The file is too large to open.");
1306
+ if (strict) {
1307
+ if (/(^|[\\/])\.\.([\\/]|$)|^[\\/]/.test(name) || name.includes("\0")) throw new Error(strict.message);
1308
+ if (total > strict.maxTotal || usize > 1048576 && usize > Math.max(1, csize) * strict.maxRatio) throw new Error(strict.message);
1309
+ }
1310
+ if (usize > MAX_UNZIPPED && !strict) throw new Error("The file is too large to open.");
1311
+ if (total > MAX_UNZIPPED * 2 && !strict) throw new Error("The file is too large to open.");
1307
1312
  entries.set(name, {
1308
1313
  method,
1309
1314
  csize,
@@ -1314,14 +1319,20 @@ function openZip(bytes) {
1314
1319
  }
1315
1320
  return {
1316
1321
  names: [...entries.keys()],
1322
+ size: (name) => entries.get(name)?.usize ?? null,
1317
1323
  async read(name) {
1324
+ const d = await this.readBytes(name);
1325
+ return d ? new TextDecoder().decode(d) : null;
1326
+ },
1327
+ async readBytes(name) {
1318
1328
  const e = entries.get(name);
1319
1329
  if (!e) return null;
1330
+ if (e.offset + 30 > bytes.length || v.getUint32(e.offset, true) !== 67324752) throw new Error("This is not a valid zip file.");
1320
1331
  const start = e.offset + 30 + v.getUint16(e.offset + 26, true) + v.getUint16(e.offset + 28, true);
1321
1332
  const raw = bytes.subarray(start, start + e.csize);
1322
- const data = e.method === 0 ? raw : e.method === 8 ? await inflateRaw(raw, Math.min(MAX_UNZIPPED, e.usize + 1024)) : null;
1323
- if (!data) throw new Error("This spreadsheet uses a compression method that is not supported.");
1324
- return new TextDecoder().decode(data);
1333
+ const data = e.method === 0 ? raw : e.method === 8 ? await inflateRaw(raw, Math.min(strict ? strict.maxTotal : MAX_UNZIPPED, e.usize + 1024)) : null;
1334
+ if (!data) throw new Error("This file uses a compression method that is not supported.");
1335
+ return data;
1325
1336
  }
1326
1337
  };
1327
1338
  }
@@ -1438,6 +1449,6 @@ async function parseXlsx(input) {
1438
1449
  sheet: first.getAttribute("name") ?? "Sheet1"
1439
1450
  };
1440
1451
  }
1441
- export { checkFormula as S, toCSV as _, PAGE_SIZES as a, withFormulas as b, coerceCell as c, normalizeRows as d, parseCSV as f, specFromRows as g, ruleLooks as h, MAX_ROWS as i, formatCell as l, queryRows as m, COLUMN_TYPES as n, aggregate as o, parseXlsx as p, LOOKS as r, cleanSpec as s, AGGREGATES as t, groupDisplay as u, toPrintHTML as v, FORMULA_FUNCTIONS as x, toXlsx as y };
1452
+ export { FORMULA_FUNCTIONS as C, zipStore as S, specFromRows as _, PAGE_SIZES as a, toXlsx as b, coerceCell as c, normalizeRows as d, openZip as f, ruleLooks as g, queryRows as h, MAX_ROWS as i, formatCell as l, parseXlsx as m, COLUMN_TYPES as n, aggregate as o, parseCSV as p, LOOKS as r, cleanSpec as s, AGGREGATES as t, groupDisplay as u, toCSV as v, checkFormula as w, withFormulas as x, toPrintHTML as y };
1442
1453
 
1443
- //# sourceMappingURL=data-table-CgmN4xc4.js.map
1454
+ //# sourceMappingURL=data-table-C4bo747f.js.map