mulmoclaude 2.7.0 → 2.8.1

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 (77) hide show
  1. package/client/assets/Graph-ChAxkn9o.js +1 -0
  2. package/client/assets/dist-Bij44LdC.js +2 -0
  3. package/client/assets/echarts-GV3U9sVf.js +1 -0
  4. package/client/assets/echarts-gl-BXx4On0Y.js +4282 -0
  5. package/client/assets/echarts-wordcloud-_9uQUAfp.js +1 -0
  6. package/client/assets/emscripten-module-Q67P5WYC-BU3Hlttz.js +2160 -0
  7. package/client/assets/ffi-4V2b7xu3.js +1 -0
  8. package/client/assets/{index-lkpgbKtk.js → index-D6xzCZhP.js} +600 -589
  9. package/client/assets/index-DzMsXDnj.css +2 -0
  10. package/client/assets/{install-wxcddUsO.js → install-C1moNcrn.js} +1 -1
  11. package/client/assets/{marp-DcTywifN.js → marp-Du5Sym5b.js} +1 -1
  12. package/client/assets/module-ES6BEMUI-CDTIpMiA.js +6 -0
  13. package/client/index.html +4 -3
  14. package/package.json +5 -4
  15. package/server/api/routes/plugins.ts +8 -59
  16. package/server/build/mcp-server.mjs +609 -608
  17. package/server/index.ts +1 -0
  18. package/server/plugins/confined-file-ops.ts +136 -0
  19. package/server/plugins/runtime.ts +8 -115
  20. package/server/plugins/spreadsheet-builtin.ts +26 -0
  21. package/server/plugins/spreadsheet-save-outcome.ts +7 -0
  22. package/server/utils/files/store-resolvers.ts +1 -1
  23. package/src/lang/de.ts +0 -20
  24. package/src/lang/en.ts +0 -20
  25. package/src/lang/es.ts +0 -20
  26. package/src/lang/fr.ts +0 -20
  27. package/src/lang/ja.ts +0 -20
  28. package/src/lang/ko.ts +0 -20
  29. package/src/lang/pt-BR.ts +0 -20
  30. package/src/lang/zh.ts +0 -20
  31. package/src/plugins/spreadsheet/definition.ts +9 -127
  32. package/src/plugins/spreadsheet/index.ts +7 -4
  33. package/src/plugins/spreadsheet/meta.ts +2 -4
  34. package/client/assets/echarts-gl-Cq76Z6DA.js +0 -4282
  35. package/client/assets/index-Cz7JLE0k.css +0 -2
  36. package/server/utils/files/spreadsheet-store.ts +0 -26
  37. package/src/plugins/spreadsheet/Preview.vue +0 -31
  38. package/src/plugins/spreadsheet/View.vue +0 -1007
  39. package/src/plugins/spreadsheet/cellHighlights.ts +0 -78
  40. package/src/plugins/spreadsheet/engine/calculator.ts +0 -427
  41. package/src/plugins/spreadsheet/engine/cellBuilder.ts +0 -89
  42. package/src/plugins/spreadsheet/engine/cellEmpty.ts +0 -26
  43. package/src/plugins/spreadsheet/engine/cellFormatting.ts +0 -59
  44. package/src/plugins/spreadsheet/engine/coerce-boolean.ts +0 -26
  45. package/src/plugins/spreadsheet/engine/condition.ts +0 -182
  46. package/src/plugins/spreadsheet/engine/date-locale.ts +0 -47
  47. package/src/plugins/spreadsheet/engine/date-parser.ts +0 -234
  48. package/src/plugins/spreadsheet/engine/date-utils.ts +0 -73
  49. package/src/plugins/spreadsheet/engine/datedif.ts +0 -93
  50. package/src/plugins/spreadsheet/engine/engine.ts +0 -177
  51. package/src/plugins/spreadsheet/engine/evaluator.ts +0 -494
  52. package/src/plugins/spreadsheet/engine/financial-math.ts +0 -114
  53. package/src/plugins/spreadsheet/engine/formatter.ts +0 -158
  54. package/src/plugins/spreadsheet/engine/formulaError.ts +0 -67
  55. package/src/plugins/spreadsheet/engine/formulaRefs.ts +0 -202
  56. package/src/plugins/spreadsheet/engine/functions/date.ts +0 -212
  57. package/src/plugins/spreadsheet/engine/functions/financial.ts +0 -244
  58. package/src/plugins/spreadsheet/engine/functions/index.ts +0 -16
  59. package/src/plugins/spreadsheet/engine/functions/logical.ts +0 -221
  60. package/src/plugins/spreadsheet/engine/functions/lookup-math.ts +0 -28
  61. package/src/plugins/spreadsheet/engine/functions/lookup.ts +0 -257
  62. package/src/plugins/spreadsheet/engine/functions/mathematical.ts +0 -283
  63. package/src/plugins/spreadsheet/engine/functions/statistical-math.ts +0 -91
  64. package/src/plugins/spreadsheet/engine/functions/statistical.ts +0 -290
  65. package/src/plugins/spreadsheet/engine/functions/text.ts +0 -398
  66. package/src/plugins/spreadsheet/engine/index.ts +0 -34
  67. package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +0 -111
  68. package/src/plugins/spreadsheet/engine/math-ops.ts +0 -98
  69. package/src/plugins/spreadsheet/engine/numericCoercion.ts +0 -61
  70. package/src/plugins/spreadsheet/engine/parser.ts +0 -40
  71. package/src/plugins/spreadsheet/engine/registry.ts +0 -201
  72. package/src/plugins/spreadsheet/engine/responseDecoder.ts +0 -68
  73. package/src/plugins/spreadsheet/engine/spreadsheet-errors.ts +0 -82
  74. package/src/plugins/spreadsheet/engine/textFormat.ts +0 -108
  75. package/src/plugins/spreadsheet/engine/translateFormula.ts +0 -69
  76. package/src/plugins/spreadsheet/engine/types.ts +0 -81
  77. package/src/plugins/spreadsheet/keyboardNav.ts +0 -38
