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.
- package/client/assets/Graph-ChAxkn9o.js +1 -0
- package/client/assets/dist-Bij44LdC.js +2 -0
- package/client/assets/echarts-GV3U9sVf.js +1 -0
- package/client/assets/echarts-gl-BXx4On0Y.js +4282 -0
- package/client/assets/echarts-wordcloud-_9uQUAfp.js +1 -0
- package/client/assets/emscripten-module-Q67P5WYC-BU3Hlttz.js +2160 -0
- package/client/assets/ffi-4V2b7xu3.js +1 -0
- package/client/assets/{index-lkpgbKtk.js → index-D6xzCZhP.js} +600 -589
- package/client/assets/index-DzMsXDnj.css +2 -0
- package/client/assets/{install-wxcddUsO.js → install-C1moNcrn.js} +1 -1
- package/client/assets/{marp-DcTywifN.js → marp-Du5Sym5b.js} +1 -1
- package/client/assets/module-ES6BEMUI-CDTIpMiA.js +6 -0
- package/client/index.html +4 -3
- package/package.json +5 -4
- package/server/api/routes/plugins.ts +8 -59
- package/server/build/mcp-server.mjs +609 -608
- package/server/index.ts +1 -0
- package/server/plugins/confined-file-ops.ts +136 -0
- package/server/plugins/runtime.ts +8 -115
- package/server/plugins/spreadsheet-builtin.ts +26 -0
- package/server/plugins/spreadsheet-save-outcome.ts +7 -0
- package/server/utils/files/store-resolvers.ts +1 -1
- package/src/lang/de.ts +0 -20
- package/src/lang/en.ts +0 -20
- package/src/lang/es.ts +0 -20
- package/src/lang/fr.ts +0 -20
- package/src/lang/ja.ts +0 -20
- package/src/lang/ko.ts +0 -20
- package/src/lang/pt-BR.ts +0 -20
- package/src/lang/zh.ts +0 -20
- package/src/plugins/spreadsheet/definition.ts +9 -127
- package/src/plugins/spreadsheet/index.ts +7 -4
- package/src/plugins/spreadsheet/meta.ts +2 -4
- package/client/assets/echarts-gl-Cq76Z6DA.js +0 -4282
- package/client/assets/index-Cz7JLE0k.css +0 -2
- package/server/utils/files/spreadsheet-store.ts +0 -26
- package/src/plugins/spreadsheet/Preview.vue +0 -31
- package/src/plugins/spreadsheet/View.vue +0 -1007
- package/src/plugins/spreadsheet/cellHighlights.ts +0 -78
- package/src/plugins/spreadsheet/engine/calculator.ts +0 -427
- package/src/plugins/spreadsheet/engine/cellBuilder.ts +0 -89
- package/src/plugins/spreadsheet/engine/cellEmpty.ts +0 -26
- package/src/plugins/spreadsheet/engine/cellFormatting.ts +0 -59
- package/src/plugins/spreadsheet/engine/coerce-boolean.ts +0 -26
- package/src/plugins/spreadsheet/engine/condition.ts +0 -182
- package/src/plugins/spreadsheet/engine/date-locale.ts +0 -47
- package/src/plugins/spreadsheet/engine/date-parser.ts +0 -234
- package/src/plugins/spreadsheet/engine/date-utils.ts +0 -73
- package/src/plugins/spreadsheet/engine/datedif.ts +0 -93
- package/src/plugins/spreadsheet/engine/engine.ts +0 -177
- package/src/plugins/spreadsheet/engine/evaluator.ts +0 -494
- package/src/plugins/spreadsheet/engine/financial-math.ts +0 -114
- package/src/plugins/spreadsheet/engine/formatter.ts +0 -158
- package/src/plugins/spreadsheet/engine/formulaError.ts +0 -67
- package/src/plugins/spreadsheet/engine/formulaRefs.ts +0 -202
- package/src/plugins/spreadsheet/engine/functions/date.ts +0 -212
- package/src/plugins/spreadsheet/engine/functions/financial.ts +0 -244
- package/src/plugins/spreadsheet/engine/functions/index.ts +0 -16
- package/src/plugins/spreadsheet/engine/functions/logical.ts +0 -221
- package/src/plugins/spreadsheet/engine/functions/lookup-math.ts +0 -28
- package/src/plugins/spreadsheet/engine/functions/lookup.ts +0 -257
- package/src/plugins/spreadsheet/engine/functions/mathematical.ts +0 -283
- package/src/plugins/spreadsheet/engine/functions/statistical-math.ts +0 -91
- package/src/plugins/spreadsheet/engine/functions/statistical.ts +0 -290
- package/src/plugins/spreadsheet/engine/functions/text.ts +0 -398
- package/src/plugins/spreadsheet/engine/index.ts +0 -34
- package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +0 -111
- package/src/plugins/spreadsheet/engine/math-ops.ts +0 -98
- package/src/plugins/spreadsheet/engine/numericCoercion.ts +0 -61
- package/src/plugins/spreadsheet/engine/parser.ts +0 -40
- package/src/plugins/spreadsheet/engine/registry.ts +0 -201
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +0 -68
- package/src/plugins/spreadsheet/engine/spreadsheet-errors.ts +0 -82
- package/src/plugins/spreadsheet/engine/textFormat.ts +0 -108
- package/src/plugins/spreadsheet/engine/translateFormula.ts +0 -69
- package/src/plugins/spreadsheet/engine/types.ts +0 -81
- package/src/plugins/spreadsheet/keyboardNav.ts +0 -38
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Spreadsheet Engine
|
|
3
|
-
*
|
|
4
|
-
* Framework-agnostic spreadsheet calculation engine
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
// Export types
|
|
8
|
-
export * from "./types";
|
|
9
|
-
|
|
10
|
-
// Export utilities
|
|
11
|
-
export * from "./parser";
|
|
12
|
-
export * from "./condition";
|
|
13
|
-
export * from "./date-locale";
|
|
14
|
-
export * from "./cellEmpty";
|
|
15
|
-
export * from "./datedif";
|
|
16
|
-
export * from "./formatter";
|
|
17
|
-
export * from "./translateFormula";
|
|
18
|
-
export * from "./formulaError";
|
|
19
|
-
export * from "./spreadsheet-errors";
|
|
20
|
-
export * from "./evaluator";
|
|
21
|
-
export * from "./calculator";
|
|
22
|
-
export * from "./formulaRefs";
|
|
23
|
-
export * from "./cellBuilder";
|
|
24
|
-
export * from "./responseDecoder";
|
|
25
|
-
export * from "./jsonCellLocator";
|
|
26
|
-
|
|
27
|
-
// Export function registry
|
|
28
|
-
export * from "./registry";
|
|
29
|
-
|
|
30
|
-
// Load all built-in functions
|
|
31
|
-
import "./functions";
|
|
32
|
-
|
|
33
|
-
// Export main SpreadsheetEngine class
|
|
34
|
-
export { SpreadsheetEngine } from "./engine";
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Locate the start offset of a specific cell value inside a
|
|
3
|
-
* pretty-printed JSON spreadsheet document. Extracted from
|
|
4
|
-
* `handleTableClick` in `src/plugins/spreadsheet/View.vue` where the
|
|
5
|
-
* inline scanner pushed cognitive complexity to 163.
|
|
6
|
-
*
|
|
7
|
-
* The scanner walks the raw editor text character-by-character,
|
|
8
|
-
* tracking string boundaries, bracket depth, and object depth to
|
|
9
|
-
* find the n-th cell of the m-th row within a named sheet's `data`
|
|
10
|
-
* array. We deliberately do NOT parse the JSON — we need the
|
|
11
|
-
* character offset inside the user's text buffer (which may not be
|
|
12
|
-
* valid JSON mid-edit), so a positional scan is required.
|
|
13
|
-
*
|
|
14
|
-
* Pure — no refs, no DOM. Returns -1 if the cell can't be located.
|
|
15
|
-
* Tested in `test/plugins/spreadsheet/engine/test_jsonCellLocator.ts`.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
// Advance `pos` through `text` until we reach the `rowIndex`-th
|
|
19
|
-
// opening `[` after `startPos` (counting from -1 so the first `[`
|
|
20
|
-
// encountered is index 0). Returns the position just after that
|
|
21
|
-
// opening bracket, or -1 if we ran off the end.
|
|
22
|
-
function findRowOpenBracket(text: string, startPos: number, rowIndex: number): number {
|
|
23
|
-
let currentRow = -1;
|
|
24
|
-
let inString = false;
|
|
25
|
-
for (let i = startPos; i < text.length; i++) {
|
|
26
|
-
const c = text[i];
|
|
27
|
-
const prevChar = i > 0 ? text[i - 1] : "";
|
|
28
|
-
// Track string literal boundaries so that a `[` inside a cell
|
|
29
|
-
// value like `"has [bracket]"` doesn't get mistaken for a row
|
|
30
|
-
// opener and throw off the row offset.
|
|
31
|
-
if (c === '"' && prevChar !== "\\") {
|
|
32
|
-
inString = !inString;
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
if (!inString && c === "[") {
|
|
36
|
-
currentRow++;
|
|
37
|
-
if (currentRow === rowIndex) return i + 1;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return -1;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Starting just inside the row's `[`, scan for the start offset of
|
|
44
|
-
// the `colIndex`-th cell. Tracks string/object/bracket state so
|
|
45
|
-
// commas inside cell objects don't miscounted as cell separators.
|
|
46
|
-
// Returns -1 if the row ends before we reach colIndex.
|
|
47
|
-
function findCellStartWithinRow(text: string, rowStart: number, colIndex: number): number {
|
|
48
|
-
let currentCol = 0;
|
|
49
|
-
let inString = false;
|
|
50
|
-
let inObject = 0;
|
|
51
|
-
let bracketDepth = 1; // we already stepped past one `[`
|
|
52
|
-
|
|
53
|
-
for (let i = rowStart; i < text.length; i++) {
|
|
54
|
-
const c = text[i];
|
|
55
|
-
const prevChar = i > 0 ? text[i - 1] : "";
|
|
56
|
-
|
|
57
|
-
if (c === '"' && prevChar !== "\\") {
|
|
58
|
-
inString = !inString;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!inString) {
|
|
62
|
-
if (c === "[") bracketDepth++;
|
|
63
|
-
if (c === "]") {
|
|
64
|
-
bracketDepth--;
|
|
65
|
-
if (bracketDepth === 0) return -1; // row ended before colIndex
|
|
66
|
-
}
|
|
67
|
-
if (c === "{") inObject++;
|
|
68
|
-
if (c === "}") inObject--;
|
|
69
|
-
if (c === "," && inObject === 0 && bracketDepth === 1) {
|
|
70
|
-
currentCol++;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Once currentCol matches, skip any structural whitespace /
|
|
75
|
-
// opening bracket / comma and return the first content char.
|
|
76
|
-
if (currentCol === colIndex) {
|
|
77
|
-
if (c !== " " && c !== "\n" && c !== "\t" && c !== "[" && c !== ",") {
|
|
78
|
-
return i;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return -1;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Given the pretty-printed JSON editor text, a sheet name, and
|
|
87
|
-
* (rowIndex, colIndex), return the character offset of that cell's
|
|
88
|
-
* JSON token. The returned offset can be fed into
|
|
89
|
-
* `textarea.setSelectionRange(offset, offset + cellJsonLength)` to
|
|
90
|
-
* highlight the cell.
|
|
91
|
-
*
|
|
92
|
-
* Returns -1 if the sheet isn't found or the (row, col) is out of
|
|
93
|
-
* range. Never throws.
|
|
94
|
-
*/
|
|
95
|
-
export function findCellJsonPosition(editorText: string, sheetName: string, rowIndex: number, colIndex: number): number {
|
|
96
|
-
// JSON.stringify escapes embedded quotes/backslashes so the marker
|
|
97
|
-
// matches the way the sheet name actually appears in editorText.
|
|
98
|
-
const sheetStartMarker = `"name": ${JSON.stringify(sheetName)}`;
|
|
99
|
-
const dataStartMarker = `"data": [`;
|
|
100
|
-
|
|
101
|
-
const sheetPos = editorText.indexOf(sheetStartMarker);
|
|
102
|
-
if (sheetPos === -1) return -1;
|
|
103
|
-
|
|
104
|
-
const dataPos = editorText.indexOf(dataStartMarker, sheetPos);
|
|
105
|
-
if (dataPos === -1) return -1;
|
|
106
|
-
|
|
107
|
-
const rowStart = findRowOpenBracket(editorText, dataPos + dataStartMarker.length, rowIndex);
|
|
108
|
-
if (rowStart === -1) return -1;
|
|
109
|
-
|
|
110
|
-
return findCellStartWithinRow(editorText, rowStart, colIndex);
|
|
111
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rounding, modulo and significance math for the mathematical functions.
|
|
3
|
-
*
|
|
4
|
-
* Pure number-in / (number | formula-error-value)-out helpers. The rounding
|
|
5
|
-
* direction and the domain rules are exactly where the engine diverged from
|
|
6
|
-
* Excel (rounding toward +∞ instead of away from zero, modulo taking the
|
|
7
|
-
* dividend's sign, negative bases and domains slipping through as NaN/∞), so
|
|
8
|
-
* they are worth testing apart from the argument-reading handlers.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { DIV_ZERO_ERROR, NUM_ERROR, isSpreadsheetErrorValue, type SpreadsheetError } from "./spreadsheet-errors";
|
|
12
|
-
|
|
13
|
-
const DECIMAL_BASE = 10;
|
|
14
|
-
|
|
15
|
-
const scale = (digits: number): number => Math.pow(DECIMAL_BASE, digits);
|
|
16
|
-
|
|
17
|
-
/** Excel ROUND: half away from zero (JS `Math.round` breaks half toward +∞, so
|
|
18
|
-
* `ROUND(-2.5, 0)` came back -2 instead of -3). */
|
|
19
|
-
export function roundTo(value: number, digits: number): number {
|
|
20
|
-
const factor = scale(digits);
|
|
21
|
-
return (Math.sign(value) * Math.round(Math.abs(value) * factor)) / factor;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Excel ROUNDUP: away from zero. */
|
|
25
|
-
export function roundUpTo(value: number, digits: number): number {
|
|
26
|
-
const factor = scale(digits);
|
|
27
|
-
return (Math.sign(value) * Math.ceil(Math.abs(value) * factor)) / factor;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Excel ROUNDDOWN: toward zero. */
|
|
31
|
-
export function roundDownTo(value: number, digits: number): number {
|
|
32
|
-
const factor = scale(digits);
|
|
33
|
-
return (Math.sign(value) * Math.floor(Math.abs(value) * factor)) / factor;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Whether `value` and `significance` point the same way; opposite signs are the
|
|
37
|
-
* #NUM! case for FLOOR / CEILING. A zero value is always in range. */
|
|
38
|
-
const sameSign = (value: number, significance: number): boolean => value === 0 || Math.sign(value) === Math.sign(significance);
|
|
39
|
-
|
|
40
|
-
/** Excel FLOOR: nearest multiple of `significance` toward zero; opposite signs
|
|
41
|
-
* are #NUM!, and a zero significance is #DIV/0! — the division by the
|
|
42
|
-
* significance is what FLOOR reports, so it wins over the sign check. */
|
|
43
|
-
export function floorToSignificance(value: number, significance: number): number | SpreadsheetError {
|
|
44
|
-
if (significance === 0) return DIV_ZERO_ERROR;
|
|
45
|
-
if (!sameSign(value, significance)) return NUM_ERROR;
|
|
46
|
-
return Math.floor(value / significance) * significance;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Excel CEILING: nearest multiple of `significance` away from zero; opposite
|
|
50
|
-
* signs are #NUM!, and a zero significance is 0 — deliberately NOT FLOOR's
|
|
51
|
-
* #DIV/0!, an asymmetry Excel keeps and this engine has to match. */
|
|
52
|
-
export function ceilingToSignificance(value: number, significance: number): number | SpreadsheetError {
|
|
53
|
-
if (significance === 0) return 0;
|
|
54
|
-
if (!sameSign(value, significance)) return NUM_ERROR;
|
|
55
|
-
return Math.ceil(value / significance) * significance;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Excel MOD: result takes the divisor's sign (`MOD(-3, 2) === 1`); dividing by
|
|
59
|
-
* zero is #DIV/0!, not a silent 0. */
|
|
60
|
-
export function modulo(value: number, divisor: number): number | SpreadsheetError {
|
|
61
|
-
if (divisor === 0) return DIV_ZERO_ERROR;
|
|
62
|
-
return value - divisor * Math.floor(value / divisor);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Excel POWER: a negative base with a non-integer exponent has no real root, so
|
|
66
|
-
* it is #NUM! rather than JS's NaN. */
|
|
67
|
-
export function power(base: number, exponent: number): number | SpreadsheetError {
|
|
68
|
-
if (base < 0 && !Number.isInteger(exponent)) return NUM_ERROR;
|
|
69
|
-
return Math.pow(base, exponent);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Natural log guarded to its domain: `LN(x)` for `x <= 0` is #NUM!, not
|
|
73
|
-
* -∞ / NaN. Reused by LN and LOG. */
|
|
74
|
-
export function safeLog(value: number): number | SpreadsheetError {
|
|
75
|
-
if (value <= 0) return NUM_ERROR;
|
|
76
|
-
return Math.log(value);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Excel LOG(value, base): the base must also be positive and not 1, or the
|
|
80
|
-
* result is #NUM! rather than the ∞ / NaN a bare division would give. */
|
|
81
|
-
export function logWithBase(value: number, base: number): number | SpreadsheetError {
|
|
82
|
-
const lnValue = safeLog(value);
|
|
83
|
-
if (isSpreadsheetErrorValue(lnValue)) return lnValue;
|
|
84
|
-
if (base <= 0 || base === 1) return NUM_ERROR;
|
|
85
|
-
return lnValue / Math.log(base);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Base-10 log guarded to its domain (`LOG10(x)` for `x <= 0` is #NUM!). */
|
|
89
|
-
export function safeLog10(value: number): number | SpreadsheetError {
|
|
90
|
-
if (value <= 0) return NUM_ERROR;
|
|
91
|
-
return Math.log10(value);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Square root guarded to its domain (`SQRT(x)` for `x < 0` is #NUM!, not NaN). */
|
|
95
|
-
export function safeSqrt(value: number): number | SpreadsheetError {
|
|
96
|
-
if (value < 0) return NUM_ERROR;
|
|
97
|
-
return Math.sqrt(value);
|
|
98
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Numeric coercion helpers for the spreadsheet engine.
|
|
3
|
-
*
|
|
4
|
-
* Two intentionally different reads share one string parser (`parseNumericString`):
|
|
5
|
-
* - `registry.toNumber` — lenient, for range aggregation (SUM / AVERAGE / …):
|
|
6
|
-
* anything unreadable becomes 0. PINNED (booleans are 0, not Excel's 1/0).
|
|
7
|
-
* - `toScalarNumber` — strict, for single-value math functions (ABS / SIGN):
|
|
8
|
-
* booleans are 1/0 and non-numeric text is `#VALUE!`, matching Excel.
|
|
9
|
-
*
|
|
10
|
-
* `holdsNumber` asks the question neither read can answer once it has coerced:
|
|
11
|
-
* was there a number here at all, or is this 0 the reading of something else.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { CellValue } from "./types";
|
|
15
|
-
import { VALUE_ERROR, type SpreadsheetError } from "./spreadsheet-errors";
|
|
16
|
-
|
|
17
|
-
const numberOrNull = (num: number): number | null => (isNaN(num) ? null : num);
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Parse a spreadsheet string as a number, or `null` when it holds no number.
|
|
21
|
-
*
|
|
22
|
-
* Mirrors the engine's long-standing lenient read: a percentage, a currency
|
|
23
|
-
* amount, or a thousands-separated value, else a bare `parseFloat` (which reads a
|
|
24
|
-
* leading number out of "12abc"). The branch order is load-bearing — each strips
|
|
25
|
-
* only its own characters, so a string mixing "%" and "$" fails in the first.
|
|
26
|
-
*/
|
|
27
|
-
export function parseNumericString(value: string): number | null {
|
|
28
|
-
if (value.includes("%")) {
|
|
29
|
-
const num = numberOrNull(parseFloat(value.replace("%", "").trim()));
|
|
30
|
-
return num === null ? null : num / 100;
|
|
31
|
-
}
|
|
32
|
-
if (value.includes("$")) return numberOrNull(parseFloat(value.replace(/[$,]/g, "").trim()));
|
|
33
|
-
if (value.includes(",")) return numberOrNull(parseFloat(value.replace(/,/g, "").trim()));
|
|
34
|
-
return numberOrNull(parseFloat(value));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Whether a value holds a number at all, under the same reading `toNumber` uses.
|
|
39
|
-
*
|
|
40
|
-
* `toNumber` answers 0 for text, for a boolean and for a genuine 0 alike, so a
|
|
41
|
-
* caller that must tell "no number here" from "the number zero" — COUNT — cannot
|
|
42
|
-
* ask it. Booleans are not numbers here, matching `toNumber`'s PINNED behaviour.
|
|
43
|
-
*/
|
|
44
|
-
export function holdsNumber(value: CellValue): boolean {
|
|
45
|
-
if (typeof value === "number") return !isNaN(value);
|
|
46
|
-
if (typeof value !== "string") return false;
|
|
47
|
-
return parseNumericString(value) !== null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Strict scalar coercion for single-value math functions (ABS, SIGN). Follows
|
|
52
|
-
* Excel's scalar rules: a boolean is 1 / 0 and genuinely non-numeric text is
|
|
53
|
-
* `#VALUE!` rather than a silent 0. A partly-numeric string still yields its
|
|
54
|
-
* leading number, matching the engine's other numeric reads.
|
|
55
|
-
*/
|
|
56
|
-
export function toScalarNumber(value: CellValue): number | SpreadsheetError {
|
|
57
|
-
if (typeof value === "number") return value;
|
|
58
|
-
if (typeof value === "boolean") return value ? 1 : 0;
|
|
59
|
-
if (typeof value !== "string") return value;
|
|
60
|
-
return parseNumericString(value) ?? VALUE_ERROR;
|
|
61
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cell and Range Reference Parsing
|
|
3
|
-
*
|
|
4
|
-
* Handles Excel A1 notation parsing and conversion
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { CellRef, RangeRef } from "./types";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Convert Excel column letters to 0-based index
|
|
11
|
-
* A=0, B=1, ..., Z=25, AA=26, etc.
|
|
12
|
-
*
|
|
13
|
-
* @param col - Column letters (e.g., "A", "Z", "AA")
|
|
14
|
-
* @returns 0-based column index
|
|
15
|
-
*/
|
|
16
|
-
export function columnToIndex(col: string): number {
|
|
17
|
-
let result = 0;
|
|
18
|
-
for (let i = 0; i < col.length; i++) {
|
|
19
|
-
result = result * 26 + (col.charCodeAt(i) - 64); // A=1, B=2, etc.
|
|
20
|
-
}
|
|
21
|
-
return result - 1; // Convert to 0-based
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Convert 0-based index to Excel column letters
|
|
26
|
-
* 0=A, 1=B, ..., 25=Z, 26=AA, etc.
|
|
27
|
-
*
|
|
28
|
-
* @param index - 0-based column index
|
|
29
|
-
* @returns Column letters (e.g., "A", "Z", "AA")
|
|
30
|
-
*/
|
|
31
|
-
export function indexToColumn(index: number): string {
|
|
32
|
-
let col = "";
|
|
33
|
-
let num = index + 1; // Convert to 1-based
|
|
34
|
-
while (num > 0) {
|
|
35
|
-
const remainder = (num - 1) % 26;
|
|
36
|
-
col = String.fromCharCode(65 + remainder) + col;
|
|
37
|
-
num = Math.floor((num - 1) / 26);
|
|
38
|
-
}
|
|
39
|
-
return col;
|
|
40
|
-
}
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Spreadsheet Function Registry
|
|
3
|
-
*
|
|
4
|
-
* This module provides a registry system for spreadsheet functions,
|
|
5
|
-
* allowing modular organization and easy extension of formula capabilities.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { CellValue } from "./types";
|
|
9
|
-
import { parseNumericString } from "./numericCoercion";
|
|
10
|
-
export type { CellValue };
|
|
11
|
-
export type CellGetter = (ref: string) => CellValue;
|
|
12
|
-
export type RangeGetter = (range: string) => CellValue[];
|
|
13
|
-
export type RawRangeGetter = (range: string) => CellValue[];
|
|
14
|
-
|
|
15
|
-
export interface FunctionContext {
|
|
16
|
-
/** The registry name the formula invoked, so a handler can report a failure
|
|
17
|
-
* the way the evaluator names it. */
|
|
18
|
-
functionName: string;
|
|
19
|
-
getCellValue: CellGetter;
|
|
20
|
-
getRangeValues: RangeGetter;
|
|
21
|
-
getRangeValuesRaw?: RawRangeGetter | undefined;
|
|
22
|
-
evaluateFormula: (formula: string) => CellValue;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** The too-few-arguments error, raised by the evaluator from the registry's
|
|
26
|
-
* `minArgs` before any handler runs — and by `requiredArg` for the argument a
|
|
27
|
-
* handler actually reads, so both report one wording. */
|
|
28
|
-
export const tooFewArgumentsError = (funcName: string, minArgs: number): Error =>
|
|
29
|
-
new Error(`${funcName} requires at least ${minArgs} argument${minArgs !== 1 ? "s" : ""}`);
|
|
30
|
-
|
|
31
|
-
/** The argument at `index`, which the caller has already established is there —
|
|
32
|
-
* the registry's `minArgs` for a mandatory argument, an `args.length` branch for
|
|
33
|
-
* an optional one. An absent one means that guarantee is wrong, so it raises the
|
|
34
|
-
* arity error the guarantee should have raised. Never a default value: a
|
|
35
|
-
* substituted 0 or "" computes a plausible wrong answer, which is worse than
|
|
36
|
-
* the error the formula deserves. */
|
|
37
|
-
export const requiredArg = (context: FunctionContext, args: string[], index: number): string => {
|
|
38
|
-
const arg = args[index];
|
|
39
|
-
if (arg === undefined) throw tooFewArgumentsError(context.functionName, index + 1);
|
|
40
|
-
return arg;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/** The range reader that keeps every cell, for functions whose answer depends on
|
|
44
|
-
* a range's POSITIONS rather than its numbers.
|
|
45
|
-
*
|
|
46
|
-
* `getRangeValues` drops non-numeric cells, which silently renumbers the rows
|
|
47
|
-
* underneath: a criteria/value pair read that way falls out of alignment and
|
|
48
|
-
* aggregates a different row, and an index returned from it points at the wrong
|
|
49
|
-
* one. SUMIF/AVERAGEIF were moved here in #2358; MATCH/XLOOKUP in #2765.
|
|
50
|
-
*
|
|
51
|
-
* Falls back to the numeric reader because `getRangeValuesRaw` is optional on
|
|
52
|
-
* `FunctionContext` — a host that predates it keeps its old behaviour rather
|
|
53
|
-
* than throwing. */
|
|
54
|
-
export const rawRangeReader = (context: FunctionContext): RangeGetter => context.getRangeValuesRaw ?? context.getRangeValues;
|
|
55
|
-
|
|
56
|
-
export type FunctionHandler = (args: string[], context: FunctionContext) => CellValue;
|
|
57
|
-
|
|
58
|
-
export interface FunctionDefinition {
|
|
59
|
-
name: string;
|
|
60
|
-
handler: FunctionHandler;
|
|
61
|
-
minArgs?: number;
|
|
62
|
-
maxArgs?: number;
|
|
63
|
-
description?: string;
|
|
64
|
-
examples?: string[];
|
|
65
|
-
category?: string;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
class FunctionRegistry {
|
|
69
|
-
private functions = new Map<string, FunctionDefinition>();
|
|
70
|
-
|
|
71
|
-
register(def: FunctionDefinition): void {
|
|
72
|
-
this.functions.set(def.name.toUpperCase(), def);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
get(name: string): FunctionDefinition | undefined {
|
|
76
|
-
return this.functions.get(name.toUpperCase());
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
hasFunction(name: string): boolean {
|
|
80
|
-
return this.functions.has(name.toUpperCase());
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
getAllFunctions(): FunctionDefinition[] {
|
|
84
|
-
return Array.from(this.functions.values());
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
getFunctionsByCategory(): Map<string, FunctionDefinition[]> {
|
|
88
|
-
const categories = new Map<string, FunctionDefinition[]>();
|
|
89
|
-
|
|
90
|
-
for (const func of Array.from(this.functions.values())) {
|
|
91
|
-
const category = func.category || "Other";
|
|
92
|
-
if (!categories.has(category)) {
|
|
93
|
-
categories.set(category, []);
|
|
94
|
-
}
|
|
95
|
-
categories.get(category)!.push(func);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return categories;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export const functionRegistry = new FunctionRegistry();
|
|
103
|
-
|
|
104
|
-
/** Whether `name` written before "(" is a function call. A letters-only name is,
|
|
105
|
-
* registered or not (an unknown one reports #NAME?). A name with digits is only
|
|
106
|
-
* when registered: LOG10 is also shaped like a cell reference, so `A1(2)` is not. */
|
|
107
|
-
export const isCallableName = (name: string): boolean => /^[A-Z]+$/i.test(name) || functionRegistry.hasFunction(name);
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Lenient numeric coercion for range aggregation: anything unreadable is 0.
|
|
111
|
-
* PINNED behaviour (booleans are 0, not Excel's 1/0) — the string parsing lives
|
|
112
|
-
* in numericCoercion.parseNumericString, shared with the strict scalar read.
|
|
113
|
-
*/
|
|
114
|
-
export function toNumber(value: CellValue): number {
|
|
115
|
-
if (typeof value === "number") return value;
|
|
116
|
-
return parseNumericString(String(value)) ?? 0;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Helper function to convert a value to a string
|
|
121
|
-
*/
|
|
122
|
-
export function toString(value: CellValue): string {
|
|
123
|
-
return String(value);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const REGEXP_METACHARACTERS = /[.*+?^${}()|[\]\\]/;
|
|
127
|
-
|
|
128
|
-
const escapeRegExpChar = (char: string): string => (REGEXP_METACHARACTERS.test(char) ? `\\${char}` : char);
|
|
129
|
-
|
|
130
|
-
// One criteria token: `~x` (an escaped character) or any single character. The
|
|
131
|
-
// escape branch needs a following character, so a TRAILING `~` falls through to
|
|
132
|
-
// the single-character branch and stands for itself.
|
|
133
|
-
const CRITERIA_TOKEN = /~([\s\S])|[\s\S]/g;
|
|
134
|
-
|
|
135
|
-
const criteriaRegexSource = (pattern: string): string =>
|
|
136
|
-
pattern.replace(CRITERIA_TOKEN, (token, escaped: string | undefined) => {
|
|
137
|
-
if (escaped !== undefined) return escapeRegExpChar(escaped);
|
|
138
|
-
if (token === "*") return ".*";
|
|
139
|
-
if (token === "?") return ".";
|
|
140
|
-
return escapeRegExpChar(token);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Match text the way a spreadsheet criteria does: case-insensitively, with `*`
|
|
145
|
-
* standing for any run of characters, `?` for exactly one, and `~` escaping the
|
|
146
|
-
* next character. A plain `String(v) === criteria` missed both — `COUNTIF(range,
|
|
147
|
-
* "yes")` skipped a cell holding `Yes`, and `"A*"` was compared literally.
|
|
148
|
-
*/
|
|
149
|
-
function textMatcher(pattern: string): (text: string) => boolean {
|
|
150
|
-
const regex = new RegExp(`^${criteriaRegexSource(pattern)}$`, "iu");
|
|
151
|
-
return (text) => regex.test(text);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Helper to parse criteria for conditional functions like COUNTIF, SUMIF
|
|
156
|
-
* Returns a comparison function that tests if a value matches the criteria
|
|
157
|
-
*/
|
|
158
|
-
export function parseCriteria(criteria: string): (value: CellValue) => boolean {
|
|
159
|
-
// eslint-disable -- sonarjs/anchor-precedence
|
|
160
|
-
const trimmedCriteria = criteria.trim().replace(/^["']|["']$/g, "");
|
|
161
|
-
|
|
162
|
-
// Check for comparison operators
|
|
163
|
-
// eslint-disable -- sonarjs/slow-regex
|
|
164
|
-
const [, op, value] = trimmedCriteria.match(/^([><=!]+)(.+)$/) ?? [];
|
|
165
|
-
if (op !== undefined && value !== undefined) {
|
|
166
|
-
const numValue = parseFloat(value);
|
|
167
|
-
|
|
168
|
-
// `=` / `<>` compare like the bare criteria does — case-insensitive text
|
|
169
|
-
// with wildcards, or the number. `<>` is exactly its negation.
|
|
170
|
-
const equals = matchesTextOrNumber(value);
|
|
171
|
-
switch (op) {
|
|
172
|
-
case ">":
|
|
173
|
-
return (v) => toNumber(v) > numValue;
|
|
174
|
-
case ">=":
|
|
175
|
-
return (v) => toNumber(v) >= numValue;
|
|
176
|
-
case "<":
|
|
177
|
-
return (v) => toNumber(v) < numValue;
|
|
178
|
-
case "<=":
|
|
179
|
-
return (v) => toNumber(v) <= numValue;
|
|
180
|
-
case "=":
|
|
181
|
-
case "==":
|
|
182
|
-
return equals;
|
|
183
|
-
case "!=":
|
|
184
|
-
case "<>":
|
|
185
|
-
return (v) => !equals(v);
|
|
186
|
-
default:
|
|
187
|
-
return () => false;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return matchesTextOrNumber(trimmedCriteria);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/** A value matches when its text matches the criteria (case-insensitively, with
|
|
195
|
-
* wildcards) or, for a numeric criteria, when its number is equal. */
|
|
196
|
-
function matchesTextOrNumber(criteria: string): (value: CellValue) => boolean {
|
|
197
|
-
const matchesText = textMatcher(criteria);
|
|
198
|
-
const numCriteria = parseFloat(criteria);
|
|
199
|
-
const hasNumber = !isNaN(numCriteria);
|
|
200
|
-
return (value) => matchesText(String(value)) || (hasNumber && toNumber(value) === numCriteria);
|
|
201
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Decode the `/api/files/content` response for a spreadsheet file
|
|
3
|
-
* into an "ok with sheets" / "error with message" discriminated
|
|
4
|
-
* union. Extracted from `fetchSheets` in
|
|
5
|
-
* `src/plugins/spreadsheet/View.vue` where the decision tree was
|
|
6
|
-
* inlined as several nested try/catch + if branches.
|
|
7
|
-
*
|
|
8
|
-
* Pure — no fetch, no refs. Takes the parsed JSON body and returns
|
|
9
|
-
* a result the caller can pattern-match on. Tested in
|
|
10
|
-
* `test/plugins/spreadsheet/engine/test_responseDecoder.ts`.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { SheetData } from "./types.js";
|
|
14
|
-
import { errorMessage } from "../../../utils/errors";
|
|
15
|
-
|
|
16
|
-
/** Shape of the `/api/files/content` response we care about. The
|
|
17
|
-
* server returns more fields (kind, size, modifiedMs, …) but this
|
|
18
|
-
* decoder only depends on the three that drive branching. */
|
|
19
|
-
export interface FilesContentResponseLike {
|
|
20
|
-
kind?: string;
|
|
21
|
-
content?: string;
|
|
22
|
-
message?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export type DecodeResult = { kind: "ok"; sheets: SheetData[] } | { kind: "error"; message: string };
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Turn a parsed `/files/content` body into an OK/error decision:
|
|
29
|
-
*
|
|
30
|
-
* - `kind` present and not "text" → error with the server's message
|
|
31
|
-
* (e.g. "too-large", "binary"). Spreadsheets only live in text
|
|
32
|
-
* JSON files.
|
|
33
|
-
* - Missing or non-string `content` → error.
|
|
34
|
-
* - `content` is not valid JSON → error.
|
|
35
|
-
* - Parsed `content` is not an array → error (server should never
|
|
36
|
-
* return a non-array but the guard protects downstream render).
|
|
37
|
-
* - Otherwise → ok with the sheets array.
|
|
38
|
-
*/
|
|
39
|
-
export function decodeSpreadsheetResponse(body: FilesContentResponseLike): DecodeResult {
|
|
40
|
-
if (body.kind && body.kind !== "text") {
|
|
41
|
-
return {
|
|
42
|
-
kind: "error",
|
|
43
|
-
message: body.message ?? `Cannot load spreadsheet: ${body.kind}`,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
if (typeof body.content !== "string") {
|
|
47
|
-
return { kind: "error", message: "Spreadsheet file has no content" };
|
|
48
|
-
}
|
|
49
|
-
let parsed: unknown;
|
|
50
|
-
try {
|
|
51
|
-
parsed = JSON.parse(body.content);
|
|
52
|
-
} catch (err) {
|
|
53
|
-
return {
|
|
54
|
-
kind: "error",
|
|
55
|
-
message: `Spreadsheet JSON is malformed: ${errorMessage(err, "parse error")}`,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
if (!Array.isArray(parsed)) {
|
|
59
|
-
return {
|
|
60
|
-
kind: "error",
|
|
61
|
-
message: "Spreadsheet content is not an array of sheets",
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
// Array.isArray narrows to unknown[]; we trust the server contract
|
|
65
|
-
// and type the local explicitly rather than using an inline `as`.
|
|
66
|
-
const sheets: SheetData[] = parsed;
|
|
67
|
-
return { kind: "ok", sheets };
|
|
68
|
-
}
|