@pagent-libs/core 0.1.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 (111) hide show
  1. package/README.md +32 -0
  2. package/dist/canvas/cell-renderer.d.ts +45 -0
  3. package/dist/canvas/cell-renderer.d.ts.map +1 -0
  4. package/dist/canvas/grid-renderer.d.ts +29 -0
  5. package/dist/canvas/grid-renderer.d.ts.map +1 -0
  6. package/dist/canvas/header-renderer.d.ts +58 -0
  7. package/dist/canvas/header-renderer.d.ts.map +1 -0
  8. package/dist/canvas/hit-testing.d.ts +81 -0
  9. package/dist/canvas/hit-testing.d.ts.map +1 -0
  10. package/dist/canvas/index.d.ts +9 -0
  11. package/dist/canvas/index.d.ts.map +1 -0
  12. package/dist/canvas/renderer.d.ts +140 -0
  13. package/dist/canvas/renderer.d.ts.map +1 -0
  14. package/dist/canvas/selection-renderer.d.ts +55 -0
  15. package/dist/canvas/selection-renderer.d.ts.map +1 -0
  16. package/dist/canvas/text-renderer.d.ts +49 -0
  17. package/dist/canvas/text-renderer.d.ts.map +1 -0
  18. package/dist/canvas/types.d.ts +200 -0
  19. package/dist/canvas/types.d.ts.map +1 -0
  20. package/dist/collaboration/firebase-provider.d.ts +13 -0
  21. package/dist/collaboration/firebase-provider.d.ts.map +1 -0
  22. package/dist/collaboration/index.d.ts +3 -0
  23. package/dist/collaboration/index.d.ts.map +1 -0
  24. package/dist/collaboration/types.d.ts +34 -0
  25. package/dist/collaboration/types.d.ts.map +1 -0
  26. package/dist/event-emitter.d.ts +13 -0
  27. package/dist/event-emitter.d.ts.map +1 -0
  28. package/dist/export/csv.d.ts +5 -0
  29. package/dist/export/csv.d.ts.map +1 -0
  30. package/dist/export/index.d.ts +2 -0
  31. package/dist/export/index.d.ts.map +1 -0
  32. package/dist/features/filter.d.ts +58 -0
  33. package/dist/features/filter.d.ts.map +1 -0
  34. package/dist/features/freeze.d.ts +86 -0
  35. package/dist/features/freeze.d.ts.map +1 -0
  36. package/dist/features/index.d.ts +4 -0
  37. package/dist/features/index.d.ts.map +1 -0
  38. package/dist/features/sort.d.ts +15 -0
  39. package/dist/features/sort.d.ts.map +1 -0
  40. package/dist/format-pool.d.ts +17 -0
  41. package/dist/format-pool.d.ts.map +1 -0
  42. package/dist/formula-graph.d.ts +12 -0
  43. package/dist/formula-graph.d.ts.map +1 -0
  44. package/dist/formula-parser/cell-reference.d.ts +7 -0
  45. package/dist/formula-parser/cell-reference.d.ts.map +1 -0
  46. package/dist/formula-parser/formula-adjust.d.ts +13 -0
  47. package/dist/formula-parser/formula-adjust.d.ts.map +1 -0
  48. package/dist/formula-parser/formula-ranges.d.ts +22 -0
  49. package/dist/formula-parser/formula-ranges.d.ts.map +1 -0
  50. package/dist/formula-parser/index.d.ts +6 -0
  51. package/dist/formula-parser/index.d.ts.map +1 -0
  52. package/dist/formula-parser/parser.d.ts +18 -0
  53. package/dist/formula-parser/parser.d.ts.map +1 -0
  54. package/dist/formula-parser/types.d.ts +33 -0
  55. package/dist/formula-parser/types.d.ts.map +1 -0
  56. package/dist/index.d.ts +15 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.esm.js +5823 -0
  59. package/dist/index.esm.js.map +1 -0
  60. package/dist/index.js +5885 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/sheet.d.ts +119 -0
  63. package/dist/sheet.d.ts.map +1 -0
  64. package/dist/style-pool.d.ts +17 -0
  65. package/dist/style-pool.d.ts.map +1 -0
  66. package/dist/types.d.ts +260 -0
  67. package/dist/types.d.ts.map +1 -0
  68. package/dist/utils/cell-key.d.ts +7 -0
  69. package/dist/utils/cell-key.d.ts.map +1 -0
  70. package/dist/utils/format-utils.d.ts +75 -0
  71. package/dist/utils/format-utils.d.ts.map +1 -0
  72. package/dist/utils/range.d.ts +13 -0
  73. package/dist/utils/range.d.ts.map +1 -0
  74. package/dist/workbook.d.ts +155 -0
  75. package/dist/workbook.d.ts.map +1 -0
  76. package/package.json +46 -0
  77. package/src/canvas/cell-renderer.ts +181 -0
  78. package/src/canvas/grid-renderer.ts +238 -0
  79. package/src/canvas/header-renderer.ts +402 -0
  80. package/src/canvas/hit-testing.ts +537 -0
  81. package/src/canvas/index.ts +16 -0
  82. package/src/canvas/renderer.ts +1056 -0
  83. package/src/canvas/selection-renderer.ts +604 -0
  84. package/src/canvas/text-renderer.ts +321 -0
  85. package/src/canvas/types.ts +289 -0
  86. package/src/collaboration/firebase-provider.ts +48 -0
  87. package/src/collaboration/index.ts +5 -0
  88. package/src/collaboration/types.ts +38 -0
  89. package/src/event-emitter.ts +73 -0
  90. package/src/export/csv.ts +101 -0
  91. package/src/export/index.ts +4 -0
  92. package/src/features/filter.ts +231 -0
  93. package/src/features/freeze.ts +271 -0
  94. package/src/features/index.ts +5 -0
  95. package/src/features/sort.ts +282 -0
  96. package/src/format-pool.ts +61 -0
  97. package/src/formula-graph.ts +84 -0
  98. package/src/formula-parser/cell-reference.ts +99 -0
  99. package/src/formula-parser/formula-adjust.ts +129 -0
  100. package/src/formula-parser/formula-ranges.ts +159 -0
  101. package/src/formula-parser/index.ts +8 -0
  102. package/src/formula-parser/parser.ts +438 -0
  103. package/src/formula-parser/types.ts +39 -0
  104. package/src/index.ts +25 -0
  105. package/src/sheet.ts +502 -0
  106. package/src/style-pool.ts +62 -0
  107. package/src/types.ts +291 -0
  108. package/src/utils/cell-key.ts +19 -0
  109. package/src/utils/format-utils.ts +515 -0
  110. package/src/utils/range.ts +53 -0
  111. package/src/workbook.ts +1031 -0
