mulmoclaude 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/client/assets/Graph-ChAxkn9o.js +1 -0
  2. package/client/assets/{JsonEditor-FKfzzMkt.js → JsonEditor-COQcmjLQ.js} +1 -1
  3. package/client/assets/dist-Bij44LdC.js +2 -0
  4. package/client/assets/echarts-GV3U9sVf.js +1 -0
  5. package/client/assets/echarts-gl-BXx4On0Y.js +4282 -0
  6. package/client/assets/echarts-wordcloud-_9uQUAfp.js +1 -0
  7. package/client/assets/emscripten-module-Q67P5WYC-BU3Hlttz.js +2160 -0
  8. package/client/assets/ffi-4V2b7xu3.js +1 -0
  9. package/client/assets/{html2canvas-D0m9Y8pD-y1tDAhR3.js → html2canvas-NBfz6Rb9-CIdEBoH2.js} +1 -1
  10. package/client/assets/{index-gnbD2OHU.js → index-D6xzCZhP.js} +605 -604
  11. package/client/assets/index-DzMsXDnj.css +2 -0
  12. package/client/assets/{index.es-HSMDtZ_a-CYtNwG_G.js → index.es-STZX6pYP-BGvt3MI2.js} +3 -3
  13. package/client/assets/install-C1moNcrn.js +15 -0
  14. package/client/assets/japan-CyDc-cz7-BgORDJsl.js +1 -0
  15. package/client/assets/{marp-BVojbGre.js → marp-Du5Sym5b.js} +57 -57
  16. package/client/assets/module-ES6BEMUI-CDTIpMiA.js +6 -0
  17. package/client/assets/purify.es-DkTnAgLw-BZxk_wWO.js +3 -0
  18. package/client/assets/style-BZwVK1dO-BFtilmZh.js +1 -0
  19. package/client/assets/vue-i18n-YREco4q1.js +3 -0
  20. package/client/assets/world-R-tMZ4nZ-B1c4T9X2.js +1 -0
  21. package/client/index.html +6 -4
  22. package/package.json +13 -12
  23. package/server/api/routes/plugins.ts +9 -59
  24. package/server/build/mcp-server.mjs +806 -714
  25. package/server/index.ts +1 -0
  26. package/server/plugins/confined-file-ops.ts +56 -0
  27. package/server/plugins/spreadsheet-builtin.ts +26 -0
  28. package/server/plugins/spreadsheet-save-outcome.ts +7 -0
  29. package/server/utils/files/store-resolvers.ts +1 -1
  30. package/src/lang/de.ts +0 -20
  31. package/src/lang/en.ts +0 -20
  32. package/src/lang/es.ts +0 -20
  33. package/src/lang/fr.ts +0 -20
  34. package/src/lang/ja.ts +0 -20
  35. package/src/lang/ko.ts +0 -20
  36. package/src/lang/pt-BR.ts +0 -20
  37. package/src/lang/zh.ts +0 -20
  38. package/src/plugins/spreadsheet/definition.ts +9 -127
  39. package/src/plugins/spreadsheet/index.ts +7 -4
  40. package/src/plugins/spreadsheet/meta.ts +2 -4
  41. package/client/assets/index-BA0fpWIZ.css +0 -2
  42. package/client/assets/purify.es-BaSjDiWD-Dbm5oOcm.js +0 -3
  43. package/client/assets/style-CCV5GAnU-D55tgtzm.js +0 -1
  44. package/client/assets/vue-i18n-2bgWpywp.js +0 -3
  45. package/server/utils/files/spreadsheet-store.ts +0 -26
  46. package/src/plugins/spreadsheet/Preview.vue +0 -31
  47. package/src/plugins/spreadsheet/View.vue +0 -1007
  48. package/src/plugins/spreadsheet/cellHighlights.ts +0 -78
  49. package/src/plugins/spreadsheet/engine/calculator.ts +0 -427
  50. package/src/plugins/spreadsheet/engine/cellBuilder.ts +0 -89
  51. package/src/plugins/spreadsheet/engine/cellEmpty.ts +0 -26
  52. package/src/plugins/spreadsheet/engine/cellFormatting.ts +0 -59
  53. package/src/plugins/spreadsheet/engine/coerce-boolean.ts +0 -26
  54. package/src/plugins/spreadsheet/engine/condition.ts +0 -182
  55. package/src/plugins/spreadsheet/engine/date-locale.ts +0 -47
  56. package/src/plugins/spreadsheet/engine/date-parser.ts +0 -234
  57. package/src/plugins/spreadsheet/engine/date-utils.ts +0 -73
  58. package/src/plugins/spreadsheet/engine/datedif.ts +0 -93
  59. package/src/plugins/spreadsheet/engine/engine.ts +0 -177
  60. package/src/plugins/spreadsheet/engine/evaluator.ts +0 -494
  61. package/src/plugins/spreadsheet/engine/financial-math.ts +0 -114
  62. package/src/plugins/spreadsheet/engine/formatter.ts +0 -158
  63. package/src/plugins/spreadsheet/engine/formulaError.ts +0 -67
  64. package/src/plugins/spreadsheet/engine/formulaRefs.ts +0 -202
  65. package/src/plugins/spreadsheet/engine/functions/date.ts +0 -212
  66. package/src/plugins/spreadsheet/engine/functions/financial.ts +0 -244
  67. package/src/plugins/spreadsheet/engine/functions/index.ts +0 -16
  68. package/src/plugins/spreadsheet/engine/functions/logical.ts +0 -221
  69. package/src/plugins/spreadsheet/engine/functions/lookup-math.ts +0 -28
  70. package/src/plugins/spreadsheet/engine/functions/lookup.ts +0 -257
  71. package/src/plugins/spreadsheet/engine/functions/mathematical.ts +0 -283
  72. package/src/plugins/spreadsheet/engine/functions/statistical-math.ts +0 -91
  73. package/src/plugins/spreadsheet/engine/functions/statistical.ts +0 -290
  74. package/src/plugins/spreadsheet/engine/functions/text.ts +0 -398
  75. package/src/plugins/spreadsheet/engine/index.ts +0 -34
  76. package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +0 -111
  77. package/src/plugins/spreadsheet/engine/math-ops.ts +0 -98
  78. package/src/plugins/spreadsheet/engine/numericCoercion.ts +0 -61
  79. package/src/plugins/spreadsheet/engine/parser.ts +0 -40
  80. package/src/plugins/spreadsheet/engine/registry.ts +0 -201
  81. package/src/plugins/spreadsheet/engine/responseDecoder.ts +0 -68
  82. package/src/plugins/spreadsheet/engine/spreadsheet-errors.ts +0 -82
  83. package/src/plugins/spreadsheet/engine/textFormat.ts +0 -108
  84. package/src/plugins/spreadsheet/engine/translateFormula.ts +0 -69
  85. package/src/plugins/spreadsheet/engine/types.ts +0 -81
  86. package/src/plugins/spreadsheet/keyboardNav.ts +0 -38
