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,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
- }