@@ -0,0 +1,159 @@
1
+ // Utility to extract cell references and ranges from formula strings
2
+
3
+ import { parseCellReference, parseRangeReference } from './cell-reference';
4
+ import type { CellReference, RangeReference } from './types';
5
+
6
+ export interface FormulaRange {
7
+ type: 'cell' | 'range';
8
+ cellRef?: CellReference;
9
+ rangeRef?: RangeReference;
10
+ startRow: number;
11
+ startCol: number;
12
+ endRow: number;
13
+ endCol: number;
14
+ sheetName?: string; // Sheet name if this is a cross-sheet reference
15
+ }
16
+
17
+ /**
18
+ * Extracts all cell references and ranges from a formula string.
19
+ * Returns an array of ranges, where each range represents either:
20
+ * - A single cell (startRow === endRow && startCol === endCol)
21
+ * - A range of cells (e.g., A1:A5)
22
+ *
23
+ * @param formula The formula string (e.g., "=SUM(A1:A5)+B2")
24
+ * @returns Array of FormulaRange objects
25
+ */
26
+ export function extractFormulaRanges(formula: string): FormulaRange[] {
27
+ if (!formula.startsWith('=')) {
28
+ return [];
29
+ }
30
+
31
+ const expression = formula.slice(1); // Remove leading =
32
+ const ranges: FormulaRange[] = [];
33
+
34
+ // Match patterns like:
35
+ // - Single cells: A1, $A1, A$1, $A$1, Sheet1!A1, 'Sheet 1'!A1
36
+ // - Ranges: A1:A5, $A$1:$B$10, Sheet1!A1:A5, 'Sheet 1'!A1:A5
37
+ // This regex matches cell references and ranges, including cross-sheet references
38
+ // We need to match cross-sheet references first, then same-sheet references
39
+ // Pattern for cross-sheet: ('sheet name'|sheetname)!cellref(:cellref)?
40
+ // Pattern for same-sheet: cellref(:cellref)?
41
+
42
+ // First, try to match cross-sheet references
43
+ const crossSheetPattern = /(?:'([^']+)'|([^!'"]+))!(\$?[A-Z]+\$?\d+)(?::(\$?[A-Z]+\$?\d+))?/g;
44
+ let match;
45
+ const processedIndices = new Set<number>();
46
+
47
+ // Process cross-sheet references first
48
+ while ((match = crossSheetPattern.exec(expression)) !== null) {
49
+ const sheetName = match[1] || match[2]; // Use quoted name if present, otherwise unquoted
50
+ const startRef = match[3];
51
+ const endRef = match[4]; // undefined for single cells, defined for ranges
52
+ const matchIndex = match.index;
53
+
54
+ if (endRef) {
55
+ // It's a range (e.g., Sheet1!A1:A5)
56
+ const rangeRef = parseRangeReference(`${sheetName}!${startRef}:${endRef}`);
57
+ if (rangeRef) {
58
+ const { start, end } = rangeRef;
59
+ const startRow = start.row;
60
+ const startCol = start.col;
61
+ const endRow = end.row;
62
+ const endCol = end.col;
63
+
64
+ ranges.push({
65
+ type: 'range',
66
+ rangeRef,
67
+ startRow: Math.min(startRow, endRow),
68
+ startCol: Math.min(startCol, endCol),
69
+ endRow: Math.max(startRow, endRow),
70
+ endCol: Math.max(startCol, endCol),
71
+ sheetName,
72
+ });
73
+ processedIndices.add(matchIndex);
74
+ }
75
+ } else {
76
+ // It's a single cell (e.g., Sheet1!A1)
77
+ const cellRef = parseCellReference(`${sheetName}!${startRef}`);
78
+ if (cellRef) {
79
+ const row = cellRef.row;
80
+ const col = cellRef.col;
81
+
82
+ ranges.push({
83
+ type: 'cell',
84
+ cellRef,
85
+ startRow: row,
86
+ startCol: col,
87
+ endRow: row,
88
+ endCol: col,
89
+ sheetName,
90
+ });
91
+ processedIndices.add(matchIndex);
92
+ }
93
+ }
94
+ }
95
+
96
+ // Now match same-sheet references (those not already processed)
97
+ const sameSheetPattern = /(\$?[A-Z]+\$?\d+)(?::(\$?[A-Z]+\$?\d+))?/g;
98
+ while ((match = sameSheetPattern.exec(expression)) !== null) {
99
+ // Skip if this position was already processed as part of a cross-sheet reference
100
+ // Check if there's a ! before this match (indicating it's part of a cross-sheet ref)
101
+ const beforeMatch = expression.substring(Math.max(0, match.index - 100), match.index);
102
+ // Look for ! that's not inside quotes (simple check: ! not followed by quote)
103
+ const lastExclamation = beforeMatch.lastIndexOf('!');
104
+ if (lastExclamation !== -1) {
105
+ const afterExclamation = beforeMatch.substring(lastExclamation + 1);
106
+ // If there's no quote after the !, this is likely part of a cross-sheet reference
107
+ // But we need to be more careful - check if the ! is part of a sheet name pattern
108
+ if (!afterExclamation.match(/^['"]/)) {
109
+ // This is likely part of a cross-sheet reference, skip it
110
+ continue;
111
+ }
112
+ }
113
+
114
+ const startRef = match[1];
115
+ const endRef = match[2]; // undefined for single cells, defined for ranges
116
+
117
+ if (endRef) {
118
+ // It's a range (e.g., A1:A5)
119
+ const rangeRef = parseRangeReference(`${startRef}:${endRef}`);
120
+ if (rangeRef) {
121
+ const { start, end } = rangeRef;
122
+ const startRow = start.row;
123
+ const startCol = start.col;
124
+ const endRow = end.row;
125
+ const endCol = end.col;
126
+
127
+ ranges.push({
128
+ type: 'range',
129
+ rangeRef,
130
+ startRow: Math.min(startRow, endRow),
131
+ startCol: Math.min(startCol, endCol),
132
+ endRow: Math.max(startRow, endRow),
133
+ endCol: Math.max(startCol, endCol),
134
+ // No sheetName means it's on the same sheet as the formula
135
+ });
136
+ }
137
+ } else {
138
+ // It's a single cell (e.g., A1)
139
+ const cellRef = parseCellReference(startRef);
140
+ if (cellRef) {
141
+ const row = cellRef.row;
142
+ const col = cellRef.col;
143
+
144
+ ranges.push({
145
+ type: 'cell',
146
+ cellRef,
147
+ startRow: row,
148
+ startCol: col,
149
+ endRow: row,
150
+ endCol: col,
151
+ // No sheetName means it's on the same sheet as the formula
152
+ });
153
+ }
154
+ }
155
+ }
156
+
157
+ return ranges;
158
+ }
159
+
@@ -0,0 +1,8 @@
1
+ // Formula parser exports
2
+
3
+ export * from './types';
4
+ export * from './parser';
5
+ export * from './cell-reference';
6
+ export * from './formula-adjust';
7
+ export * from './formula-ranges';
8
+
@@ -0,0 +1,438 @@
1
+ // Formula parser - simplified but functional
2
+
3
+ import type { ParsedFormulaNode, ParseResult, EvaluationContext, RangeReference } from './types';
4
+ import { parseCellReference, parseRangeReference, cellReferenceToKey } from './cell-reference';
5
+
6
+ export class FormulaParser {
7
+ private functions: Map<string, (args: unknown[], ctx: EvaluationContext) => unknown> = new Map();
8
+
9
+ constructor() {
10
+ this.registerBuiltInFunctions();
11
+ }
12
+
13
+ private registerBuiltInFunctions(): void {
14
+ // SUM function
15
+ this.functions.set('SUM', (args, ctx) => {
16
+ const values = this.flattenArgs(args, ctx);
17
+ return values.reduce((sum: number, val) => {
18
+ const num = Number(val);
19
+ return sum + (isNaN(num) ? 0 : num);
20
+ }, 0);
21
+ });
22
+
23
+ // AVERAGE function
24
+ this.functions.set('AVERAGE', (args, ctx) => {
25
+ const values = this.flattenArgs(args, ctx);
26
+ const numbers = values.map(Number).filter((n) => !isNaN(n));
27
+ return numbers.length > 0 ? numbers.reduce((a, b) => a + b, 0) / numbers.length : 0;
28
+ });
29
+
30
+ // COUNT function
31
+ this.functions.set('COUNT', (args, ctx) => {
32
+ const values = this.flattenArgs(args, ctx);
33
+ return values.filter((v) => v != null && v !== '').length;
34
+ });
35
+
36
+ // MAX function
37
+ this.functions.set('MAX', (args, ctx) => {
38
+ const values = this.flattenArgs(args, ctx);
39
+ const numbers = values.map(Number).filter((n) => !isNaN(n));
40
+ return numbers.length > 0 ? Math.max(...numbers) : 0;
41
+ });
42
+
43
+ // MIN function
44
+ this.functions.set('MIN', (args, ctx) => {
45
+ const values = this.flattenArgs(args, ctx);
46
+ const numbers = values.map(Number).filter((n) => !isNaN(n));
47
+ return numbers.length > 0 ? Math.min(...numbers) : 0;
48
+ });
49
+
50
+ // IF function - args are already evaluated when passed to functions
51
+ this.functions.set('IF', (args, _ctx) => {
52
+ void _ctx;
53
+ if (args.length < 2) return null;
54
+ const condition = args[0];
55
+ const truthy = Boolean(condition);
56
+ return truthy ? args[1] : (args.length > 2 ? args[2] : null);
57
+ });
58
+ }
59
+
60
+ parse(formula: string, currentRow: number = 0, currentCol: number = 0): ParseResult {
61
+ const dependencies = new Set<string>();
62
+ let error: string | undefined;
63
+
64
+ try {
65
+ // Remove leading = if present
66
+ const expression = formula.startsWith('=') ? formula.slice(1) : formula;
67
+
68
+ // Simple parser - handles basic cases
69
+ const ast = this.parseExpression(expression, currentRow, currentCol, dependencies);
70
+
71
+ return { ast, dependencies, error };
72
+ } catch (e) {
73
+ error = e instanceof Error ? e.message : '#ERROR!';
74
+ return {
75
+ ast: { type: 'string', value: '' },
76
+ dependencies,
77
+ error,
78
+ };
79
+ }
80
+ }
81
+
82
+ private parseExpression(
83
+ expr: string,
84
+ currentRow: number,
85
+ currentCol: number,
86
+ dependencies: Set<string>
87
+ ): ParsedFormulaNode {
88
+ expr = expr.trim();
89
+
90
+ // Try to parse as arithmetic/comparison expression
91
+ const operatorMatch = this.findLowestPrecedenceOperator(expr);
92
+ if (operatorMatch) {
93
+ const { operator, index } = operatorMatch;
94
+ const leftExpr = expr.substring(0, index).trim();
95
+ const rightExpr = expr.substring(index + operator.length).trim();
96
+
97
+ return {
98
+ type: 'operator',
99
+ operator,
100
+ left: this.parseExpression(leftExpr, currentRow, currentCol, dependencies),
101
+ right: this.parseExpression(rightExpr, currentRow, currentCol, dependencies),
102
+ };
103
+ }
104
+
105
+ // Try to parse as cell reference
106
+ const cellRef = parseCellReference(expr);
107
+ if (cellRef) {
108
+ // For relative references, convert absolute position to offset
109
+ if (!cellRef.rowAbsolute || !cellRef.colAbsolute) {
110
+ // Calculate offset from current cell
111
+ const rowOffset = cellRef.row - currentRow;
112
+ const colOffset = cellRef.col - currentCol;
113
+ // Store as offset for relative references
114
+ const adjustedRef = {
115
+ row: cellRef.rowAbsolute ? cellRef.row : rowOffset,
116
+ col: cellRef.colAbsolute ? cellRef.col : colOffset,
117
+ rowAbsolute: cellRef.rowAbsolute,
118
+ colAbsolute: cellRef.colAbsolute,
119
+ sheetName: cellRef.sheetName, // Preserve sheet name
120
+ };
121
+ const key = cellReferenceToKey(adjustedRef, currentRow, currentCol);
122
+ // For cross-sheet references, include sheet name in dependency key
123
+ const depKey = cellRef.sheetName ? `${cellRef.sheetName}!${key}` : key;
124
+ dependencies.add(depKey);
125
+ return { type: 'cell', cellRef: adjustedRef };
126
+ }
127
+ // Absolute reference - use as-is
128
+ const key = cellReferenceToKey(cellRef, currentRow, currentCol);
129
+ // For cross-sheet references, include sheet name in dependency key
130
+ const depKey = cellRef.sheetName ? `${cellRef.sheetName}!${key}` : key;
131
+ dependencies.add(depKey);
132
+ return { type: 'cell', cellRef };
133
+ }
134
+
135
+ // Try to parse as range
136
+ const rangeRef = parseRangeReference(expr);
137
+ if (rangeRef) {
138
+ // Add all cells in range to dependencies
139
+ const { start, end } = rangeRef;
140
+
141
+ // For relative references, convert absolute position to offset (like we do for single cells)
142
+ // parseCellReference returns absolute positions, so we need to calculate the offset
143
+ const startRowOffset = start.rowAbsolute ? 0 : start.row - currentRow;
144
+ const startColOffset = start.colAbsolute ? 0 : start.col - currentCol;
145
+ const endRowOffset = end.rowAbsolute ? 0 : end.row - currentRow;
146
+ const endColOffset = end.colAbsolute ? 0 : end.col - currentCol;
147
+
148
+ // Create adjusted range reference with offsets for relative refs
149
+ const sheetName = start.sheetName || end.sheetName; // Get sheet name from either start or end
150
+ const adjustedRangeRef: RangeReference = {
151
+ start: {
152
+ row: start.rowAbsolute ? start.row : startRowOffset,
153
+ col: start.colAbsolute ? start.col : startColOffset,
154
+ rowAbsolute: start.rowAbsolute,
155
+ colAbsolute: start.colAbsolute,
156
+ sheetName: sheetName, // Preserve sheet name
157
+ },
158
+ end: {
159
+ row: end.rowAbsolute ? end.row : endRowOffset,
160
+ col: end.colAbsolute ? end.col : endColOffset,
161
+ rowAbsolute: end.rowAbsolute,
162
+ colAbsolute: end.colAbsolute,
163
+ sheetName: sheetName, // Preserve sheet name
164
+ },
165
+ };
166
+
167
+ // Calculate absolute positions for dependency tracking
168
+ const startRow = adjustedRangeRef.start.rowAbsolute
169
+ ? adjustedRangeRef.start.row
170
+ : currentRow + adjustedRangeRef.start.row;
171
+ const endRow = adjustedRangeRef.end.rowAbsolute
172
+ ? adjustedRangeRef.end.row
173
+ : currentRow + adjustedRangeRef.end.row;
174
+ const startCol = adjustedRangeRef.start.colAbsolute
175
+ ? adjustedRangeRef.start.col
176
+ : currentCol + adjustedRangeRef.start.col;
177
+ const endCol = adjustedRangeRef.end.colAbsolute
178
+ ? adjustedRangeRef.end.col
179
+ : currentCol + adjustedRangeRef.end.col;
180
+
181
+ // Add dependencies with sheet name prefix if it's a cross-sheet reference
182
+ const sheetPrefix = sheetName ? `${sheetName}!` : '';
183
+ for (let r = Math.min(startRow, endRow); r <= Math.max(startRow, endRow); r++) {
184
+ for (let c = Math.min(startCol, endCol); c <= Math.max(startCol, endCol); c++) {
185
+ dependencies.add(`${sheetPrefix}${r}:${c}`);
186
+ }
187
+ }
188
+
189
+ return { type: 'range', rangeRef: adjustedRangeRef };
190
+ }
191
+
192
+ // Try to parse as function call (e.g., SUM(A1:A10))
193
+ const functionMatch = expr.match(/^([A-Z]+)\s*\((.*)\)$/);
194
+ if (functionMatch) {
195
+ const [, funcName, argsStr] = functionMatch;
196
+ const args = this.parseArguments(argsStr, currentRow, currentCol, dependencies);
197
+ return {
198
+ type: 'function',
199
+ functionName: funcName.toUpperCase(),
200
+ args,
201
+ };
202
+ }
203
+
204
+ // Try to parse as number
205
+ const num = Number(expr);
206
+ if (!isNaN(num) && expr.trim() !== '') {
207
+ return { type: 'number', value: num };
208
+ }
209
+
210
+ // Try to parse as string (quoted)
211
+ if ((expr.startsWith('"') && expr.endsWith('"')) || (expr.startsWith("'") && expr.endsWith("'"))) {
212
+ return { type: 'string', value: expr.slice(1, -1) };
213
+ }
214
+
215
+ // Default to string
216
+ return { type: 'string', value: expr };
217
+ }
218
+
219
+ /**
220
+ * Find the operator with lowest precedence (to parse correctly)
221
+ * Returns the rightmost operator of lowest precedence to handle left-associativity
222
+ */
223
+ private findLowestPrecedenceOperator(expr: string): { operator: string; index: number } | null {
224
+ let depth = 0;
225
+ let lowestPrecOp: { operator: string; index: number } | null = null;
226
+ let lowestPrec = Infinity;
227
+
228
+ // Operator precedence (lower number = lower precedence, evaluated later):
229
+ // Comparison operators (precedence 0) < +, - (precedence 1) < *, / (precedence 2)
230
+ const operators = [
231
+ { op: '>=', prec: 0 },
232
+ { op: '<=', prec: 0 },
233
+ { op: '<>', prec: 0 },
234
+ { op: '>', prec: 0 },
235
+ { op: '<', prec: 0 },
236
+ { op: '=', prec: 0 },
237
+ { op: '+', prec: 1 },
238
+ { op: '-', prec: 1 },
239
+ { op: '*', prec: 2 },
240
+ { op: '/', prec: 2 },
241
+ ];
242
+
243
+ for (let i = 0; i < expr.length; i++) {
244
+ const char = expr[i];
245
+
246
+ // Track parentheses depth
247
+ if (char === '(') depth++;
248
+ else if (char === ')') depth--;
249
+ // Skip if inside parentheses or quotes
250
+ else if (depth > 0) continue;
251
+ else if (char === '"' || char === "'") {
252
+ // Skip quoted strings
253
+ const quote = char;
254
+ i++;
255
+ while (i < expr.length && expr[i] !== quote) {
256
+ if (expr[i] === '\\') i++; // Skip escaped characters
257
+ i++;
258
+ }
259
+ continue;
260
+ }
261
+ // Check for operators (but not at start, and not part of a number or cell reference)
262
+ else {
263
+ for (const { op, prec } of operators) {
264
+ // Check for multi-character operators first
265
+ if (op.length === 2 && i < expr.length - 1) {
266
+ const twoChars = expr.substring(i, i + 2);
267
+ if (twoChars === op) {
268
+ if (prec < lowestPrec || (prec === lowestPrec && i > (lowestPrecOp?.index ?? -1))) {
269
+ lowestPrec = prec;
270
+ lowestPrecOp = { operator: op, index: i };
271
+ }
272
+ continue;
273
+ }
274
+ }
275
+
276
+ // Single character operators
277
+ if (op.length === 1 && char === op) {
278
+ // Make sure it's not part of a number (e.g., -5 or 1e-5)
279
+ const prevChar = i > 0 ? expr[i - 1] : '';
280
+ const isPartOfNumber =
281
+ (op === '-' && (prevChar === 'e' || prevChar === 'E')) ||
282
+ (op === '+' && (prevChar === 'e' || prevChar === 'E'));
283
+
284
+ // Make sure single < or > isn't part of a two-char operator
285
+ const nextChar = i < expr.length - 1 ? expr[i + 1] : '';
286
+ const isTwoCharOp =
287
+ (op === '<' && (nextChar === '>' || nextChar === '=')) ||
288
+ (op === '>' && nextChar === '=');
289
+
290
+ if (!isPartOfNumber && !isTwoCharOp && (prec < lowestPrec || (prec === lowestPrec && i > (lowestPrecOp?.index ?? -1)))) {
291
+ lowestPrec = prec;
292
+ lowestPrecOp = { operator: op, index: i };
293
+ }
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ return lowestPrecOp;
300
+ }
301
+
302
+ private parseArguments(
303
+ argsStr: string,
304
+ currentRow: number,
305
+ currentCol: number,
306
+ dependencies: Set<string>
307
+ ): ParsedFormulaNode[] {
308
+ if (!argsStr.trim()) return [];
309
+
310
+ const args: ParsedFormulaNode[] = [];
311
+ let depth = 0;
312
+ let current = '';
313
+
314
+ for (const char of argsStr) {
315
+ if (char === '(') depth++;
316
+ else if (char === ')') depth--;
317
+ else if (char === ',' && depth === 0) {
318
+ if (current.trim()) {
319
+ args.push(this.parseExpression(current.trim(), currentRow, currentCol, dependencies));
320
+ }
321
+ current = '';
322
+ continue;
323
+ }
324
+ current += char;
325
+ }
326
+
327
+ if (current.trim()) {
328
+ args.push(this.parseExpression(current.trim(), currentRow, currentCol, dependencies));
329
+ }
330
+
331
+ return args;
332
+ }
333
+
334
+ evaluate(ast: ParsedFormulaNode, ctx: EvaluationContext, currentRow: number = 0, currentCol: number = 0): unknown {
335
+ return this.evaluateNode(ast, ctx, currentRow, currentCol);
336
+ }
337
+
338
+ private evaluateNode(
339
+ node: ParsedFormulaNode,
340
+ ctx: EvaluationContext,
341
+ currentRow: number = 0,
342
+ currentCol: number = 0
343
+ ): unknown {
344
+ switch (node.type) {
345
+ case 'number':
346
+ return node.value;
347
+ case 'string':
348
+ return node.value;
349
+ case 'cell':
350
+ if (node.cellRef) {
351
+ const ref = node.cellRef;
352
+ const row = ref.rowAbsolute ? ref.row : currentRow + ref.row;
353
+ const col = ref.colAbsolute ? ref.col : currentCol + ref.col;
354
+ // Use sheet name if provided, otherwise use sheetId from context
355
+ return ctx.getCellValue(row, col, undefined, ref.sheetName);
356
+ }
357
+ return null;
358
+ case 'range':
359
+ if (node.rangeRef) {
360
+ const sheetName = node.rangeRef.start.sheetName || node.rangeRef.end.sheetName;
361
+ return ctx.getRangeValues(node.rangeRef, undefined, sheetName);
362
+ }
363
+ return null;
364
+ case 'operator':
365
+ if (node.operator && node.left && node.right) {
366
+ const leftVal = this.evaluateNode(node.left, ctx, currentRow, currentCol);
367
+ const rightVal = this.evaluateNode(node.right, ctx, currentRow, currentCol);
368
+
369
+ // Comparison operators work with any types
370
+ switch (node.operator) {
371
+ case '>':
372
+ return Number(leftVal) > Number(rightVal);
373
+ case '<':
374
+ return Number(leftVal) < Number(rightVal);
375
+ case '>=':
376
+ return Number(leftVal) >= Number(rightVal);
377
+ case '<=':
378
+ return Number(leftVal) <= Number(rightVal);
379
+ case '=':
380
+ // Excel-style equality: compare values directly
381
+ return leftVal === rightVal || Number(leftVal) === Number(rightVal);
382
+ case '<>':
383
+ return leftVal !== rightVal && Number(leftVal) !== Number(rightVal);
384
+ }
385
+
386
+ // Arithmetic operators require numbers
387
+ const leftNum = Number(leftVal);
388
+ const rightNum = Number(rightVal);
389
+
390
+ if (isNaN(leftNum) || isNaN(rightNum)) {
391
+ throw new Error('#VALUE!');
392
+ }
393
+
394
+ switch (node.operator) {
395
+ case '+':
396
+ return leftNum + rightNum;
397
+ case '-':
398
+ return leftNum - rightNum;
399
+ case '*':
400
+ return leftNum * rightNum;
401
+ case '/':
402
+ if (rightNum === 0) {
403
+ throw new Error('#DIV/0!');
404
+ }
405
+ return leftNum / rightNum;
406
+ default:
407
+ throw new Error(`#ERROR! Unknown operator: ${node.operator}`);
408
+ }
409
+ }
410
+ return null;
411
+ case 'function':
412
+ if (node.functionName && node.args) {
413
+ const func = this.functions.get(node.functionName);
414
+ if (!func) {
415
+ throw new Error(`#NAME? Function ${node.functionName} not found`);
416
+ }
417
+ const evaluatedArgs = node.args.map((arg) => this.evaluateNode(arg, ctx, currentRow, currentCol));
418
+ return func(evaluatedArgs, ctx);
419
+ }
420
+ return null;
421
+ default:
422
+ return null;
423
+ }
424
+ }
425
+
426
+ private flattenArgs(args: unknown[], ctx: EvaluationContext): unknown[] {
427
+ const result: unknown[] = [];
428
+ for (const arg of args) {
429
+ if (Array.isArray(arg)) {
430
+ result.push(...this.flattenArgs(arg, ctx));
431
+ } else {
432
+ result.push(arg);
433
+ }
434
+ }
435
+ return result;
436
+ }
437
+ }
438
+
@@ -0,0 +1,39 @@
1
+ // Formula parser types
2
+
3
+ export interface CellReference {
4
+ row: number;
5
+ col: number;
6
+ rowAbsolute: boolean;
7
+ colAbsolute: boolean;
8
+ sheetName?: string; // Optional sheet name for cross-sheet references
9
+ }
10
+
11
+ export interface RangeReference {
12
+ start: CellReference;
13
+ end: CellReference;
14
+ }
15
+
16
+ export interface ParsedFormulaNode {
17
+ type: 'number' | 'string' | 'cell' | 'range' | 'function' | 'operator' | 'variable';
18
+ value?: unknown;
19
+ cellRef?: CellReference;
20
+ rangeRef?: RangeReference;
21
+ functionName?: string;
22
+ args?: ParsedFormulaNode[];
23
+ operator?: string;
24
+ left?: ParsedFormulaNode;
25
+ right?: ParsedFormulaNode;
26
+ }
27
+
28
+ export interface ParseResult {
29
+ ast: ParsedFormulaNode;
30
+ dependencies: Set<string>; // cellKeys
31
+ error?: string;
32
+ }
33
+
34
+ export interface EvaluationContext {
35
+ getCellValue: (row: number, col: number, sheetId?: string, sheetName?: string) => unknown;
36
+ getRangeValues: (range: RangeReference, sheetId?: string, sheetName?: string) => unknown[][];
37
+ getSheetIdByName?: (sheetName: string) => string | undefined; // Helper to resolve sheet name to sheet ID
38
+ }
39
+
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ // Core library exports
2
+
3
+ export * from './types';
4
+ export { WorkbookImpl } from './workbook';
5
+ export { SheetImpl } from './sheet';
6
+ export { StylePool } from './style-pool';
7
+ export { FormulaGraphImpl } from './formula-graph';
8
+ export { EventEmitter } from './event-emitter';
9
+ export * from './utils/cell-key';
10
+ export * from './utils/range';
11
+ export * from './utils/format-utils';
12
+ export * from './export';
13
+
14
+ // Canvas rendering module
15
+ export * from './canvas';
16
+
17
+ // Features module
18
+ export * from './features';
19
+
20
+ // Formula parser module
21
+ export * from './formula-parser';
22
+
23
+ // Collaboration module
24
+ export * from './collaboration';
25
+