@@ -1,114 +0,0 @@
1
- /**
2
- * Annuity math for the financial functions, as pure numeric helpers.
3
- *
4
- * Excel's cash-flow sign convention: money received is positive, money paid out
5
- * is negative — so a loan's `pv` is positive and its payment and interest come
6
- * back negative. Keeping the per-period split as plain number-in / number-out
7
- * functions lets it be tested against known Excel values without routing string
8
- * arguments back through the formula evaluator.
9
- */
10
-
11
- import { NUM_ERROR, type SpreadsheetError } from "./spreadsheet-errors";
12
-
13
- /** Future value after `nper` periods. `type` is 0 (end of period) or 1 (begin). */
14
- export function computeFv(rate: number, nper: number, pmt: number, pv: number, type: number): number {
15
- if (rate === 0) return -(pv + pmt * nper);
16
- const growth = Math.pow(1 + rate, nper);
17
- return -pv * growth - (pmt * (growth - 1) * (1 + rate * type)) / rate;
18
- }
19
-
20
- /** Constant per-period payment amortising a present value `pv` to `fv`. */
21
- export function computePmt(rate: number, nper: number, pv: number, fv: number, type: number): number {
22
- if (rate === 0) return -(fv + pv) / nper;
23
- const growth = Math.pow(1 + rate, nper);
24
- return (-rate * (fv + pv * growth)) / ((growth - 1) * (1 + rate * type));
25
- }
26
-
27
- /** Interest portion of the `per`-th payment. */
28
- export function computeIpmt(rate: number, per: number, nper: number, pv: number, fv: number, type: number): number {
29
- const pmt = computePmt(rate, nper, pv, fv, type);
30
- if (per === 1 && type === 1) return 0;
31
- const priorPeriods = type === 1 ? per - 2 : per - 1;
32
- // Interest is the rate applied to the balance outstanding at the start of the
33
- // period. computeFv already carries the payment-negative sign, so this product
34
- // is the interest as a (negative) cash outflow — negating it again inverted the
35
- // sign (IPMT came back +1250 for a -1250 payment) and PPMT amplified the error.
36
- const interest = computeFv(rate, priorPeriods, pmt, pv, type) * rate;
37
- return type === 1 ? interest / (1 + rate) : interest;
38
- }
39
-
40
- /** Principal portion of the `per`-th payment (total payment minus interest). */
41
- export function computePpmt(rate: number, per: number, nper: number, pv: number, fv: number, type: number): number {
42
- return computePmt(rate, nper, pv, fv, type) - computeIpmt(rate, per, nper, pv, fv, type);
43
- }
44
-
45
- /** Present value of an annuity that grows to `fv` after `nper` payments of `pmt`. */
46
- export function computePv(rate: number, nper: number, pmt: number, fv: number, type: number): number {
47
- if (rate === 0) return -(fv + pmt * nper);
48
- const growth = Math.pow(1 + rate, nper);
49
- return (-fv - (pmt * (growth - 1) * (1 + rate * type)) / rate) / growth;
50
- }
51
-
52
- /** Number of periods needed to amortise `pv` to `fv` at constant `pmt`. */
53
- export function computeNper(rate: number, pmt: number, pv: number, fv: number, type: number): number {
54
- if (rate === 0) return -(fv + pv) / pmt;
55
- const pmtWithType = pmt * (1 + rate * type);
56
- return Math.log((pmtWithType - fv * rate) / (pmtWithType + pv * rate)) / Math.log(1 + rate);
57
- }
58
-
59
- // Newton-Raphson is iterative; a valid annuity / cash-flow series converges well
60
- // inside these bounds. A series with no real solution never does, so exhausting
61
- // the loop means "no answer" — Excel reports #NUM! there, and returning the last
62
- // iterate instead would hand back a divergent value or NaN as if it were a rate.
63
- const NEWTON_MAX_ITERATIONS = 100;
64
- const NEWTON_TOLERANCE = 1e-7;
65
-
66
- /** Interest rate per period solving the annuity equation, via Newton-Raphson. */
67
- export function computeRate(nper: number, pmt: number, pv: number, fv: number, type: number, guess: number): number | SpreadsheetError {
68
- let rate = guess;
69
- for (let i = 0; i < NEWTON_MAX_ITERATIONS; i++) {
70
- if (Math.abs(rate) < NEWTON_TOLERANCE) rate = NEWTON_TOLERANCE; // avoid division by zero
71
- const growth = Math.pow(1 + rate, nper);
72
- const f = pv * growth + pmt * ((growth - 1) / rate) * (1 + rate * type) + fv;
73
- const df =
74
- nper * pv * Math.pow(1 + rate, nper - 1) +
75
- (pmt * (1 + rate * type) * (nper * Math.pow(1 + rate, nper - 1) * rate - (growth - 1))) / (rate * rate) +
76
- pmt * type * ((growth - 1) / rate);
77
- const newRate = rate - f / df;
78
- if (Math.abs(newRate - rate) < NEWTON_TOLERANCE) return Number.isFinite(newRate) ? newRate : NUM_ERROR;
79
- rate = newRate;
80
- }
81
- return NUM_ERROR;
82
- }
83
-
84
- /** Net present value of `cashflows`, each discounted one period further into the future. */
85
- export function computeNpv(rate: number, cashflows: number[]): number {
86
- return cashflows.reduce((npv, value, index) => npv + value / Math.pow(1 + rate, index + 1), 0);
87
- }
88
-
89
- /** The present value of `values` (element 0 at period 0) discounted at `rate`,
90
- * together with its derivative — the pair one Newton-Raphson step needs. */
91
- function discountedSeries(values: number[], rate: number): { npv: number; dnpv: number } {
92
- return values.reduce(
93
- (totals, value, period) => {
94
- const factor = Math.pow(1 + rate, period);
95
- return { npv: totals.npv + value / factor, dnpv: totals.dnpv - (period * value) / (factor * (1 + rate)) };
96
- },
97
- { npv: 0, dnpv: 0 },
98
- );
99
- }
100
-
101
- /** Internal rate of return of `values` (element 0 at period 0), via Newton-Raphson. */
102
- export function computeIrr(values: number[], guess: number): number | SpreadsheetError {
103
- let rate = guess;
104
- for (let i = 0; i < NEWTON_MAX_ITERATIONS; i++) {
105
- const { npv, dnpv } = discountedSeries(values, rate);
106
- if (Math.abs(npv) < NEWTON_TOLERANCE) return rate;
107
- // A flat derivative leaves nowhere to step: the series has no root here.
108
- if (Math.abs(dnpv) < NEWTON_TOLERANCE) return NUM_ERROR;
109
- const newRate = rate - npv / dnpv;
110
- if (Math.abs(newRate - rate) < NEWTON_TOLERANCE) return Number.isFinite(newRate) ? newRate : NUM_ERROR;
111
- rate = newRate;
112
- }
113
- return NUM_ERROR;
114
- }
@@ -1,158 +0,0 @@
1
- /**
2
- * Number Formatting Utilities
3
- *
4
- * Handles Excel-style format codes for currency, percentages, decimals, dates, etc.
5
- */
6
-
7
- import { serialToDate, MONTH_NAMES_SHORT, MONTH_NAMES_FULL, DAY_NAMES_SHORT, DAY_NAMES_FULL } from "./date-utils";
8
-
9
- /**
10
- * Check if a format code is for dates
11
- */
12
- function isDateFormat(format: string): boolean {
13
- // Date formats contain date/time tokens: Y, M, D, h, m, s
14
- // But not percentage (which also has 'm' in format like #,##0)
15
- // Look for specific date patterns
16
- return /[YMD]|MMM|DD|YYYY|h:mm|AM\/PM/i.test(format);
17
- }
18
-
19
- /**
20
- * Format a number as a date according to Excel format code
21
- *
22
- * Supported formats:
23
- * - MM/DD/YYYY, M/D/YYYY
24
- * - DD/MM/YYYY, D/M/YYYY
25
- * - YYYY-MM-DD
26
- * - DD-MMM-YYYY, D-MMM-YYYY
27
- * - MMM D, YYYY, MMMM D, YYYY
28
- * - h:mm AM/PM, HH:mm:ss
29
- *
30
- * @param serial - Excel serial number
31
- * @param format - Date format code
32
- * @returns Formatted date string
33
- */
34
- // Every token, longest-first within each family so `MMMM` wins over `MMM` and
35
- // `MM`. Matched in ONE pass: a sequence of `replace` calls re-scans its own
36
- // output, so an inserted "March" had its `M` rewritten by the later month-number
37
- // step and "AM/PM" was destroyed before the meridiem branch could see it.
38
- const DATE_TOKEN_RE = /YYYY|YY|MMMM|MMM|MM|M|dddd|ddd|DD|D|AM\/PM|am\/pm|HH|H|hh|h|mm|ss/g;
39
-
40
- function formatDate(serial: number, format: string): string {
41
- const date = serialToDate(serial);
42
-
43
- const year = date.getUTCFullYear();
44
- const month = date.getUTCMonth(); // 0-11
45
- const day = date.getUTCDate();
46
- const hours = date.getUTCHours();
47
- const minutes = date.getUTCMinutes();
48
- const seconds = date.getUTCSeconds();
49
- const dayOfWeek = date.getUTCDay(); // 0-6
50
-
51
- // `h` means the 12-hour clock only when the format also asks for a meridiem;
52
- // decided from the ORIGINAL format, before any substitution.
53
- const uses12Hour = /AM\/PM|am\/pm/.test(format);
54
- const hours12 = hours % 12 || 12; // 0 becomes 12
55
- const isPM = hours >= 12;
56
-
57
- const replacements: Record<string, string> = {
58
- YYYY: year.toString(),
59
- YY: (year % 100).toString().padStart(2, "0"),
60
- MMMM: MONTH_NAMES_FULL[month] ?? MONTH_NAMES_FULL[0],
61
- MMM: MONTH_NAMES_SHORT[month] ?? MONTH_NAMES_SHORT[0],
62
- MM: (month + 1).toString().padStart(2, "0"),
63
- M: (month + 1).toString(),
64
- dddd: DAY_NAMES_FULL[dayOfWeek] ?? DAY_NAMES_FULL[0],
65
- ddd: DAY_NAMES_SHORT[dayOfWeek] ?? DAY_NAMES_SHORT[0],
66
- DD: day.toString().padStart(2, "0"),
67
- D: day.toString(),
68
- "AM/PM": isPM ? "PM" : "AM",
69
- "am/pm": isPM ? "pm" : "am",
70
- HH: hours.toString().padStart(2, "0"),
71
- H: hours.toString(),
72
- hh: (uses12Hour ? hours12 : hours).toString().padStart(2, "0"),
73
- h: (uses12Hour ? hours12 : hours).toString(),
74
- mm: minutes.toString().padStart(2, "0"),
75
- ss: seconds.toString().padStart(2, "0"),
76
- };
77
-
78
- return format.replace(DATE_TOKEN_RE, (token) => replacements[token] ?? token);
79
- }
80
-
81
- const THOUSANDS_GROUP_SIZE = 3;
82
-
83
- /**
84
- * Insert thousands separators into a run of digits ("1234567" → "1,234,567").
85
- * Regex free on purpose: the classic `\B(?=(\d{3})+(?!\d))` lookahead
86
- * backtracks badly on a long run of digits.
87
- */
88
- export const groupThousands = (digits: string): string =>
89
- Array.from(digits).reduce((grouped, digit, index) => {
90
- const needsSeparator = index > 0 && (digits.length - index) % THOUSANDS_GROUP_SIZE === 0;
91
- return needsSeparator ? `${grouped},${digit}` : `${grouped}${digit}`;
92
- }, "");
93
-
94
- /** Group the integer part of a formatted number ("1234567.89" → "1,234,567.89"),
95
- * leaving any fractional part after the decimal point untouched. */
96
- export const addThousandSeparators = (formatted: string): string => {
97
- const [whole, ...fraction] = formatted.split(".");
98
- if (whole === undefined) return formatted;
99
- return [groupThousands(whole), ...fraction].join(".");
100
- };
101
-
102
- /**
103
- * Format a number according to Excel format code
104
- *
105
- * Supported formats:
106
- * - Currency: $#,##0.00, $#,##0
107
- * - Percentage: 0.00%, 0.0%
108
- * - Integer with commas: #,##0
109
- * - Decimal: 0.00, 0.000
110
- * - Dates: MM/DD/YYYY, DD-MMM-YYYY, etc.
111
- *
112
- * @param value - The numeric value to format
113
- * @param format - The Excel format code
114
- * @returns Formatted string representation
115
- */
116
- export function formatNumber(value: number, format: string): string {
117
- if (!format) return value.toString();
118
-
119
- try {
120
- // Check if it's a date format
121
- if (isDateFormat(format)) {
122
- return formatDate(value, format);
123
- }
124
-
125
- // Handle currency formats
126
- if (format.includes("$")) {
127
- const decimals = (format.match(/\.0+/) || [""])[0].length - 1;
128
- const magnitude = Math.abs(value).toFixed(decimals >= 0 ? decimals : 0);
129
- const grouped = format.includes(",") ? addThousandSeparators(magnitude) : magnitude;
130
- const withSymbol = "$" + grouped;
131
- return value < 0 ? "-" + withSymbol : withSymbol;
132
- }
133
-
134
- // Handle percentage
135
- if (format.includes("%")) {
136
- const decimals = (format.match(/\.0+/) || [""])[0].length - 1;
137
- return (value * 100).toFixed(decimals >= 0 ? decimals : 2) + "%";
138
- }
139
-
140
- // Handle comma separator
141
- if (format.includes(",")) {
142
- const decimals = (format.match(/\.0+/) || [""])[0].length - 1;
143
- const grouped = addThousandSeparators(Math.abs(value).toFixed(decimals >= 0 ? decimals : 0));
144
- return value < 0 ? "-" + grouped : grouped;
145
- }
146
-
147
- // Handle decimal places
148
- const decimals = (format.match(/\.0+/) || [""])[0].length - 1;
149
- if (decimals >= 0) {
150
- return value.toFixed(decimals);
151
- }
152
-
153
- return value.toString();
154
- } catch (error) {
155
- console.error("Format error:", error);
156
- return value.toString();
157
- }
158
- }
@@ -1,67 +0,0 @@
1
- /**
2
- * Typed formula-evaluation errors
3
- *
4
- * A failed formula must surface as a typed error with an Excel-style error value
5
- * in the cell, never as a swallowed bare string (issue #2359). This module holds
6
- * the error taxonomy, the throwable carrier, and the pure helpers that classify a
7
- * thrown error and propagate an error value across cell references. No engine
8
- * state is captured — every export is input → output only.
9
- */
10
-
11
- import type { CalculationError } from "./types";
12
- import type { SpreadsheetErrorCode } from "./spreadsheet-errors";
13
-
14
- /** The evaluator-facing error kinds: every `CalculationError["type"]` except
15
- * `circular`, which the calculator raises directly (not via a thrown error). */
16
- export type FormulaErrorType = Exclude<CalculationError["type"], "circular">;
17
-
18
- /** The error code shown for each kind, matching Excel's error literals. */
19
- export const FORMULA_ERROR_VALUES: Record<FormulaErrorType, SpreadsheetErrorCode> = {
20
- div_zero: "#DIV/0!",
21
- invalid_ref: "#REF!",
22
- syntax: "#NAME?",
23
- unknown: "#ERROR!",
24
- };
25
-
26
- /** Reverse map for propagation: an error code back to the kind it represents.
27
- * Codes without a dedicated kind (`#N/A`, `#NUM!`, …) propagate as `unknown`. */
28
- const ERROR_VALUE_TO_TYPE: Record<string, FormulaErrorType> = {
29
- [FORMULA_ERROR_VALUES.div_zero]: "div_zero",
30
- [FORMULA_ERROR_VALUES.invalid_ref]: "invalid_ref",
31
- [FORMULA_ERROR_VALUES.syntax]: "syntax",
32
- };
33
-
34
- /** A recoverable formula failure carrying the kind and the code to show. */
35
- export class FormulaError extends Error {
36
- constructor(
37
- readonly errorType: FormulaErrorType,
38
- readonly display: SpreadsheetErrorCode,
39
- message?: string,
40
- ) {
41
- super(message ?? display);
42
- this.name = "FormulaError";
43
- }
44
- }
45
-
46
- export const isFormulaError = (error: unknown): error is FormulaError => error instanceof FormulaError;
47
-
48
- export const divZeroError = (): FormulaError => new FormulaError("div_zero", FORMULA_ERROR_VALUES.div_zero, "Division by zero");
49
-
50
- export const invalidRefError = (ref: string): FormulaError => new FormulaError("invalid_ref", FORMULA_ERROR_VALUES.invalid_ref, `Invalid reference: ${ref}`);
51
-
52
- export const nameError = (funcName: string): FormulaError => new FormulaError("syntax", FORMULA_ERROR_VALUES.syntax, `Unknown function: ${funcName}`);
53
-
54
- export const unknownError = (message?: string): FormulaError => new FormulaError("unknown", FORMULA_ERROR_VALUES.unknown, message);
55
-
56
- /** The FormulaError that re-raises an error read from a referenced cell or a
57
- * nested call, so `=A1+1` inherits A1's error instead of corrupting into
58
- * `"#DIV/0!"+1`. */
59
- export const propagatedError = (value: SpreadsheetErrorCode): FormulaError =>
60
- new FormulaError(ERROR_VALUE_TO_TYPE[value] ?? "unknown", value, `Propagated error: ${value}`);
61
-
62
- /** Map any thrown value onto the typed entry the calculator records. A
63
- * `FormulaError` keeps its own kind and display; anything else is `unknown`. */
64
- export const classifyThrownError = (error: unknown): { type: FormulaErrorType; display: SpreadsheetErrorCode } => {
65
- if (isFormulaError(error)) return { type: error.errorType, display: error.display };
66
- return { type: "unknown", display: FORMULA_ERROR_VALUES.unknown };
67
- };
@@ -1,202 +0,0 @@
1
- /**
2
- * Extract the set of cells that a formula references.
3
- *
4
- * Extracted from `src/plugins/spreadsheet/View.vue` (was the body of
5
- * `extractCellReferences`, cognitive complexity 32). The original
6
- * function combined regex scanning, range expansion, single-cell
7
- * parsing, and deduplication all in one body; splitting each concern
8
- * into a named helper brings the top-level function well under the
9
- * sonarjs/cognitive-complexity threshold of 15 and makes the pure
10
- * logic unit-testable in isolation (see
11
- * `test/plugins/spreadsheet/engine/test_formulaRefs.ts`).
12
- *
13
- * Tracks #175. No behavioural change — the wrapper in View.vue
14
- * still returns exactly the same `{ row, col }` list as before.
15
- */
16
-
17
- import { columnToIndex } from "./parser.js";
18
-
19
- export interface CellCoord {
20
- row: number;
21
- col: number;
22
- }
23
-
24
- // `A1:B10`, `$A$1:$B$10`, `Sheet` refs are out of scope here — the
25
- // caller only passes the formula body, and cross-sheet ranges never
26
- // reached the original regex anyway. Keeping the patterns identical
27
- // to the pre-refactor code preserves behaviour exactly.
28
- const RANGE_REGEX = /\$?[A-Z]+\$?\d+:\$?[A-Z]+\$?\d+/g;
29
- const CELL_REGEX = /\$?[A-Z]+\$?\d+/g;
30
-
31
- // Excel formulas start with `=`. Strip it for uniform handling.
32
- // Keeps any inner `=` intact (Excel does not allow them but the
33
- // caller may pass partial text during live editing).
34
- export function stripFormulaPrefix(formula: string): string {
35
- return formula.startsWith("=") ? formula.slice(1) : formula;
36
- }
37
-
38
- const A1_REGEX = /^([A-Z]+)(\d+)$/;
39
-
40
- // One `A1` token as its column index (0-based) and its row NUMBER as written
41
- // (1-based). The single place a column letter + row digit pair is read, so the
42
- // range and single-cell parsers below cannot drift apart.
43
- function parseA1Token(token: string): { col: number; rowNumber: number } | null {
44
- const [, column, row] = token.match(A1_REGEX) ?? [];
45
- if (column === undefined || row === undefined) return null;
46
- return { col: columnToIndex(column), rowNumber: parseInt(row, 10) };
47
- }
48
-
49
- // The two endpoints of an `A1:B3` range, or null for anything that is not
50
- // exactly two `A1` tokens joined by one colon.
51
- function parseRangeEndpoints(body: string): { start: { col: number; rowNumber: number }; end: { col: number; rowNumber: number } } | null {
52
- const [startToken, endToken, ...extra] = body.split(":");
53
- if (startToken === undefined || endToken === undefined || extra.length > 0) return null;
54
- const start = parseA1Token(startToken);
55
- const end = parseA1Token(endToken);
56
- return start && end ? { start, end } : null;
57
- }
58
-
59
- // Expand a single range token (`A1:B3`, `$A$1:$C$5`) into every
60
- // coordinate the range covers. Returns an empty array for malformed
61
- // input so callers never have to handle exceptions; the worst case
62
- // is "we silently ignored a weird-looking substring," which matches
63
- // the original inline behaviour.
64
- export function expandRange(rangeStr: string): CellCoord[] {
65
- const endpoints = parseRangeEndpoints(rangeStr.replace(/\$/g, ""));
66
- if (!endpoints) return [];
67
- const { start, end } = endpoints;
68
- const cells: CellCoord[] = [];
69
- for (let row = start.rowNumber - 1; row <= end.rowNumber - 1; row++) {
70
- for (let col = start.col; col <= end.col; col++) {
71
- cells.push({ row, col });
72
- }
73
- }
74
- return cells;
75
- }
76
-
77
- // Expand a range OR a single cell into coordinates, upcasing first so
78
- // lowercase references (`a1:b2`, which spreadsheets accept) are not dropped,
79
- // and falling back to a single cell when there is no colon. `collectRangeValues`
80
- // in the calculator used a range-only, case-sensitive regex, so `A1`,
81
- // `$A$1:$A$10` and `a1:a10` all silently produced no values (#2356). Ordering
82
- // matches `expandRange`: top-to-bottom, left-to-right.
83
- export function expandRangeOrCell(ref: string): CellCoord[] | null {
84
- const upper = ref.trim().toUpperCase();
85
- if (upper.includes(":")) {
86
- const cells = expandRange(upper);
87
- return cells.length > 0 ? cells : null;
88
- }
89
- const single = parseSingleCellRef(upper);
90
- return single ? [single] : null;
91
- }
92
-
93
- // Parse a single cell ref (`A1`, `$A$1`, `AA100`) into a coord.
94
- // Returns null for malformed input rather than throwing — keeps the
95
- // caller's loop flat (the engine-layer `parseCellRef` throws, which
96
- // is fine for the evaluator but wrong for a best-effort scanner).
97
- export function parseSingleCellRef(refStr: string): CellCoord | null {
98
- const parsed = parseA1Token(refStr.replace(/\$/g, ""));
99
- return parsed ? { col: parsed.col, row: parsed.rowNumber - 1 } : null;
100
- }
101
-
102
- // Numeric bounds of a `A2:C10` range, with any `Sheet1!` / `'My Sheet'!`
103
- // prefix kept verbatim so callers can rebuild sheet-qualified refs. Columns
104
- // are 0-based (via `columnToIndex`); rows stay 1-based, matching A1 notation.
105
- export interface RangeBounds {
106
- sheetPrefix: string;
107
- startCol: number;
108
- startRow: number;
109
- endCol: number;
110
- endRow: number;
111
- }
112
-
113
- // Split an optional sheet prefix from a range, then parse the `A2:C10` body.
114
- // The prefix is everything up to and including the last `!`, so a quoted sheet
115
- // name that itself contains no `!` (the common case) is preserved intact. The
116
- // lookup functions each carried their own copy of this parse; one copy ran a
117
- // sheet-unaware regex against the whole string and threw on `Sheet1!A2:C10`
118
- // before the sheet-aware copy could run (#2390). Returns null for anything that
119
- // is not a two-endpoint range so callers surface one "Invalid range" message.
120
- export function parseRangeBounds(range: string): RangeBounds | null {
121
- const bang = range.lastIndexOf("!");
122
- const sheetPrefix = bang >= 0 ? range.slice(0, bang + 1) : "";
123
- const body = bang >= 0 ? range.slice(bang + 1) : range;
124
- const endpoints = parseRangeEndpoints(body);
125
- if (!endpoints) return null;
126
- const { start, end } = endpoints;
127
- return { sheetPrefix, startCol: start.col, startRow: start.rowNumber, endCol: end.col, endRow: end.rowNumber };
128
- }
129
-
130
- // Excel's `0` row/column index selects the entire row/column. This scalar engine
131
- // returns a single cell, so `0` is representable only when that dimension is one
132
- // line long; otherwise it is out of range.
133
- const WHOLE_LINE_INDEX = 0;
134
- const FIRST_INDEX = 1;
135
-
136
- // Map a 1-based INDEX position onto a 0-based offset within a `size`-long line,
137
- // truncating toward zero as Excel does. Returns null when the position falls
138
- // outside the line — including a `0` (whole-line) request the scalar engine
139
- // cannot collapse to one cell. Callers turn null into #REF!.
140
- function lineOffset(position: number, size: number): number | null {
141
- const index = Math.trunc(position);
142
- if (!Number.isFinite(index)) return null;
143
- if (index === WHOLE_LINE_INDEX) return size === 1 ? 0 : null;
144
- if (index < FIRST_INDEX || index > size) return null;
145
- return index - FIRST_INDEX;
146
- }
147
-
148
- // Resolve INDEX(range, rowNum, colNum)'s 1-based position to an absolute cell
149
- // within the range, or null (→ #REF!) when it falls outside. The former handler
150
- // computed the target with no bounds check, so an out-of-range index silently
151
- // read a cell outside the range (#2390: `INDEX(A1:A3,5)` read A5, `INDEX(A2:B5,
152
- // 0,1)` read A1). Returned column is 0-based; row is 1-based (A1 notation).
153
- export function resolveIndexTarget(bounds: RangeBounds, rowNum: number, colNum: number): { colIndex: number; row: number } | null {
154
- const rowOffset = lineOffset(rowNum, bounds.endRow - bounds.startRow + 1);
155
- const colOffset = lineOffset(colNum, bounds.endCol - bounds.startCol + 1);
156
- if (rowOffset === null || colOffset === null) return null;
157
- return { colIndex: bounds.startCol + colOffset, row: bounds.startRow + rowOffset };
158
- }
159
-
160
- // VLOOKUP's col_index_num / HLOOKUP's row_index_num as a 0-based offset inside
161
- // the table, or null (→ #REF!) when it points outside. Excel rejects an index
162
- // past the table's width; without the check the handler addressed a cell beyond
163
- // the range and returned whatever lived there — usually a silent 0 (#2360).
164
- export function resolveTableOffset(position: number, size: number): number | null {
165
- // Not `lineOffset`: INDEX reads a `0` position as "the whole line", which it
166
- // can collapse to one cell when the line is one long. A lookup index has no
167
- // such meaning — VLOOKUP's columns are numbered from 1 — so `0` is out of
168
- // range even for a single-column table (Codex review).
169
- const index = Math.trunc(position);
170
- if (!Number.isFinite(index)) return null;
171
- if (index < FIRST_INDEX || index > size) return null;
172
- return index - FIRST_INDEX;
173
- }
174
-
175
- // Top-level: scan the formula, expand any ranges, then pick up
176
- // remaining single-cell refs, deduplicating as we go. Kept short
177
- // (~15 lines) so the cognitive-complexity signal lands on the
178
- // helpers if anything grows here.
179
- export function extractCellReferences(formula: string): CellCoord[] {
180
- const clean = stripFormulaPrefix(formula);
181
- const refs: CellCoord[] = [];
182
- const seen = new Set<string>();
183
- const addUnique = (coord: CellCoord): void => {
184
- const key = `${coord.row},${coord.col}`;
185
- if (seen.has(key)) return;
186
- seen.add(key);
187
- refs.push(coord);
188
- };
189
-
190
- for (const range of clean.match(RANGE_REGEX) ?? []) {
191
- for (const coord of expandRange(range)) addUnique(coord);
192
- }
193
- // Strip matched ranges so the cell-regex doesn't re-emit their
194
- // endpoints as standalone refs (mirrors the original's second
195
- // `.replace(rangeRegex, "")` pass).
196
- const withoutRanges = clean.replace(RANGE_REGEX, "");
197
- for (const cellStr of withoutRanges.match(CELL_REGEX) ?? []) {
198
- const coord = parseSingleCellRef(cellStr);
199
- if (coord) addUnique(coord);
200
- }
201
- return refs;
202
- }