@svgrid/enterprise 3.0.1 → 3.0.2

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 (197) hide show
  1. package/dist/SvFormulaBar.svelte +606 -0
  2. package/dist/SvFormulaBar.svelte.d.ts +62 -0
  3. package/dist/SvPivotDesigner.svelte +65 -14
  4. package/dist/SvRibbonIcon.svelte +46 -0
  5. package/dist/SvRibbonIcon.svelte.d.ts +17 -0
  6. package/dist/SvSchemaChart.svelte +4 -2
  7. package/dist/SvSheet.svelte +4114 -0
  8. package/dist/SvSheet.svelte.d.ts +142 -0
  9. package/dist/SvSheetComment.svelte +98 -0
  10. package/dist/SvSheetComment.svelte.d.ts +22 -0
  11. package/dist/SvSheetConditionalFormat.svelte +162 -0
  12. package/dist/SvSheetConditionalFormat.svelte.d.ts +14 -0
  13. package/dist/SvSheetDataValidation.svelte +249 -0
  14. package/dist/SvSheetDataValidation.svelte.d.ts +14 -0
  15. package/dist/SvSheetFilterMenu.svelte +217 -0
  16. package/dist/SvSheetFilterMenu.svelte.d.ts +17 -0
  17. package/dist/SvSheetFindReplace.svelte +132 -0
  18. package/dist/SvSheetFindReplace.svelte.d.ts +10 -0
  19. package/dist/SvSheetFormatCells.svelte +421 -0
  20. package/dist/SvSheetFormatCells.svelte.d.ts +17 -0
  21. package/dist/SvSheetGoalSeek.svelte +144 -0
  22. package/dist/SvSheetGoalSeek.svelte.d.ts +12 -0
  23. package/dist/SvSheetInsertFunction.svelte +147 -0
  24. package/dist/SvSheetInsertFunction.svelte.d.ts +8 -0
  25. package/dist/SvSheetListPicker.svelte +89 -0
  26. package/dist/SvSheetListPicker.svelte.d.ts +16 -0
  27. package/dist/SvSheetManageRules.svelte +185 -0
  28. package/dist/SvSheetManageRules.svelte.d.ts +16 -0
  29. package/dist/SvSheetNameManager.svelte +225 -0
  30. package/dist/SvSheetNameManager.svelte.d.ts +11 -0
  31. package/dist/SvSheetPasteSpecial.svelte +105 -0
  32. package/dist/SvSheetPasteSpecial.svelte.d.ts +12 -0
  33. package/dist/SvSheetRemoveDuplicates.svelte +149 -0
  34. package/dist/SvSheetRemoveDuplicates.svelte.d.ts +12 -0
  35. package/dist/SvSheetRibbon.svelte +1201 -0
  36. package/dist/SvSheetRibbon.svelte.d.ts +34 -0
  37. package/dist/SvSheetSizeDialog.svelte +58 -0
  38. package/dist/SvSheetSizeDialog.svelte.d.ts +11 -0
  39. package/dist/SvSheetTabs.svelte +441 -0
  40. package/dist/SvSheetTabs.svelte.d.ts +36 -0
  41. package/dist/SvSheetTabs.test-harness.svelte.d.ts +17 -0
  42. package/dist/SvSheetTabs.test-harness.svelte.js +22 -0
  43. package/dist/SvSheetTextToColumns.svelte +172 -0
  44. package/dist/SvSheetTextToColumns.svelte.d.ts +13 -0
  45. package/dist/SvSheetValidationAlert.svelte +83 -0
  46. package/dist/SvSheetValidationAlert.svelte.d.ts +15 -0
  47. package/dist/cdn/svgrid-enterprise.svelte-external.js +18997 -5396
  48. package/dist/designer/assets/GridMenus-BlsvL_aU.js +8 -0
  49. package/dist/designer/assets/SvChartMenu-BYO9cYdy.css +1 -0
  50. package/dist/designer/assets/SvChartMenu-D9hEeHar.js +1 -0
  51. package/dist/designer/assets/SvDateRangeInput-BBRzrQeg.js +1 -0
  52. package/dist/designer/assets/SvDateTimePicker-DARvKUL2.js +1 -0
  53. package/dist/designer/assets/SvGridCellEditor-C7uQXYds.js +4 -0
  54. package/dist/designer/assets/SvGridChart-B8j1SSy3.js +3 -0
  55. package/dist/designer/assets/SvGridChart-BgLuPO7I.css +1 -0
  56. package/dist/designer/assets/SvGridChartBuilder-08YWWeqY.css +1 -0
  57. package/dist/designer/assets/SvGridChartBuilder-kvyIU_Rl.js +1 -0
  58. package/dist/designer/assets/SvGridChartPanel-4Pc8y5yO.css +1 -0
  59. package/dist/designer/assets/SvGridChartPanel-pT6ITc2P.js +4 -0
  60. package/dist/designer/assets/{SvGridChartView-DUc-1RUf.css → SvGridChartView-7msfnWZT.css} +1 -1
  61. package/dist/designer/assets/SvGridChartView-bg281UA1.js +1 -0
  62. package/dist/designer/assets/SvListBox-BDS66lmZ.js +110 -0
  63. package/dist/designer/assets/SvMenuList-DJ47ba1h.css +1 -0
  64. package/dist/designer/assets/SvMenuList-Dgq2D5VN.js +1 -0
  65. package/dist/designer/assets/SvTabs-DKmwb5p1.css +1 -0
  66. package/dist/designer/assets/SvTabs-UFCMTHXS.js +1 -0
  67. package/dist/designer/assets/chart-cIPUsbM5.js +1 -0
  68. package/dist/designer/assets/chart-export-B6bkQ3X8.js +11 -0
  69. package/dist/designer/assets/chart-export-pdf-BbA28-gt.js +11 -0
  70. package/dist/designer/assets/chart-panel-messages-DtTxpBUU.js +1 -0
  71. package/dist/designer/assets/chart-summary-Czu_5epb.js +1 -0
  72. package/dist/designer/assets/chart-trend-D5I5VEe8.js +1 -0
  73. package/dist/designer/assets/chart-validate-BMqM-qkF.js +1 -0
  74. package/dist/designer/assets/column-resize-BYDzR1Ve.js +1 -0
  75. package/dist/designer/assets/date-format-B8JbEtIG.js +1 -0
  76. package/dist/designer/assets/date-format-DKS-z0xS.css +1 -0
  77. package/dist/designer/assets/editor-contract-Bpoa7A5g.js +1 -0
  78. package/dist/designer/assets/focus-trap-CAreAhlU.js +1 -0
  79. package/dist/designer/assets/index-CMFgAoVV.css +1 -0
  80. package/dist/designer/assets/{index-C_a9Cj8Y.js → index-DwU7CAMm.js} +291 -290
  81. package/dist/designer/assets/list-option-BP_dWS66.js +1 -0
  82. package/dist/designer/assets/row-resize-DLktcqUK.js +1 -0
  83. package/dist/designer/assets/src-CITpcm6b.js +1 -0
  84. package/dist/designer/assets/src-EdI3Xqds.js +58 -0
  85. package/dist/designer/index.html +23 -14
  86. package/dist/export-pdf.d.ts +89 -0
  87. package/dist/export-pdf.js +130 -0
  88. package/dist/export.js +17 -10
  89. package/dist/index.d.ts +64 -1
  90. package/dist/index.js +68 -1
  91. package/dist/install.js +5 -0
  92. package/dist/node/studio.js +64 -2
  93. package/dist/pivot-chart.d.ts +25 -1
  94. package/dist/pivot-chart.js +25 -69
  95. package/dist/sheet/address.d.ts +43 -0
  96. package/dist/sheet/address.js +81 -0
  97. package/dist/sheet/ast.d.ts +94 -0
  98. package/dist/sheet/ast.js +56 -0
  99. package/dist/sheet/auto-filter.d.ts +66 -0
  100. package/dist/sheet/auto-filter.js +147 -0
  101. package/dist/sheet/autocomplete.d.ts +42 -0
  102. package/dist/sheet/autocomplete.js +162 -0
  103. package/dist/sheet/coerce.d.ts +31 -0
  104. package/dist/sheet/coerce.js +122 -0
  105. package/dist/sheet/commands.d.ts +89 -0
  106. package/dist/sheet/commands.js +230 -0
  107. package/dist/sheet/comments.d.ts +29 -0
  108. package/dist/sheet/comments.js +73 -0
  109. package/dist/sheet/conditional-formats.d.ts +144 -0
  110. package/dist/sheet/conditional-formats.js +316 -0
  111. package/dist/sheet/deps.d.ts +60 -0
  112. package/dist/sheet/deps.js +199 -0
  113. package/dist/sheet/document.d.ts +150 -0
  114. package/dist/sheet/document.js +264 -0
  115. package/dist/sheet/edit-keys.d.ts +14 -0
  116. package/dist/sheet/edit-keys.js +40 -0
  117. package/dist/sheet/entry.d.ts +31 -0
  118. package/dist/sheet/entry.js +76 -0
  119. package/dist/sheet/evaluate.d.ts +61 -0
  120. package/dist/sheet/evaluate.js +337 -0
  121. package/dist/sheet/find-replace.d.ts +62 -0
  122. package/dist/sheet/find-replace.js +152 -0
  123. package/dist/sheet/format-store.d.ts +99 -0
  124. package/dist/sheet/format-store.js +233 -0
  125. package/dist/sheet/freeze.d.ts +54 -0
  126. package/dist/sheet/freeze.js +58 -0
  127. package/dist/sheet/function-catalog.d.ts +10 -0
  128. package/dist/sheet/function-catalog.js +94 -0
  129. package/dist/sheet/functions.d.ts +28 -0
  130. package/dist/sheet/functions.js +362 -0
  131. package/dist/sheet/goal-seek.d.ts +73 -0
  132. package/dist/sheet/goal-seek.js +153 -0
  133. package/dist/sheet/index.d.ts +53 -0
  134. package/dist/sheet/index.js +53 -0
  135. package/dist/sheet/merges.d.ts +71 -0
  136. package/dist/sheet/merges.js +137 -0
  137. package/dist/sheet/names.d.ts +31 -0
  138. package/dist/sheet/names.js +87 -0
  139. package/dist/sheet/navigate.d.ts +52 -0
  140. package/dist/sheet/navigate.js +135 -0
  141. package/dist/sheet/number-format.d.ts +63 -0
  142. package/dist/sheet/number-format.js +508 -0
  143. package/dist/sheet/palette.d.ts +19 -0
  144. package/dist/sheet/palette.js +84 -0
  145. package/dist/sheet/parse.d.ts +13 -0
  146. package/dist/sheet/parse.js +108 -0
  147. package/dist/sheet/paste-special.d.ts +164 -0
  148. package/dist/sheet/paste-special.js +592 -0
  149. package/dist/sheet/protection.d.ts +21 -0
  150. package/dist/sheet/protection.js +48 -0
  151. package/dist/sheet/rects.d.ts +45 -0
  152. package/dist/sheet/rects.js +112 -0
  153. package/dist/sheet/refs.d.ts +82 -0
  154. package/dist/sheet/refs.js +266 -0
  155. package/dist/sheet/ribbon-icons.d.ts +31 -0
  156. package/dist/sheet/ribbon-icons.js +350 -0
  157. package/dist/sheet/ribbon.d.ts +173 -0
  158. package/dist/sheet/ribbon.js +753 -0
  159. package/dist/sheet/shortcuts.d.ts +164 -0
  160. package/dist/sheet/shortcuts.js +623 -0
  161. package/dist/sheet/structure.d.ts +87 -0
  162. package/dist/sheet/structure.js +166 -0
  163. package/dist/sheet/tables.d.ts +70 -0
  164. package/dist/sheet/tables.js +147 -0
  165. package/dist/sheet/tokenize.d.ts +54 -0
  166. package/dist/sheet/tokenize.js +304 -0
  167. package/dist/sheet/transforms.d.ts +76 -0
  168. package/dist/sheet/transforms.js +154 -0
  169. package/dist/sheet/validation.d.ts +92 -0
  170. package/dist/sheet/validation.js +241 -0
  171. package/dist/sheet/workbook.d.ts +72 -0
  172. package/dist/sheet/workbook.js +409 -0
  173. package/dist/sheet-enable.d.ts +6 -0
  174. package/dist/sheet-enable.js +46 -0
  175. package/dist/studio/ui-components.generated.js +63 -1
  176. package/dist/version.d.ts +1 -1
  177. package/dist/version.js +1 -1
  178. package/package.json +9 -4
  179. package/dist/designer/assets/GridMenus-B1JtIN21.js +0 -8
  180. package/dist/designer/assets/SvDateRangeInput-CjO7nn_x.js +0 -1
  181. package/dist/designer/assets/SvDateTimePicker-ByBD_ov6.js +0 -1
  182. package/dist/designer/assets/SvField-YDLipR6O.css +0 -1
  183. package/dist/designer/assets/SvField-cj1PFNgZ.js +0 -1
  184. package/dist/designer/assets/SvGridCellEditor-0W1L2hJ1.js +0 -2
  185. package/dist/designer/assets/SvGridChart-DBOR4xcz.js +0 -2
  186. package/dist/designer/assets/SvGridChart-fkMWeh1J.css +0 -1
  187. package/dist/designer/assets/SvGridChartPanel-symrhNnR.js +0 -11
  188. package/dist/designer/assets/SvGridChartView-BmRxq6mU.js +0 -1
  189. package/dist/designer/assets/SvListBox-1oUF6Iav.js +0 -110
  190. package/dist/designer/assets/chart-DXFganU8.js +0 -1
  191. package/dist/designer/assets/column-resize-C_inr5zT.js +0 -1
  192. package/dist/designer/assets/date-format-DGagAqRc.css +0 -1
  193. package/dist/designer/assets/date-format-onlYDsrY.js +0 -1
  194. package/dist/designer/assets/easing-zaypKrr0.js +0 -3
  195. package/dist/designer/assets/index-k2stW6L8.css +0 -1
  196. package/dist/designer/assets/row-resize-B0FWHcNr.js +0 -1
  197. package/dist/designer/assets/src-D9p27OGC.js +0 -58