@@ -1,78 +0,0 @@
1
- /**
2
- * DOM helpers for the mini-editor cell-highlight pass. Extracted from
3
- * the post-flush watch in `src/plugins/spreadsheet/View.vue`, which
4
- * had a cognitive complexity of 30 driven by four levels of nested
5
- * optional chaining + loops.
6
- *
7
- * These helpers are side-effectful by nature (they add/remove CSS
8
- * classes on DOM nodes) but each one is small enough that its
9
- * behaviour is obvious. Unit-testable with a minimal mock DOM; see
10
- * `test/plugins/spreadsheet/test_cellHighlights.ts`.
11
- */
12
-
13
- /** Minimal DOM surface the helpers need. Defined here so tests can
14
- * pass plain objects without pulling in jsdom. */
15
- export interface HighlightableElement {
16
- classList: { add: (cls: string) => void; remove: (cls: string) => void };
17
- }
18
-
19
- export interface HighlightableRow {
20
- querySelectorAll: (selector: string) => ArrayLike<HighlightableElement>;
21
- }
22
-
23
- export interface HighlightableTable {
24
- querySelectorAll: (selector: string) => ArrayLike<HighlightableRow>;
25
- }
26
-
27
- export interface HighlightableContainer {
28
- // Overload: the spreadsheet root container is known to return a
29
- // table when asked for the table id, so callers can keep the
30
- // result strongly typed without casting.
31
- querySelector: ((selector: "#spreadsheet-table") => HighlightableTable | null) & ((selector: string) => HighlightableElement | null);
32
- querySelectorAll: (selector: string) => ArrayLike<HighlightableElement> & Iterable<HighlightableElement>;
33
- }
34
-
35
- export interface CellCoord {
36
- row: number;
37
- col: number;
38
- }
39
-
40
- const CELL_EDITING = "cell-editing";
41
- const CELL_REFERENCED = "cell-referenced";
42
-
43
- /** Remove both kinds of highlight classes from the container. */
44
- export function clearCellHighlights(container: HighlightableContainer | null | undefined): void {
45
- if (!container) return;
46
- container.querySelector(`.${CELL_EDITING}`)?.classList.remove(CELL_EDITING);
47
- for (const cell of container.querySelectorAll(`.${CELL_REFERENCED}`)) {
48
- cell.classList.remove(CELL_REFERENCED);
49
- }
50
- }
51
-
52
- /** Add `className` to the <td> at (row, col) of the given table.
53
- * No-op if the row or cell doesn't exist. */
54
- export function highlightCell(table: HighlightableTable | null | undefined, coord: CellCoord, className: string): void {
55
- if (!table) return;
56
- const rows = table.querySelectorAll("tr");
57
- const row = rows[coord.row];
58
- if (!row) return;
59
- const cells = row.querySelectorAll("td");
60
- const cell = cells[coord.col];
61
- if (!cell) return;
62
- cell.classList.add(className);
63
- }
64
-
65
- /** Apply the editing cell + referenced cells highlights. Looks up
66
- * the #spreadsheet-table inside the container and no-ops if the
67
- * table hasn't rendered yet. */
68
- export function applyCellHighlights(
69
- container: HighlightableContainer | null | undefined,
70
- editingCell: CellCoord | null,
71
- references: readonly CellCoord[],
72
- ): void {
73
- if (!container) return;
74
- const table = container.querySelector("#spreadsheet-table");
75
- if (!table) return;
76
- if (editingCell) highlightCell(table, editingCell, CELL_EDITING);
77
- for (const ref of references) highlightCell(table, ref, CELL_REFERENCED);
78
- }
@@ -1,427 +0,0 @@
1
- /**
2
- * Spreadsheet Calculator
3
- *
4
- * Core calculation engine with circular reference detection and cross-sheet support
5
- */
6
-
7
- import { formatCellForDisplay } from "./cellFormatting";
8
- import { evaluateFormula as evaluateFormulaFn } from "./evaluator";
9
- import { expandRangeOrCell, parseSingleCellRef } from "./formulaRefs";
10
- import { parseDate, getDefaultDateFormat } from "./date-parser";
11
- import type { SheetData, CellValue, CalculatedSheet, CalculationError, FormulaInfo, SpreadsheetCell, CalculateOptions } from "./types";
12
- import { isObj } from "../../../utils/types";
13
- import { isEmptyCell } from "./cellEmpty";
14
- import { errorMessage } from "../../../utils/errors";
15
- import { classifyThrownError, invalidRefError } from "./formulaError";
16
- import { isSpreadsheetErrorValue, spreadsheetError } from "./spreadsheet-errors";
17
-
18
- // The grid a reference should read from, plus the sheet-name-stripped ref and
19
- // whether it points at the sheet currently being calculated (which decides
20
- // whether recursive formula evaluation is allowed for the cell it lands on).
21
- type ResolvedSheetRef = { sheetData: (SpreadsheetCell | CellValue)[][]; ref: string; isCurrentSheet: boolean };
22
-
23
- // Where a cell sits in the grid CURRENTLY being calculated, carrying the row it
24
- // belongs to so a formula's result is written back without re-indexing. Only a
25
- // same-sheet cell has one: a cross-sheet read is resolved by that sheet's own
26
- // calculateSheet pass, so it has no position here to recurse from.
27
- type CellPosition = { cells: any[]; row: number; col: number };
28
-
29
- /**
30
- * Normalize malformed data structures
31
- * Some models generate flat arrays instead of 2D arrays - fix them
32
- *
33
- * @param data - Potentially malformed sheet data
34
- * @returns Normalized 2D array
35
- */
36
- export function normalizeData(data: any): SpreadsheetCell[][] {
37
- // Handle null/undefined
38
- if (!data) {
39
- return [];
40
- }
41
-
42
- // If not an array, wrap in array
43
- if (!Array.isArray(data)) {
44
- return [];
45
- }
46
-
47
- // Empty array
48
- if (data.length === 0) {
49
- return [];
50
- }
51
-
52
- // If data is already a 2D array, return as-is
53
- if (Array.isArray(data[0])) {
54
- return data as SpreadsheetCell[][];
55
- }
56
-
57
- // If data is a flat array of cell objects, convert to 2D by pairing cells
58
- // Pattern: [cell1, cell2, cell3, cell4] -> [[cell1, cell2], [cell3, cell4]]
59
- // This handles the case where models output flat arrays instead of rows
60
- if (isObj(data[0])) {
61
- const rows: SpreadsheetCell[][] = [];
62
- for (let i = 0; i < data.length; i += 2) {
63
- const row = [data[i]];
64
- if (i + 1 < data.length) {
65
- row.push(data[i + 1]);
66
- }
67
- rows.push(row);
68
- }
69
- return rows;
70
- }
71
-
72
- // Unknown structure - return empty
73
- console.warn("Unknown data structure in spreadsheet, returning empty:", data);
74
- return [];
75
- }
76
-
77
- /**
78
- * Pre-process sheet data to parse date strings into serial numbers
79
- *
80
- * @param data - Raw sheet data
81
- * @returns Processed data with dates converted to serial numbers
82
- */
83
- function preprocessDates(data: SpreadsheetCell[][], preferDDMMYYYY: boolean): SpreadsheetCell[][] {
84
- return data.map((row) =>
85
- row.map((cell) => {
86
- // Skip if not a cell object or if it has a formula
87
- if (!isObj(cell) || !("v" in cell)) {
88
- return cell;
89
- }
90
-
91
- const value = cell.v;
92
-
93
- // Only parse strings that aren't formulas
94
- if (typeof value === "string" && !value.startsWith("=")) {
95
- const dateSerial = parseDate(value, preferDDMMYYYY);
96
-
97
- if (dateSerial !== null) {
98
- // It's a date! Convert to serial number
99
- return {
100
- v: dateSerial,
101
- f: cell.f || getDefaultDateFormat(value, preferDDMMYYYY), // Use existing format or detect from input
102
- };
103
- }
104
- }
105
-
106
- // Not a date, return as-is
107
- return cell;
108
- }),
109
- );
110
- }
111
-
112
- // `skipFormatting` is internal, not a public knob: a cross-sheet reference
113
- // computes its target sheet only to READ values, so the display-formatting pass
114
- // is skipped there — a date must stay a serial, not become "03/04/2025" that a
115
- // downstream parseFloat reads as 3 (issue #2332).
116
- type SheetCalculateOptions = CalculateOptions & { skipFormatting?: boolean };
117
-
118
- /**
119
- * Calculate formulas in a single sheet
120
- *
121
- * @param sheet - Sheet data to calculate
122
- * @param allSheets - All sheets for cross-sheet references
123
- * @returns Calculated sheet with formulas evaluated
124
- */
125
- export function calculateSheet(sheet: SheetData, allSheets?: SheetData[], options: SheetCalculateOptions = {}): CalculatedSheet {
126
- const preferDDMMYYYY = options.preferDDMMYYYY ?? false;
127
- const skipFormatting = options.skipFormatting ?? false;
128
- // Normalize malformed data structures first
129
- const normalizedData = normalizeData(sheet.data);
130
-
131
- // Pre-process dates before calculation
132
- const processedData = preprocessDates(normalizedData, preferDDMMYYYY);
133
-
134
- // Also preprocess all sheets if provided
135
- const processedAllSheets = allSheets?.map((s) => ({
136
- ...s,
137
- data: preprocessDates(normalizeData(s.data), preferDDMMYYYY),
138
- }));
139
-
140
- const data = processedData;
141
- const sheetName = sheet.name;
142
- // Cache stores either SpreadsheetCell[][] (before calculation) or CellValue[][] (after)
143
- const sheetsCache = new Map<string, (SpreadsheetCell | CellValue)[][]>();
144
- const errors: CalculationError[] = [];
145
- const formulas: FormulaInfo[] = [];
146
-
147
- // Create a copy of the data with calculated values
148
- const calculated: any[][] = data.map((row) => [...row]);
149
-
150
- // Add current sheet to cache to prevent infinite loops
151
- sheetsCache.set(sheetName, calculated);
152
-
153
- // Track cells being calculated to detect circular references
154
- const calculating = new Set<string>();
155
- // Cells whose result is already stored in `calculated`, of ANY type. A number
156
- // check alone missed string/error results, so a cell referenced before the
157
- // top loop reached it was re-evaluated (and, once formulas can throw, would
158
- // re-emit its error). Membership here means "read the cached value, do not
159
- // re-run".
160
- const evaluated = new Set<string>();
161
-
162
- // Evaluate one formula cell, guarding circular references, caching the result,
163
- // and turning a thrown failure into a typed errors[] entry plus the Excel
164
- // error value in the cell — never a swallowed bare string/number (#2359).
165
- const resolveFormulaCell = (formulaText: string, { cells, row, col }: CellPosition): CellValue => {
166
- const cellKey = `${row},${col}`;
167
- if (calculating.has(cellKey)) {
168
- errors.push({ cell: { row, col }, formula: formulaText, error: "Circular reference detected", type: "circular" });
169
- return 0;
170
- }
171
- if (evaluated.has(cellKey)) return cells[col];
172
- calculating.add(cellKey);
173
- try {
174
- const result = evaluateFormula(formulaText.substring(1)); // drop leading "="
175
- cells[col] = result;
176
- return result;
177
- } catch (error) {
178
- const { type, display } = classifyThrownError(error);
179
- errors.push({ cell: { row, col }, formula: formulaText, error: errorMessage(error), type });
180
- // The error VALUE, not its text: a cell that reads this one must see a
181
- // real error, and the display pass renders it back to `#DIV/0!`.
182
- const errorValue = spreadsheetError(display);
183
- cells[col] = errorValue;
184
- return errorValue;
185
- } finally {
186
- calculating.delete(cellKey);
187
- evaluated.add(cellKey);
188
- }
189
- };
190
-
191
- // Helper to extract raw value from cell with recursive formula evaluation
192
- const getRawValue = (cell: any, position?: CellPosition): CellValue => {
193
- // Handle null/undefined cells - treat as 0
194
- if (cell === null || cell === undefined) return 0;
195
-
196
- // An already-calculated cell can hold a formula error; it stays an error.
197
- if (isSpreadsheetErrorValue(cell)) return cell;
198
-
199
- if (typeof cell === "number") return cell;
200
-
201
- // Handle string values (for legacy or calculated cells)
202
- if (typeof cell === "string") {
203
- // Handle empty strings as 0
204
- if (cell.trim() === "") return 0;
205
-
206
- // Handle percentage strings like "5%" or "0.4167%"
207
- if (cell.includes("%")) {
208
- const numericPart = cell.replace("%", "").trim();
209
- const value = parseFloat(numericPart);
210
- return isNaN(value) ? 0 : value / 100;
211
- }
212
- // Handle currency strings like "$1,000" or "$1,000.00"
213
- if (cell.includes("$")) {
214
- const numericPart = cell.replace(/[$,]/g, "").trim();
215
- const value = parseFloat(numericPart);
216
- return isNaN(value) ? 0 : value;
217
- }
218
- // Handle comma-separated numbers like "1,000"
219
- if (cell.includes(",")) {
220
- const numericPart = cell.replace(/,/g, "").trim();
221
- const value = parseFloat(numericPart);
222
- return isNaN(value) ? 0 : value;
223
- }
224
- // Handle regular numeric strings, but preserve non-numeric strings
225
- const num = parseFloat(cell);
226
- return isNaN(num) ? cell : num;
227
- }
228
-
229
- // Handle new cell format {v, f}
230
- if (isObj(cell) && "v" in cell) {
231
- const value = cell.v;
232
- // If value is a string starting with "=", it's a formula
233
- if (typeof value === "string" && value.startsWith("=")) {
234
- // Only evaluatable when we know the cell's position (for recursion +
235
- // circular tracking); otherwise treat as 0.
236
- return position ? resolveFormulaCell(value, position) : 0;
237
- }
238
- // Try to parse as number, but preserve original type on failure
239
- if (typeof value === "number") return value;
240
- if (typeof value === "boolean") return value;
241
- if (typeof value === "string") {
242
- const num = parseFloat(value);
243
- return isNaN(num) ? value : num;
244
- }
245
- return String(value);
246
- }
247
-
248
- // Try to parse cell as number, but preserve strings
249
- const num = parseFloat(cell);
250
- return isNaN(num) ? cell : num;
251
- };
252
-
253
- // Resolve a possibly cross-sheet reference to the grid it reads from, plus the
254
- // sheet-name-stripped ref. A same-sheet ref returns the current `calculated`
255
- // grid; a cross-sheet ref computes and caches its target sheet. null means the
256
- // named sheet does not exist — the caller picks the terminal action (#REF! for
257
- // a single cell, [] for a range). The two-stage cache seed (raw copy published
258
- // BEFORE recursing, real result after) is the cross-sheet infinite-loop guard.
259
- const resolveSheetData = (fullRef: string): ResolvedSheetRef | null => {
260
- const sheetMatch = fullRef.match(/^(?:'([^']+)'|([^!]+))!(.+)$/);
261
- if (!sheetMatch) return { sheetData: calculated, ref: fullRef, isCurrentSheet: true };
262
-
263
- // Exactly one of the two name branches participates; the reference part
264
- // always does. A shortfall would mean the pattern and this read disagree, so
265
- // it lands on the caller's "sheet not found" path rather than a guess.
266
- const [, quotedName, plainName, innerRef] = sheetMatch;
267
- const targetSheetName = quotedName ?? plainName;
268
- if (targetSheetName === undefined || innerRef === undefined) return null;
269
-
270
- // Check cache first to prevent infinite loops
271
- const cached = sheetsCache.get(targetSheetName);
272
- if (cached) return { sheetData: cached, ref: innerRef, isCurrentSheet: false };
273
-
274
- const targetSheet = processedAllSheets?.find((s) => s.name === targetSheetName);
275
- if (!targetSheet || !targetSheet.data) return null;
276
-
277
- // Seed the cache with a raw copy BEFORE recursing so a cyclic back-reference
278
- // finds this sheet mid-flight, then overwrite it with the calculated result.
279
- // Resolve cross-sheet values RAW (skip display formatting) so a date cell
280
- // reads as its serial, not the presentation string "03/04/2025".
281
- const targetCalculated = targetSheet.data.map((row) => [...row]);
282
- sheetsCache.set(targetSheetName, targetCalculated);
283
- const targetResult = calculateSheet(targetSheet, processedAllSheets, { preferDDMMYYYY, skipFormatting: true });
284
- sheetsCache.set(targetSheetName, targetResult.data);
285
- return { sheetData: targetResult.data, ref: innerRef, isCurrentSheet: false };
286
- };
287
-
288
- // Helper to get cell value by reference (e.g., "B2", "$B$2", or "'Sheet1'!B2")
289
- const getCellValue = (ref: string): CellValue => {
290
- const resolved = resolveSheetData(ref);
291
- if (!resolved) throw invalidRefError(ref); // Sheet not found → #REF!
292
- const { sheetData, ref: cellRef, isCurrentSheet } = resolved;
293
-
294
- // `$` symbols and the A1 shape are parsed by the shared single-cell reader.
295
- const coord = parseSingleCellRef(cellRef);
296
- if (!coord) return 0;
297
-
298
- const { row, col } = coord;
299
- const gridRow = row >= 0 ? sheetData[row] : undefined;
300
- if (!gridRow || col < 0 || col >= gridRow.length) return 0;
301
-
302
- // Pass the position only if this is the current sheet (for recursive evaluation)
303
- return getRawValue(gridRow[col], isCurrentSheet ? { cells: gridRow, row, col } : undefined);
304
- };
305
-
306
- const collectRangeValues = (range: string, options: { numericOnly: boolean }): CellValue[] => {
307
- const resolved = resolveSheetData(range);
308
- if (!resolved) return []; // Sheet not found → empty range
309
- const { sheetData, ref: rangeRef, isCurrentSheet } = resolved;
310
-
311
- const coords = expandRangeOrCell(rangeRef);
312
- if (!coords) return [];
313
-
314
- const values: CellValue[] = [];
315
- for (const { row, col } of coords) {
316
- const gridRow = row >= 0 ? sheetData[row] : undefined;
317
- if (gridRow && col >= 0 && col < gridRow.length) {
318
- const cell = gridRow[col];
319
- // Pass the position only if current sheet (for recursive evaluation)
320
- const rawValue = getRawValue(cell, isCurrentSheet ? { cells: gridRow, row, col } : undefined);
321
-
322
- if (options.numericOnly) {
323
- // A blank cell is not a value. Dropping it from the NUMERIC list keeps
324
- // SUM unchanged (a blank read as 0) while stopping it from inflating
325
- // AVERAGE's denominator and COUNT's tally (#2358). The raw list keeps
326
- // every cell so SUMIF/AVERAGEIF's criteria and value ranges stay
327
- // row-aligned; dropping there would shift indexes and aggregate the
328
- // wrong rows (Codex review).
329
- if (!isEmptyCell(cell) && !isNaN(rawValue as number)) {
330
- values.push(rawValue);
331
- }
332
- } else {
333
- values.push(rawValue);
334
- }
335
- }
336
- }
337
- return values;
338
- };
339
-
340
- // Helper to get numeric-only range values (legacy behavior)
341
- const getRangeValues = (range: string): CellValue[] => collectRangeValues(range, { numericOnly: true });
342
-
343
- // Helper to get raw range values including text
344
- const getRangeValuesRaw = (range: string): CellValue[] => collectRangeValues(range, { numericOnly: false });
345
-
346
- // Evaluate a formula with context
347
- const evaluateFormula = (formula: string): CellValue => {
348
- return evaluateFormulaFn(formula, {
349
- getCellValue,
350
- getRangeValues,
351
- getRangeValuesRaw,
352
- evaluateFormula,
353
- preferDDMMYYYY,
354
- });
355
- };
356
-
357
- // Compute one cell into its calculated row. A cell not in {v, f} format is
358
- // left as-is (it is already a plain value).
359
- const calculateCell = (originalCell: SpreadsheetCell, position: CellPosition): void => {
360
- if (!isObj(originalCell) || !("v" in originalCell)) return;
361
- const value = originalCell.v;
362
-
363
- // A plain value is copied through, so range evaluation reads it.
364
- if (typeof value !== "string" || !value.startsWith("=")) {
365
- position.cells[position.col] = value;
366
- return;
367
- }
368
-
369
- const info: FormulaInfo = {
370
- cell: { row: position.row, col: position.col },
371
- formula: value,
372
- dependencies: [], // TODO: Extract dependencies from formula
373
- result: 0, // Will be updated below
374
- };
375
- formulas.push(info);
376
- // Route through the protected path so a thrown failure is classified into
377
- // errors[] instead of escaping this walk, and a cell already resolved via
378
- // another formula's recursion is read from cache (#2359).
379
- info.result = resolveFormulaCell(value, position);
380
- // Store result as-is (formatting will be applied at the end)
381
- position.cells[position.col] = info.result;
382
- };
383
-
384
- // Walk every cell of the grid. `calculated` is built as a row-for-row copy of
385
- // `data` and never resized, so a missing row cannot happen; skipping one keeps
386
- // the walk total rather than asserting the invariant at each cell.
387
- const forEachCell = (visit: (originalCell: SpreadsheetCell, position: CellPosition) => void): void => {
388
- data.forEach((dataRow, row) => {
389
- const cells = calculated[row];
390
- if (!cells) return;
391
- dataRow.forEach((originalCell, col) => visit(originalCell, { cells, row, col }));
392
- });
393
- };
394
-
395
- // Process all cells and calculate formulas. A cell already evaluated through
396
- // another formula's recursion keeps its number; formatting comes at the end.
397
- forEachCell((originalCell, position) => {
398
- const alreadyCalculated = typeof position.cells[position.col] === "number" && isObj(originalCell) && "f" in originalCell;
399
- if (!alreadyCalculated) calculateCell(originalCell, position);
400
- });
401
-
402
- // Final display-formatting pass: turn raw serials into presentation strings.
403
- // Skipped when this sheet is computed only to resolve a cross-sheet reference,
404
- // so the referencing cell reads the underlying value, not a display string.
405
- if (!skipFormatting) {
406
- forEachCell((originalCell, { cells, col }) => {
407
- cells[col] = formatCellForDisplay(originalCell, cells[col], preferDDMMYYYY);
408
- });
409
- }
410
-
411
- return {
412
- name: sheetName,
413
- data: calculated,
414
- formulas,
415
- errors,
416
- };
417
- }
418
-
419
- /**
420
- * Calculate all sheets in a workbook
421
- *
422
- * @param sheets - Array of sheets to calculate
423
- * @returns Array of calculated sheets
424
- */
425
- export function calculateWorkbook(sheets: SheetData[], options: CalculateOptions = {}): CalculatedSheet[] {
426
- return sheets.map((sheet) => calculateSheet(sheet, sheets, options));
427
- }
@@ -1,89 +0,0 @@
1
- /**
2
- * Build a SpreadsheetCell from the raw input captured by the mini
3
- * editor (type + value / formula / format). Extracted from
4
- * `saveMiniEditor` in `src/plugins/spreadsheet/View.vue` where it
5
- * was inlined as ~30 lines of nested if/else that pushed the
6
- * surrounding function over the cognitive-complexity threshold.
7
- *
8
- * Pure — no refs, no DOM, no side effects. Given the same inputs
9
- * it always returns the same SpreadsheetCell. Tested in
10
- * `test/plugins/spreadsheet/engine/test_cellBuilder.ts`.
11
- */
12
-
13
- import type { SpreadsheetCell } from "./types.js";
14
- import { isCallableName } from "./registry.js";
15
- // Registers the functions, so isCallableName knows LOG10 before any sheet is calculated.
16
- import "./functions/index.js";
17
-
18
- /** Inputs to the cell builder. Mirrors the mini editor refs in the
19
- * View but as plain values so unit tests don't need a Vue runtime. */
20
- export interface MiniEditorInput {
21
- /** "string" → value is stored as-is as a string.
22
- * Anything else → the `formula` field is parsed (formula / number / raw string). */
23
- type: string;
24
- /** Used when type === "string". Coerced to string. */
25
- value: unknown;
26
- /** Used when type !== "string". Trimmed before classification. */
27
- formula?: string;
28
- /** Optional format code (e.g. "$#,##0.00"). */
29
- format?: string;
30
- }
31
-
32
- // Anchored at the start of the input (after optional unary +/-) so we
33
- // only treat expressions that clearly begin with a function call as
34
- // formulas. Unanchored would match "abc FOO(" inside ordinary text.
35
- const FORMULA_FUNCTION_CALL = /^[-+]?\s*([A-Z][A-Z0-9]*)\s*\(/i;
36
-
37
- const startsWithFunctionCall = (input: string): boolean => {
38
- const name = input.match(FORMULA_FUNCTION_CALL)?.[1];
39
- return name !== undefined && isCallableName(name);
40
- };
41
-
42
- // `A1 + B2` style — cell reference next to an arithmetic operator.
43
- const FORMULA_CELL_OP = /[A-Z]+\d+\s*[+\-*/^]/;
44
-
45
- // `6/100`, `5 * 2` — arithmetic between two literal numbers.
46
- const FORMULA_NUMERIC_OP = /\d+\s*[+\-*/^]\s*\d+/;
47
-
48
- // Strict numeric literal. `parseFloat` accepts trailing junk
49
- // ("42abc" → 42) which silently corrupts user input; this anchor
50
- // ensures the ENTIRE trimmed string is a number.
51
- const STRICT_NUMBER = /^[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?$/;
52
-
53
- /**
54
- * Best-effort formula detection. The rules are conservative enough
55
- * that plain text like "hello world" stays as text, but any input
56
- * with arithmetic operators or function calls is treated as a
57
- * formula and gets the "=" prefix the engine expects.
58
- */
59
- export function looksLikeFormula(input: string): boolean {
60
- return startsWithFunctionCall(input) || FORMULA_CELL_OP.test(input) || FORMULA_NUMERIC_OP.test(input);
61
- }
62
-
63
- /**
64
- * Parse the raw (non-string-type) editor input into a cell value.
65
- * Priority: formula > number > raw string > empty string.
66
- */
67
- export function parseNonStringInput(raw: string): number | string {
68
- const input = raw.trim();
69
- if (input === "") return "";
70
- if (looksLikeFormula(input)) return `=${input}`;
71
- return STRICT_NUMBER.test(input) ? Number(input) : input;
72
- }
73
-
74
- /**
75
- * Build the full SpreadsheetCell from a mini editor input record.
76
- * String type short-circuits to `{ v: String(value) }`.
77
- * Everything else goes through parseNonStringInput for formula /
78
- * number / text classification, then optionally attaches `f`.
79
- */
80
- export function buildCellFromInput(input: MiniEditorInput): SpreadsheetCell {
81
- if (input.type === "string") {
82
- return { v: String(input.value) };
83
- }
84
- const cell: SpreadsheetCell = { v: parseNonStringInput(input.formula ?? "") };
85
- if (input.format && input.format.length > 0) {
86
- cell.f = input.format;
87
- }
88
- return cell;
89
- }
@@ -1,26 +0,0 @@
1
- /**
2
- * Telling a genuinely empty cell apart from one that holds the number 0.
3
- *
4
- * The calculator reads a blank cell as 0 for arithmetic (`=A1+1` on a blank A1
5
- * is 1, as in Excel). But an aggregate must not: `AVERAGE` divides by the count
6
- * of real values, and `COUNT` counts numbers — a blank that reads as 0 inflates
7
- * the denominator and the count. So range collection needs to skip the blanks,
8
- * which means distinguishing them from a stored 0, which this does.
9
- */
10
-
11
- import { isObj } from "../../../utils/types";
12
-
13
- /** True when a cell holds no value at all — absent, null, or an empty/whitespace
14
- * string, in either the bare or the `{ v }` form. A cell containing the number
15
- * 0, `false`, or any non-empty text is NOT empty. */
16
- export function isEmptyCell(cell: unknown): boolean {
17
- if (cell === null || cell === undefined) return true;
18
- if (typeof cell === "string") return cell.trim() === "";
19
- if (isObj(cell)) {
20
- if (!("v" in cell)) return true;
21
- const value = (cell as { v: unknown }).v;
22
- if (value === null || value === undefined) return true;
23
- return typeof value === "string" && value.trim() === "";
24
- }
25
- return false;
26
- }
@@ -1,59 +0,0 @@
1
- /**
2
- * Cell display formatting
3
- *
4
- * Turns a cell's raw calculated value into its display value (currency,
5
- * percentage, date, ...). Pure — no engine state — so cross-sheet reference
6
- * resolution can deliberately SKIP it and keep raw serial numbers, while the
7
- * final output pass applies it for presentation.
8
- */
9
-
10
- import { formatNumber } from "./formatter";
11
- import { isRecord } from "../../../utils/types";
12
- import { isSpreadsheetErrorValue } from "./spreadsheet-errors";
13
- import type { CellValue, SpreadsheetCell, StoredCellValue } from "./types";
14
-
15
- // Integer serials the engine is willing to auto-format as dates without an
16
- // explicit format code: ~Jul 1998 (36000) through ~Dec 2073 (63499). Narrow on
17
- // purpose so ordinary sums/averages are not mistaken for dates.
18
- const DATE_SERIAL_MIN = 36000;
19
- const DATE_SERIAL_MAX = 63499;
20
-
21
- const isSpreadsheetCell = (value: unknown): value is SpreadsheetCell => isRecord(value) && "v" in value;
22
-
23
- /** An integer within the date-serial window — a calculated number the engine
24
- * should display as a date when the cell carries no explicit format. */
25
- export const isLikelyDateSerial = (value: CellValue): boolean =>
26
- typeof value === "number" && Number.isInteger(value) && value >= DATE_SERIAL_MIN && value <= DATE_SERIAL_MAX;
27
-
28
- /**
29
- * Resolve the display value of one cell from its original definition and its
30
- * calculated value.
31
- *
32
- * - A formula error renders as its code, so the cell still reads `#NUM!`.
33
- * - Explicit format code wins (currency, percentage, date, ...).
34
- * - A formula that produced a date serial auto-formats as a date.
35
- * - Everything else (text, plain numbers, empty) passes through unchanged.
36
- *
37
- * The result is always a STORED value: this is the boundary where a computed
38
- * error becomes the text a cell shows and a workbook serializes.
39
- */
40
- export const formatCellForDisplay = (originalCell: unknown, calculatedValue: CellValue, preferDDMMYYYY: boolean): StoredCellValue => {
41
- if (isSpreadsheetErrorValue(calculatedValue)) {
42
- return calculatedValue.code;
43
- }
44
- if (!isSpreadsheetCell(originalCell) || typeof calculatedValue !== "number") {
45
- return calculatedValue;
46
- }
47
-
48
- const explicitFormat = typeof originalCell.f === "string" ? originalCell.f : "";
49
- if (explicitFormat) {
50
- return formatNumber(calculatedValue, explicitFormat);
51
- }
52
-
53
- const isFormula = typeof originalCell.v === "string" && originalCell.v.startsWith("=");
54
- if (isFormula && isLikelyDateSerial(calculatedValue)) {
55
- return formatNumber(calculatedValue, preferDDMMYYYY ? "DD/MM/YYYY" : "MM/DD/YYYY");
56
- }
57
-
58
- return calculatedValue;
59
- };