@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.
- package/README.md +32 -0
- package/dist/canvas/cell-renderer.d.ts +45 -0
- package/dist/canvas/cell-renderer.d.ts.map +1 -0
- package/dist/canvas/grid-renderer.d.ts +29 -0
- package/dist/canvas/grid-renderer.d.ts.map +1 -0
- package/dist/canvas/header-renderer.d.ts +58 -0
- package/dist/canvas/header-renderer.d.ts.map +1 -0
- package/dist/canvas/hit-testing.d.ts +81 -0
- package/dist/canvas/hit-testing.d.ts.map +1 -0
- package/dist/canvas/index.d.ts +9 -0
- package/dist/canvas/index.d.ts.map +1 -0
- package/dist/canvas/renderer.d.ts +140 -0
- package/dist/canvas/renderer.d.ts.map +1 -0
- package/dist/canvas/selection-renderer.d.ts +55 -0
- package/dist/canvas/selection-renderer.d.ts.map +1 -0
- package/dist/canvas/text-renderer.d.ts +49 -0
- package/dist/canvas/text-renderer.d.ts.map +1 -0
- package/dist/canvas/types.d.ts +200 -0
- package/dist/canvas/types.d.ts.map +1 -0
- package/dist/collaboration/firebase-provider.d.ts +13 -0
- package/dist/collaboration/firebase-provider.d.ts.map +1 -0
- package/dist/collaboration/index.d.ts +3 -0
- package/dist/collaboration/index.d.ts.map +1 -0
- package/dist/collaboration/types.d.ts +34 -0
- package/dist/collaboration/types.d.ts.map +1 -0
- package/dist/event-emitter.d.ts +13 -0
- package/dist/event-emitter.d.ts.map +1 -0
- package/dist/export/csv.d.ts +5 -0
- package/dist/export/csv.d.ts.map +1 -0
- package/dist/export/index.d.ts +2 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/features/filter.d.ts +58 -0
- package/dist/features/filter.d.ts.map +1 -0
- package/dist/features/freeze.d.ts +86 -0
- package/dist/features/freeze.d.ts.map +1 -0
- package/dist/features/index.d.ts +4 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/sort.d.ts +15 -0
- package/dist/features/sort.d.ts.map +1 -0
- package/dist/format-pool.d.ts +17 -0
- package/dist/format-pool.d.ts.map +1 -0
- package/dist/formula-graph.d.ts +12 -0
- package/dist/formula-graph.d.ts.map +1 -0
- package/dist/formula-parser/cell-reference.d.ts +7 -0
- package/dist/formula-parser/cell-reference.d.ts.map +1 -0
- package/dist/formula-parser/formula-adjust.d.ts +13 -0
- package/dist/formula-parser/formula-adjust.d.ts.map +1 -0
- package/dist/formula-parser/formula-ranges.d.ts +22 -0
- package/dist/formula-parser/formula-ranges.d.ts.map +1 -0
- package/dist/formula-parser/index.d.ts +6 -0
- package/dist/formula-parser/index.d.ts.map +1 -0
- package/dist/formula-parser/parser.d.ts +18 -0
- package/dist/formula-parser/parser.d.ts.map +1 -0
- package/dist/formula-parser/types.d.ts +33 -0
- package/dist/formula-parser/types.d.ts.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +5823 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +5885 -0
- package/dist/index.js.map +1 -0
- package/dist/sheet.d.ts +119 -0
- package/dist/sheet.d.ts.map +1 -0
- package/dist/style-pool.d.ts +17 -0
- package/dist/style-pool.d.ts.map +1 -0
- package/dist/types.d.ts +260 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/cell-key.d.ts +7 -0
- package/dist/utils/cell-key.d.ts.map +1 -0
- package/dist/utils/format-utils.d.ts +75 -0
- package/dist/utils/format-utils.d.ts.map +1 -0
- package/dist/utils/range.d.ts +13 -0
- package/dist/utils/range.d.ts.map +1 -0
- package/dist/workbook.d.ts +155 -0
- package/dist/workbook.d.ts.map +1 -0
- package/package.json +46 -0
- package/src/canvas/cell-renderer.ts +181 -0
- package/src/canvas/grid-renderer.ts +238 -0
- package/src/canvas/header-renderer.ts +402 -0
- package/src/canvas/hit-testing.ts +537 -0
- package/src/canvas/index.ts +16 -0
- package/src/canvas/renderer.ts +1056 -0
- package/src/canvas/selection-renderer.ts +604 -0
- package/src/canvas/text-renderer.ts +321 -0
- package/src/canvas/types.ts +289 -0
- package/src/collaboration/firebase-provider.ts +48 -0
- package/src/collaboration/index.ts +5 -0
- package/src/collaboration/types.ts +38 -0
- package/src/event-emitter.ts +73 -0
- package/src/export/csv.ts +101 -0
- package/src/export/index.ts +4 -0
- package/src/features/filter.ts +231 -0
- package/src/features/freeze.ts +271 -0
- package/src/features/index.ts +5 -0
- package/src/features/sort.ts +282 -0
- package/src/format-pool.ts +61 -0
- package/src/formula-graph.ts +84 -0
- package/src/formula-parser/cell-reference.ts +99 -0
- package/src/formula-parser/formula-adjust.ts +129 -0
- package/src/formula-parser/formula-ranges.ts +159 -0
- package/src/formula-parser/index.ts +8 -0
- package/src/formula-parser/parser.ts +438 -0
- package/src/formula-parser/types.ts +39 -0
- package/src/index.ts +25 -0
- package/src/sheet.ts +502 -0
- package/src/style-pool.ts +62 -0
- package/src/types.ts +291 -0
- package/src/utils/cell-key.ts +19 -0
- package/src/utils/format-utils.ts +515 -0
- package/src/utils/range.ts +53 -0
- 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,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
|
+
|