@@ -0,0 +1,592 @@
1
+ /**
2
+ * Paste Special, and the HTML clipboard round-trip that makes it worth having.
3
+ *
4
+ * The grid's clipboard is TSV only: `writeText` out, `readText` in. That is
5
+ * fine for values and loses everything else, so a trip through Excel drops
6
+ * every format, every formula and every merge. Excel itself writes both
7
+ * `text/plain` and `text/html`, and reads the HTML back when it is there.
8
+ * Matching that is what makes the grid part of someone's workflow rather than
9
+ * an island.
10
+ *
11
+ * The HTML is a plain `<table>`, which is what Excel, Sheets, Numbers and
12
+ * every web app already understand. Formats ride on inline styles they all
13
+ * read (with Excel's `mso-number-format` beside them so Excel keeps the
14
+ * number format too), the number behind a formatted display rides in
15
+ * Excel's `x:num`, and the formula travels in a `data-formula` attribute
16
+ * they all ignore, so a paste into Excel keeps the look while a paste back
17
+ * into the grid keeps the formula.
18
+ *
19
+ * Reading goes the other way: Excel's `<style>` classes, `x:fmla` and
20
+ * `x:num`, and Sheets' `data-sheets-*` attributes all come through, so a
21
+ * block copied from either arrives with its values, formulas and formats.
22
+ */
23
+ import { translateFormula } from './refs.js';
24
+ import { parseFormula } from './parse.js';
25
+ import { visit } from './ast.js';
26
+ import { entryToStyle } from './format-store.js';
27
+ const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
28
+ /**
29
+ * A number format the way Excel writes it in `mso-number-format`: quoted,
30
+ * every literal escaped with a backslash and a double quote as `\0022`.
31
+ * Excel reads the property back from inline styles, which is how a paste
32
+ * into it keeps `#,##0.00` rather than guessing a format from the text.
33
+ */
34
+ export function msoNumberFormat(code) {
35
+ const body = code
36
+ .replace(/\\/g, '\\\\')
37
+ .replace(/"/g, '\\0022')
38
+ .replace(/([^A-Za-z0-9 \\])/g, '\\$1');
39
+ return `"${body}"`;
40
+ }
41
+ /**
42
+ * Serialise a grid to the two clipboard flavours.
43
+ *
44
+ * The marker class is how `parseClipboardHtml` knows a paste came from a grid
45
+ * and can trust the `data-formula` attributes; HTML from anywhere else is
46
+ * read for text and styles only.
47
+ */
48
+ export function buildClipboardPayload(grid,
49
+ /** Where the copy came from, so a pasted formula knows how far it moved.
50
+ * Omit for a synthetic payload; references then paste unshifted. */
51
+ origin) {
52
+ const text = grid.map((row) => row.map((cell) => cell.text).join('\t')).join('\n');
53
+ const rows = grid.map((row) => {
54
+ const cells = row.map((cell) => {
55
+ const numFmt = cell.format?.numFmt;
56
+ const style = [entryToStyle(cell.format), numFmt ? `mso-number-format:${msoNumberFormat(numFmt)}` : '']
57
+ .filter(Boolean)
58
+ .join(';');
59
+ const numeric = cell.value !== undefined && cell.value !== '' && Number.isFinite(Number(cell.value));
60
+ const attrs = [
61
+ style ? ` style="${esc(style)}"` : '',
62
+ numeric ? ` x:num="${esc(cell.value)}"` : '',
63
+ cell.formula ? ` data-formula="${esc(cell.formula)}"` : '',
64
+ numFmt ? ` data-numfmt="${esc(numFmt)}"` : '',
65
+ ].join('');
66
+ return `<td${attrs}>${esc(cell.text)}</td>`;
67
+ });
68
+ return `<tr>${cells.join('')}</tr>`;
69
+ });
70
+ const originAttr = origin
71
+ ? ` data-origin="${origin.row} ${origin.col}"`
72
+ : '';
73
+ // The x: namespace is Excel's; declaring it is what makes Excel read x:num.
74
+ const html = `<table class="svgrid-clipboard" xmlns:x="urn:schemas-microsoft-com:office:excel"${originAttr}><tbody>` +
75
+ rows.join('') + '</tbody></table>';
76
+ return { text, html };
77
+ }
78
+ const R1C1_REF = /\bR(?:\[(-?\d+)\]|(\d+))?C(?:\[(-?\d+)\]|(\d+))?(?![A-Za-z0-9_(])/g;
79
+ /** Whether a formula is written in R1C1, as Google Sheets writes them. */
80
+ export function isR1C1(formula) {
81
+ R1C1_REF.lastIndex = 0;
82
+ const outside = formula.replace(/"[^"]*"|'[^']*'/g, (m) => ' '.repeat(m.length));
83
+ return R1C1_REF.test(outside);
84
+ }
85
+ /**
86
+ * An R1C1 formula as A1, for the cell it lands in. `R[-1]C` is the cell
87
+ * above wherever the formula sits, `R2C3` is $C$2 anywhere, and a bare
88
+ * `RC` is the cell itself. Quoted strings and sheet names pass through.
89
+ */
90
+ export function r1c1ToA1(formula, row, col) {
91
+ const letters = (index) => {
92
+ let n = index + 1;
93
+ let out = '';
94
+ while (n > 0) {
95
+ const rem = (n - 1) % 26;
96
+ out = String.fromCharCode(65 + rem) + out;
97
+ n = Math.floor((n - 1) / 26);
98
+ }
99
+ return out;
100
+ };
101
+ const convert = (chunk) => chunk.replace(R1C1_REF, (m, rRel, rAbs, cRel, cAbs) => {
102
+ const r = rAbs !== undefined ? Number(rAbs) - 1 : row + Number(rRel ?? 0);
103
+ const c = cAbs !== undefined ? Number(cAbs) - 1 : col + Number(cRel ?? 0);
104
+ if (r < 0 || c < 0)
105
+ return m;
106
+ return `${cAbs !== undefined ? '$' : ''}${letters(c)}${rAbs !== undefined ? '$' : ''}${r + 1}`;
107
+ });
108
+ // Convert only the text between quotes.
109
+ let out = '';
110
+ let start = 0;
111
+ const quoted = /"[^"]*"|'[^']*'/g;
112
+ let m;
113
+ while ((m = quoted.exec(formula))) {
114
+ out += convert(formula.slice(start, m.index)) + m[0];
115
+ start = m.index + m[0].length;
116
+ }
117
+ return out + convert(formula.slice(start));
118
+ }
119
+ /** Parse a TSV clipboard into a grid of text-only cells. */
120
+ export function parseClipboardText(text) {
121
+ const lines = text.replace(/\r\n/g, '\n').split('\n');
122
+ if (lines.length > 0 && lines[lines.length - 1] === '')
123
+ lines.pop();
124
+ return lines.map((line) => line.split('\t').map((cellText) => ({ text: cellText })));
125
+ }
126
+ /**
127
+ * The declarations of a style string, last one wins as in CSS, with quoted
128
+ * values kept whole (a number format carries ';' between its sections).
129
+ * `background-color` folds into `background` so the two spellings override
130
+ * each other in order.
131
+ */
132
+ function declarationsOf(style) {
133
+ const out = new Map();
134
+ let quote = null;
135
+ let start = 0;
136
+ const take = (part) => {
137
+ const i = part.indexOf(':');
138
+ if (i < 0)
139
+ return;
140
+ let prop = part.slice(0, i).trim().toLowerCase();
141
+ if (prop === 'background-color')
142
+ prop = 'background';
143
+ if (prop)
144
+ out.set(prop, part.slice(i + 1).trim());
145
+ };
146
+ for (let i = 0; i < style.length; i += 1) {
147
+ const ch = style[i];
148
+ if (quote) {
149
+ if (ch === quote)
150
+ quote = null;
151
+ }
152
+ else if (ch === '"' || ch === "'") {
153
+ quote = ch;
154
+ }
155
+ else if (ch === ';') {
156
+ take(style.slice(start, i));
157
+ start = i + 1;
158
+ }
159
+ }
160
+ take(style.slice(start));
161
+ return out;
162
+ }
163
+ const AUTOMATIC_COLOR = /^(?:windowtext|black|#000|#000000)$/i;
164
+ const STYLE_TO_ENTRY = {
165
+ 'font-weight': (v, into) => {
166
+ if (/^(?:bold|bolder|[6-9]00)$/i.test(v))
167
+ into.bold = true;
168
+ else
169
+ delete into.bold;
170
+ },
171
+ 'font-style': (v, into) => {
172
+ if (/^(?:italic|oblique)$/i.test(v))
173
+ into.italic = true;
174
+ else
175
+ delete into.italic;
176
+ },
177
+ 'text-decoration': (v, into) => {
178
+ if (/underline/i.test(v))
179
+ into.underline = true;
180
+ else
181
+ delete into.underline;
182
+ if (/line-through/i.test(v))
183
+ into.strike = true;
184
+ else
185
+ delete into.strike;
186
+ },
187
+ color: (v, into) => {
188
+ // Excel writes the automatic colour as windowtext (or black in older
189
+ // builds); that is "no colour set", not a black override.
190
+ if (AUTOMATIC_COLOR.test(v))
191
+ delete into.color;
192
+ else
193
+ into.color = v;
194
+ },
195
+ background: (v, into) => {
196
+ if (/^(?:transparent|none|auto)$/i.test(v))
197
+ delete into.fill;
198
+ else
199
+ into.fill = v;
200
+ },
201
+ 'text-align': (v, into) => {
202
+ if (/^(?:left|center|right)$/i.test(v))
203
+ into.align = v.toLowerCase();
204
+ else
205
+ delete into.align;
206
+ },
207
+ 'font-family': (v, into) => {
208
+ // The first family, unquoted; Excel writes "Aptos Narrow", sans-serif.
209
+ const first = v.split(',')[0].trim().replace(/^["']|["']$/g, '');
210
+ if (first)
211
+ into.fontFamily = first;
212
+ },
213
+ 'font-size': (v, into) => {
214
+ const m = /^([\d.]+)(pt|px)$/i.exec(v);
215
+ if (!m)
216
+ return;
217
+ const n = Number(m[1]);
218
+ if (!Number.isFinite(n) || n <= 0)
219
+ return;
220
+ into.fontSize = m[2].toLowerCase() === 'pt' ? Math.round((n * 96) / 72) : Math.round(n);
221
+ },
222
+ 'white-space': (v, into) => {
223
+ if (/^(?:pre-wrap|normal|pre-line)$/i.test(v))
224
+ into.wrap = true;
225
+ else
226
+ delete into.wrap;
227
+ },
228
+ };
229
+ /**
230
+ * Excel's `mso-number-format` as our number format. Excel escapes the
231
+ * literal characters of a code (`0\.00`), and names a few formats rather
232
+ * than spelling them; the names map to the codes Excel shows for them.
233
+ */
234
+ const MSO_NAMED_FORMATS = {
235
+ general: undefined,
236
+ 'general number': undefined,
237
+ standard: '#,##0.00',
238
+ fixed: '0.00',
239
+ percent: '0.00%',
240
+ currency: '$#,##0.00',
241
+ scientific: '0.00E+00',
242
+ 'short date': 'm/d/yyyy',
243
+ 'medium date': 'd-mmm-yy',
244
+ 'long date': 'dddd, mmmm d, yyyy',
245
+ 'short time': 'h:mm',
246
+ 'medium time': 'h:mm AM/PM',
247
+ 'long time': 'h:mm:ss AM/PM',
248
+ '@': '@',
249
+ };
250
+ export function numFmtFromMso(value) {
251
+ const raw = value.trim().replace(/^["']|["']$/g, '');
252
+ if (!raw)
253
+ return undefined;
254
+ const named = MSO_NAMED_FORMATS[raw.toLowerCase()];
255
+ if (raw.toLowerCase() in MSO_NAMED_FORMATS)
256
+ return named;
257
+ // Excel escapes every literal (\# \, \. \@) and writes a double quote as
258
+ // \0022; our codes take the literals plain.
259
+ return raw.replace(/\\0022/g, '"').replace(/\\([^A-Za-z0-9])/g, '$1');
260
+ }
261
+ function entryFromStyle(style, numFmt) {
262
+ const into = {};
263
+ if (style) {
264
+ for (const [prop, value] of declarationsOf(style)) {
265
+ if (prop === 'mso-number-format') {
266
+ if (numFmt !== null)
267
+ continue;
268
+ const code = numFmtFromMso(value);
269
+ if (code)
270
+ into.numFmt = code;
271
+ else
272
+ delete into.numFmt;
273
+ }
274
+ else {
275
+ STYLE_TO_ENTRY[prop]?.(value, into);
276
+ }
277
+ }
278
+ }
279
+ if (numFmt)
280
+ into.numFmt = numFmt;
281
+ return Object.keys(into).length > 0 ? into : undefined;
282
+ }
283
+ /**
284
+ * The class rules of a clipboard document. Excel puts every cell's format in
285
+ * a `<style>` block keyed by `class="xl65"`, one rule per class, and only
286
+ * the odd override inline; the declarations are merged under the inline
287
+ * style so the inline one wins where both speak.
288
+ */
289
+ function classStyles(doc) {
290
+ const out = new Map();
291
+ for (const styleEl of doc.querySelectorAll('style')) {
292
+ const css = styleEl.textContent ?? '';
293
+ const rule = /([^{}]+)\{([^{}]*)\}/g;
294
+ let m;
295
+ while ((m = rule.exec(css))) {
296
+ const declarations = m[2].replace(/\s+/g, ' ').trim();
297
+ for (const selector of m[1].split(',')) {
298
+ const cls = /\.([\w-]+)\s*$/.exec(selector.trim());
299
+ if (cls)
300
+ out.set(cls[1], (out.get(cls[1]) ? out.get(cls[1]) + ';' : '') + declarations);
301
+ }
302
+ }
303
+ }
304
+ return out;
305
+ }
306
+ /** The formula a cell carries, whichever spreadsheet wrote the HTML. */
307
+ function formulaOf(td) {
308
+ const own = td.getAttribute('data-formula');
309
+ if (own)
310
+ return own;
311
+ const excel = td.getAttribute('x:fmla');
312
+ if (excel)
313
+ return excel.startsWith('=') ? excel : `=${excel}`;
314
+ const sheets = td.getAttribute('data-sheets-formula');
315
+ if (sheets)
316
+ return sheets.startsWith('=') ? sheets : `=${sheets}`;
317
+ return undefined;
318
+ }
319
+ /** The raw value behind a formatted display, when the source wrote one. */
320
+ function rawValueOf(td) {
321
+ const excel = td.getAttribute('x:num');
322
+ if (excel !== null && excel !== '' && Number.isFinite(Number(excel)))
323
+ return excel;
324
+ const sheets = td.getAttribute('data-sheets-value');
325
+ if (sheets) {
326
+ try {
327
+ const parsed = JSON.parse(sheets);
328
+ if (typeof parsed['3'] === 'number')
329
+ return String(parsed['3']);
330
+ if (typeof parsed['2'] === 'string')
331
+ return parsed['2'];
332
+ if (typeof parsed['4'] === 'boolean')
333
+ return parsed['4'] ? 'TRUE' : 'FALSE';
334
+ }
335
+ catch {
336
+ // Not JSON: the display text stands.
337
+ }
338
+ }
339
+ return undefined;
340
+ }
341
+ /** Google Sheets' number format, when the cell carries one. */
342
+ function sheetsNumFmt(td) {
343
+ const raw = td.getAttribute('data-sheets-numberformat');
344
+ if (!raw)
345
+ return null;
346
+ try {
347
+ const parsed = JSON.parse(raw);
348
+ return typeof parsed['2'] === 'string' ? parsed['2'] : null;
349
+ }
350
+ catch {
351
+ return null;
352
+ }
353
+ }
354
+ /**
355
+ * Parse clipboard HTML into a grid.
356
+ *
357
+ * Uses DOMParser rather than a regex: clipboard HTML from Excel is full of
358
+ * conditional comments, namespaced tags and unclosed elements, and a parser
359
+ * that already handles all of that ships in the browser.
360
+ *
361
+ * Returns null when the HTML holds no table, so the caller falls back to TSV.
362
+ */
363
+ export function readClipboardOrigin(html) {
364
+ if (typeof DOMParser === 'undefined')
365
+ return null;
366
+ try {
367
+ const table = new DOMParser().parseFromString(html, 'text/html').querySelector('table');
368
+ const raw = table?.getAttribute('data-origin');
369
+ if (!raw)
370
+ return null;
371
+ const [row, col] = raw.split(' ').map(Number);
372
+ return Number.isFinite(row) && Number.isFinite(col)
373
+ ? { row: row, col: col }
374
+ : null;
375
+ }
376
+ catch {
377
+ return null;
378
+ }
379
+ }
380
+ export function parseClipboardHtml(html) {
381
+ if (typeof DOMParser === 'undefined')
382
+ return null;
383
+ let doc;
384
+ try {
385
+ doc = new DOMParser().parseFromString(html, 'text/html');
386
+ }
387
+ catch {
388
+ return null;
389
+ }
390
+ const table = doc.querySelector('table');
391
+ if (!table)
392
+ return null;
393
+ const classes = classStyles(doc);
394
+ const grid = [];
395
+ for (const tr of table.querySelectorAll('tr')) {
396
+ const row = [];
397
+ for (const td of tr.querySelectorAll('td, th')) {
398
+ const formula = formulaOf(td);
399
+ const fromClasses = (td.getAttribute('class') ?? '')
400
+ .split(/\s+/)
401
+ .map((c) => classes.get(c))
402
+ .filter((c) => !!c);
403
+ const style = [...fromClasses, td.getAttribute('style') ?? ''].filter(Boolean).join(';');
404
+ const raw = rawValueOf(td);
405
+ row.push({
406
+ // textContent rather than innerText: this document is never rendered,
407
+ // so innerText would be empty. A source that wrote the raw value
408
+ // beside a formatted display (Excel's x:num, Sheets' data-sheets-value)
409
+ // pastes the value; the format carries the look.
410
+ text: raw ?? (td.textContent ?? '').replace(/ /g, ' ').trim(),
411
+ ...(formula ? { formula } : {}),
412
+ ...(() => {
413
+ const entry = entryFromStyle(style || null, td.getAttribute('data-numfmt') ?? sheetsNumFmt(td));
414
+ return entry ? { format: entry } : {};
415
+ })(),
416
+ });
417
+ }
418
+ if (row.length > 0)
419
+ grid.push(row);
420
+ }
421
+ return grid.length > 0 ? grid : null;
422
+ }
423
+ /** Read whichever flavour is richer. */
424
+ export function parseClipboard(payload) {
425
+ if (payload.html) {
426
+ const fromHtml = parseClipboardHtml(payload.html);
427
+ if (fromHtml)
428
+ return fromHtml;
429
+ }
430
+ return parseClipboardText(payload.text ?? '');
431
+ }
432
+ function transposeGrid(grid) {
433
+ const rows = grid.length;
434
+ const cols = Math.max(0, ...grid.map((r) => r.length));
435
+ const out = [];
436
+ for (let c = 0; c < cols; c += 1) {
437
+ const row = [];
438
+ for (let r = 0; r < rows; r += 1)
439
+ row.push(grid[r]?.[c] ?? { text: '' });
440
+ out.push(row);
441
+ }
442
+ return out;
443
+ }
444
+ function arithmetic(existing, incoming, op) {
445
+ const a = Number(existing);
446
+ const b = Number(incoming);
447
+ if (!Number.isFinite(a) || !Number.isFinite(b))
448
+ return null;
449
+ switch (op) {
450
+ case 'add': return a + b;
451
+ case 'subtract': return a - b;
452
+ case 'multiply': return a * b;
453
+ // Excel gives #DIV/0! here; the caller writes the error, so report null
454
+ // and let it decide rather than inventing a zero.
455
+ case 'divide': return b === 0 ? null : a / b;
456
+ default: return null;
457
+ }
458
+ }
459
+ /**
460
+ * Decide what lands in one cell. Pure, so the whole option matrix is testable
461
+ * without a clipboard or a grid.
462
+ *
463
+ * `offset` is how far the destination sits from where the source was copied,
464
+ * which is what lets a pasted formula shift its relative references. `at`
465
+ * is the destination itself, which an R1C1 formula (Google Sheets writes
466
+ * them so) needs to become A1; it then needs no offset, being relative to
467
+ * wherever it lands.
468
+ */
469
+ export function resolvePasteCell(source, existing, opts, offset = { rows: 0, cols: 0 }, at) {
470
+ if (!source)
471
+ return { kind: 'skip' };
472
+ const what = opts.what ?? 'all';
473
+ const operation = opts.operation ?? 'none';
474
+ if (opts.skipBlanks && source.text === '' && !source.formula)
475
+ return { kind: 'skip' };
476
+ if (what === 'formats')
477
+ return { kind: 'format', format: source.format };
478
+ // Arithmetic ignores formats and formulas: Excel operates on values.
479
+ if (operation !== 'none') {
480
+ const result = arithmetic(existing, source.text, operation);
481
+ return result === null ? { kind: 'skip' } : { kind: 'value', value: String(result) };
482
+ }
483
+ const wantsFormula = what === 'all' || what === 'formulas';
484
+ const value = wantsFormula && source.formula
485
+ ? at && isR1C1(source.formula)
486
+ ? r1c1ToA1(source.formula, at.row, at.col)
487
+ : String(translateFormula(source.formula, offset.rows, offset.cols))
488
+ : source.text;
489
+ if (what === 'values' || what === 'formulas')
490
+ return { kind: 'value', value };
491
+ return { kind: 'both', value, format: source.format };
492
+ }
493
+ /**
494
+ * A block from another application says nothing about where it was copied
495
+ * from, and Excel's formulas come in A1 as written: `=B2*C2` beside a
496
+ * price and a quantity. Pasted anywhere but A1 with no shift, that formula
497
+ * reads the wrong cells. What CAN be known is whether the block is
498
+ * self-contained: with the block read as if copied from A1, a formula whose
499
+ * every reference falls inside the block (a totals column, a running sum,
500
+ * the blocks people copy) is kept and moves with the paste from that A1;
501
+ * one that reaches outside is pasted as its value, which is right wherever
502
+ * the block lands. R1C1 formulas (Google Sheets) are left alone: they are
503
+ * resolved against the cell they land in.
504
+ */
505
+ export function anchorForeignFormulas(grid) {
506
+ const rows = grid.length;
507
+ const cols = Math.max(0, ...grid.map((r) => r.length));
508
+ let anchored = false;
509
+ const out = grid.map((row) => row.map((cell) => {
510
+ if (!cell.formula || isR1C1(cell.formula))
511
+ return cell;
512
+ let inside = true;
513
+ try {
514
+ visit(parseFormula(cell.formula), (n) => {
515
+ // A defined name or a table reference belongs to the source
516
+ // workbook; a sheet-qualified reference is outside by definition.
517
+ if (n.k === 'name' || n.k === 'table')
518
+ inside = false;
519
+ if (n.k === 'ref') {
520
+ if (n.ref.sheet || n.ref.row === null || n.ref.row >= rows || n.ref.col >= cols)
521
+ inside = false;
522
+ }
523
+ if (n.k === 'range') {
524
+ if (n.from.sheet || n.to.sheet)
525
+ inside = false;
526
+ const r2 = Math.max(n.from.row ?? rows, n.to.row ?? rows);
527
+ if (r2 >= rows || Math.max(n.from.col, n.to.col) >= cols)
528
+ inside = false;
529
+ }
530
+ });
531
+ }
532
+ catch {
533
+ inside = false;
534
+ }
535
+ if (!inside) {
536
+ const { formula: _dropped, ...rest } = cell;
537
+ return rest;
538
+ }
539
+ anchored = true;
540
+ return cell;
541
+ }));
542
+ return { grid: out, origin: anchored ? { row: 0, col: 0 } : null };
543
+ }
544
+ /**
545
+ * Where each source cell lands.
546
+ *
547
+ * The reference offset is the same for every cell, not per-cell: source and
548
+ * destination both advance together as the grid is walked, so the shift is
549
+ * just `destination - origin`. Computing it per cell is how you end up
550
+ * translating by the destination's absolute position, which is wrong
551
+ * everywhere except the top-left corner.
552
+ *
553
+ * With no `origin` the offset is zero. That is right rather than lazy: a
554
+ * formula pasted in from another application has no position in this sheet to
555
+ * have moved from.
556
+ *
557
+ * `fill` is the selection's size. A block that fits it a whole number of
558
+ * times is repeated over it, as Excel repeats a copied cell across a
559
+ * selected range (or two copied rows over six selected ones); each repeat
560
+ * gets its own offset, so a repeated formula points where a hand-copied one
561
+ * would. Otherwise the block lands at the destination and keeps its shape.
562
+ */
563
+ export function planPaste(grid, destination, opts = {}, origin, fill) {
564
+ const shaped = opts.transpose ? transposeGrid(grid) : grid;
565
+ const height = shaped.length;
566
+ const width = shaped.reduce((max, row) => Math.max(max, row.length), 0);
567
+ const repeats = fill && height > 0 && width > 0
568
+ && fill.rows % height === 0 && fill.cols % width === 0
569
+ && (fill.rows > height || fill.cols > width)
570
+ ? { rows: fill.rows / height, cols: fill.cols / width }
571
+ : { rows: 1, cols: 1 };
572
+ const out = [];
573
+ for (let tr = 0; tr < repeats.rows; tr += 1) {
574
+ for (let tc = 0; tc < repeats.cols; tc += 1) {
575
+ const corner = { row: destination.row + tr * height, col: destination.col + tc * width };
576
+ // A transposed paste changes the shape, so Excel does not translate either.
577
+ const offset = origin && !opts.transpose
578
+ ? { rows: corner.row - origin.row, cols: corner.col - origin.col }
579
+ : { rows: 0, cols: 0 };
580
+ for (let r = 0; r < shaped.length; r += 1) {
581
+ const row = shaped[r] ?? [];
582
+ for (let c = 0; c < row.length; c += 1) {
583
+ const source = row[c];
584
+ if (!source)
585
+ continue;
586
+ out.push({ row: corner.row + r, col: corner.col + c, source, offset });
587
+ }
588
+ }
589
+ }
590
+ }
591
+ return out;
592
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Sheet protection, the way Excel has it: every cell is locked by default,
3
+ * Format Cells > Protection unlocks the ones that may change, and Protect
4
+ * Sheet turns the flags on. Until then a locked cell is an ordinary cell.
5
+ *
6
+ * The rules live here so the shell, the ribbon and the commands agree on
7
+ * them without each reading the format entry its own way.
8
+ */
9
+ import type { CellAddressLookup, CellFormatEntry, Rect, SheetFormatStore } from './format-store.js';
10
+ /** What the sheet says when a protected cell is asked to change. */
11
+ export declare const PROTECTED_MESSAGE = "The cell you're trying to change is on a protected sheet. To make a change, unprotect the sheet.";
12
+ /** A cell is locked unless its format says otherwise. */
13
+ export declare function isLocked(entry: CellFormatEntry | undefined): boolean;
14
+ /**
15
+ * Whether any cell of the rectangles is locked. A protected sheet refuses
16
+ * a change that touches one locked cell rather than doing the rest, which
17
+ * is what Excel does and what keeps an undo step whole.
18
+ */
19
+ export declare function rectsHaveLocked(store: SheetFormatStore, lookup: CellAddressLookup, rects: ReadonlyArray<Rect>): boolean;
20
+ /** Whether the rectangles hold both locked and unlocked cells. */
21
+ export declare function rectsMixLocked(store: SheetFormatStore, lookup: CellAddressLookup, rects: ReadonlyArray<Rect>): boolean;
@@ -0,0 +1,48 @@
1
+ /** What the sheet says when a protected cell is asked to change. */
2
+ export const PROTECTED_MESSAGE = "The cell you're trying to change is on a protected sheet. To make a change, unprotect the sheet.";
3
+ /** A cell is locked unless its format says otherwise. */
4
+ export function isLocked(entry) {
5
+ return entry?.locked !== false;
6
+ }
7
+ /**
8
+ * Whether any cell of the rectangles is locked. A protected sheet refuses
9
+ * a change that touches one locked cell rather than doing the rest, which
10
+ * is what Excel does and what keeps an undo step whole.
11
+ */
12
+ export function rectsHaveLocked(store, lookup, rects) {
13
+ for (const [minRow, minCol, maxRow, maxCol] of rects) {
14
+ for (let r = minRow; r <= maxRow; r += 1) {
15
+ for (let c = minCol; c <= maxCol; c += 1) {
16
+ const rowId = lookup.rowIdAt(r);
17
+ const columnId = lookup.columnIdAt(c);
18
+ if (rowId == null || columnId == null)
19
+ continue;
20
+ if (isLocked(store.get(rowId, columnId)))
21
+ return true;
22
+ }
23
+ }
24
+ }
25
+ return false;
26
+ }
27
+ /** Whether the rectangles hold both locked and unlocked cells. */
28
+ export function rectsMixLocked(store, lookup, rects) {
29
+ let locked = false;
30
+ let unlocked = false;
31
+ for (const [minRow, minCol, maxRow, maxCol] of rects) {
32
+ for (let r = minRow; r <= maxRow; r += 1) {
33
+ for (let c = minCol; c <= maxCol; c += 1) {
34
+ const rowId = lookup.rowIdAt(r);
35
+ const columnId = lookup.columnIdAt(c);
36
+ if (rowId == null || columnId == null)
37
+ continue;
38
+ if (isLocked(store.get(rowId, columnId)))
39
+ locked = true;
40
+ else
41
+ unlocked = true;
42
+ if (locked && unlocked)
43
+ return true;
44
+ }
45
+ }
46
+ }
47
+ return false;
48
+ }