@pixerate/editor 0.16.1 → 0.17.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.
@@ -72,7 +72,7 @@ interface SpreadsheetDataSource {
72
72
  updateRecord(recordId: string, field: string, value: any): Promise<void> | void;
73
73
  }
74
74
 
75
- type FormulaTokenType = 'NUMBER' | 'STRING' | 'BOOLEAN' | 'CELL_REF' | 'RANGE' | 'PROPERTY_REF' | 'IDENTIFIER' | 'OPERATOR' | 'LPAREN' | 'RPAREN' | 'COMMA' | 'EOF';
75
+ type FormulaTokenType = 'NUMBER' | 'STRING' | 'BOOLEAN' | 'ERROR' | 'CELL_REF' | 'RANGE' | 'PROPERTY_REF' | 'IDENTIFIER' | 'OPERATOR' | 'LPAREN' | 'RPAREN' | 'COMMA' | 'EOF';
76
76
  interface FormulaToken {
77
77
  type: FormulaTokenType;
78
78
  value: string;
@@ -80,6 +80,8 @@ interface FormulaToken {
80
80
  }
81
81
  declare function tokenizeFormula(formula: string): FormulaToken[];
82
82
 
83
+ /** Row number used for the open end of whole-column ranges such as `A:A`. */
84
+ declare const WHOLE_COLUMN_END_ROW = 1048576;
83
85
  type ASTNode = LiteralNode | CellRefNode | RangeNode | PropertyRefNode | BinaryOpNode | UnaryOpNode | FunctionCallNode;
84
86
  interface LiteralNode {
85
87
  type: 'Literal';
@@ -153,6 +155,9 @@ interface EvaluationContext {
153
155
  resolveRange?: (startCol: string, startRow: number, endCol: string, endRow: number) => any[];
154
156
  resolveProperty?: (property: string) => any;
155
157
  }
158
+ /** Spreadsheet error values. They propagate through operators and functions. */
159
+ declare const FORMULA_ERRORS: Set<string>;
160
+ declare function isFormulaError(value: unknown): value is string;
156
161
  declare function colNameToIndex(col: string): number;
157
162
  declare function indexToColName(index: number): string;
158
163
  declare class FormulaEvaluator {
@@ -167,11 +172,18 @@ interface CellDep {
167
172
  col: string;
168
173
  row: number;
169
174
  }
175
+ /**
176
+ * Dependency key for "every cell in column `col`", used by whole-column ranges
177
+ * (`A:A`) instead of expanding a million cell keys.
178
+ */
179
+ declare const columnDependencyKey: (col: string) => string;
170
180
  declare function extractCellDependencies(node: ASTNode): CellDep[];
171
181
  declare class DependencyGraph {
172
182
  private dependencies;
173
183
  private dependents;
174
184
  setCellFormula(cellKey: string, formulaStr: string | null): void;
185
+ /** Removes every registered formula and dependency. */
186
+ clear(): void;
175
187
  getDependencies(cellKey: string): string[];
176
188
  getDependents(cellKey: string): string[];
177
189
  /**
@@ -185,8 +197,39 @@ declare class DependencyGraph {
185
197
  order: string[];
186
198
  hasCycle: boolean;
187
199
  };
200
+ /**
201
+ * Plans a recalculation over `cellKeys` (and, with `includeDependents`, every
202
+ * cell that transitively depends on them). Returns the keys in evaluation
203
+ * order (dependencies first) and the set of keys that are part of a cycle,
204
+ * which must be marked `#CYCLE!` instead of evaluated. Every member of a
205
+ * cycle is reported, not just the cell that closed it.
206
+ */
207
+ getEvaluationPlan(cellKeys: Iterable<string>, options?: {
208
+ includeDependents?: boolean;
209
+ }): {
210
+ order: string[];
211
+ cyclic: Set<string>;
212
+ };
188
213
  }
189
214
 
215
+ /**
216
+ * A row or column insertion/deletion. `index` is the 0-based position of the
217
+ * inserted or deleted row/column.
218
+ */
219
+ interface StructuralChange {
220
+ axis: 'row' | 'col';
221
+ index: number;
222
+ type: 'insert' | 'delete';
223
+ }
224
+ /**
225
+ * Rewrites the cell and range references in `formula` after a row or column
226
+ * is inserted or deleted, the way spreadsheet apps do: references after the
227
+ * change shift, ranges grow or shrink, and references to a deleted row/column
228
+ * become `#REF!`. Absolute (`$`) markers are kept. String literals, property
229
+ * references and function names are left untouched.
230
+ */
231
+ declare function shiftFormulaReferences(formula: string, change: StructuralChange): string;
232
+
190
233
  declare class DataSourceBinder {
191
234
  private dataSources;
192
235
  register(dataSource: SpreadsheetDataSource): void;
@@ -230,6 +273,9 @@ declare class SpreadsheetController {
230
273
  private undoStack;
231
274
  private redoStack;
232
275
  private isApplyingHistory;
276
+ private batching;
277
+ private pendingCommits;
278
+ private evaluatingRowIndex;
233
279
  constructor(options?: SpreadsheetControllerOptions);
234
280
  loadDocument(document: SpreadsheetDocument): void;
235
281
  registerDataSource(ds: SpreadsheetDataSource): void;
@@ -239,10 +285,23 @@ declare class SpreadsheetController {
239
285
  getColumn(colIdOrKey: string): SpreadsheetColumn | undefined;
240
286
  getRow(rowIdOrIndex: string | number): SpreadsheetRow | undefined;
241
287
  getCell(rowId: string, colId: string): CellData;
288
+ /** Value of a cell with no stored data: bound record value, column formula, or blank. */
289
+ private getDerivedCell;
242
290
  setCellValue(rowId: string, colId: string, raw: string): void;
243
291
  private getCellKey;
292
+ /**
293
+ * Recalculates a cell and everything that depends on it, in dependency
294
+ * order. Every cell in a reference cycle is marked `#CYCLE!`.
295
+ */
244
296
  recalculateCell(rowId: string, colId: string): void;
297
+ /**
298
+ * Rebuilds the dependency graph from scratch and recalculates every formula
299
+ * in dependency order.
300
+ */
245
301
  recalculateAll(): void;
302
+ private runEvaluationPlan;
303
+ private locateCellKey;
304
+ private markCycle;
246
305
  private evalSingleCellByKey;
247
306
  private resolveCellByCoord;
248
307
  private resolveRangeByCoords;
@@ -257,9 +316,23 @@ declare class SpreadsheetController {
257
316
  deleteColumn(colId: string): void;
258
317
  insertRow(index: number, config?: Partial<SpreadsheetRow>): void;
259
318
  deleteRow(rowId: string): void;
319
+ private generateId;
320
+ /**
321
+ * Rewrites references in every cell formula (and, for column changes, every
322
+ * column formula) so they keep pointing at the same cells.
323
+ */
324
+ private shiftReferences;
260
325
  setColumnWidth(colId: string, width: number): void;
261
326
  autoFitColumnWidth(colId: string, customWidth?: number): void;
262
- exportToTsv(): string;
327
+ /**
328
+ * Exports cell values as TSV: the whole sheet, or only `range` (e.g. the
329
+ * current selection) when given.
330
+ */
331
+ exportToTsv(range?: CellRange | null): string;
332
+ /**
333
+ * Pastes tab-separated values starting at (startRow, startCol), adding rows
334
+ * and columns as needed. The whole paste is a single undo step.
335
+ */
263
336
  importFromTsv(tsvData: string, startRow?: number, startCol?: number): void;
264
337
  private recordSnapshot;
265
338
  undo(): void;
@@ -267,4 +340,4 @@ declare class SpreadsheetController {
267
340
  private notifyChange;
268
341
  }
269
342
 
270
- export { type ASTNode, type BinaryOpNode, type CellCoordinate, type CellData, type CellDep, type CellFormat, type CellRange, type CellRefNode, type ColumnBinding, type ColumnType, DataSourceBinder, DependencyGraph, type EvaluationContext, FormulaEvaluator, FormulaParser, type FormulaToken, type FormulaTokenType, type FunctionCallNode, type LiteralNode, type PropertyRefNode, type RangeNode, type RowType, type SpreadsheetColumn, SpreadsheetController, type SpreadsheetControllerOptions, type SpreadsheetDataSource, type SpreadsheetDataSourceField, type SpreadsheetDocument, type SpreadsheetRow, type UnaryOpNode, colNameToIndex, extractCellDependencies, indexToColName, parseCellReference, parseFormula, parseRange, tokenizeFormula };
343
+ export { type ASTNode, type BinaryOpNode, type CellCoordinate, type CellData, type CellDep, type CellFormat, type CellRange, type CellRefNode, type ColumnBinding, type ColumnType, DataSourceBinder, DependencyGraph, type EvaluationContext, FORMULA_ERRORS, FormulaEvaluator, FormulaParser, type FormulaToken, type FormulaTokenType, type FunctionCallNode, type LiteralNode, type PropertyRefNode, type RangeNode, type RowType, type SpreadsheetColumn, SpreadsheetController, type SpreadsheetControllerOptions, type SpreadsheetDataSource, type SpreadsheetDataSourceField, type SpreadsheetDocument, type SpreadsheetRow, type StructuralChange, type UnaryOpNode, WHOLE_COLUMN_END_ROW, colNameToIndex, columnDependencyKey, extractCellDependencies, indexToColName, isFormulaError, parseCellReference, parseFormula, parseRange, shiftFormulaReferences, tokenizeFormula };
@@ -72,7 +72,7 @@ interface SpreadsheetDataSource {
72
72
  updateRecord(recordId: string, field: string, value: any): Promise<void> | void;
73
73
  }
74
74
 
75
- type FormulaTokenType = 'NUMBER' | 'STRING' | 'BOOLEAN' | 'CELL_REF' | 'RANGE' | 'PROPERTY_REF' | 'IDENTIFIER' | 'OPERATOR' | 'LPAREN' | 'RPAREN' | 'COMMA' | 'EOF';
75
+ type FormulaTokenType = 'NUMBER' | 'STRING' | 'BOOLEAN' | 'ERROR' | 'CELL_REF' | 'RANGE' | 'PROPERTY_REF' | 'IDENTIFIER' | 'OPERATOR' | 'LPAREN' | 'RPAREN' | 'COMMA' | 'EOF';
76
76
  interface FormulaToken {
77
77
  type: FormulaTokenType;
78
78
  value: string;
@@ -80,6 +80,8 @@ interface FormulaToken {
80
80
  }
81
81
  declare function tokenizeFormula(formula: string): FormulaToken[];
82
82
 
83
+ /** Row number used for the open end of whole-column ranges such as `A:A`. */
84
+ declare const WHOLE_COLUMN_END_ROW = 1048576;
83
85
  type ASTNode = LiteralNode | CellRefNode | RangeNode | PropertyRefNode | BinaryOpNode | UnaryOpNode | FunctionCallNode;
84
86
  interface LiteralNode {
85
87
  type: 'Literal';
@@ -153,6 +155,9 @@ interface EvaluationContext {
153
155
  resolveRange?: (startCol: string, startRow: number, endCol: string, endRow: number) => any[];
154
156
  resolveProperty?: (property: string) => any;
155
157
  }
158
+ /** Spreadsheet error values. They propagate through operators and functions. */
159
+ declare const FORMULA_ERRORS: Set<string>;
160
+ declare function isFormulaError(value: unknown): value is string;
156
161
  declare function colNameToIndex(col: string): number;
157
162
  declare function indexToColName(index: number): string;
158
163
  declare class FormulaEvaluator {
@@ -167,11 +172,18 @@ interface CellDep {
167
172
  col: string;
168
173
  row: number;
169
174
  }
175
+ /**
176
+ * Dependency key for "every cell in column `col`", used by whole-column ranges
177
+ * (`A:A`) instead of expanding a million cell keys.
178
+ */
179
+ declare const columnDependencyKey: (col: string) => string;
170
180
  declare function extractCellDependencies(node: ASTNode): CellDep[];
171
181
  declare class DependencyGraph {
172
182
  private dependencies;
173
183
  private dependents;
174
184
  setCellFormula(cellKey: string, formulaStr: string | null): void;
185
+ /** Removes every registered formula and dependency. */
186
+ clear(): void;
175
187
  getDependencies(cellKey: string): string[];
176
188
  getDependents(cellKey: string): string[];
177
189
  /**
@@ -185,8 +197,39 @@ declare class DependencyGraph {
185
197
  order: string[];
186
198
  hasCycle: boolean;
187
199
  };
200
+ /**
201
+ * Plans a recalculation over `cellKeys` (and, with `includeDependents`, every
202
+ * cell that transitively depends on them). Returns the keys in evaluation
203
+ * order (dependencies first) and the set of keys that are part of a cycle,
204
+ * which must be marked `#CYCLE!` instead of evaluated. Every member of a
205
+ * cycle is reported, not just the cell that closed it.
206
+ */
207
+ getEvaluationPlan(cellKeys: Iterable<string>, options?: {
208
+ includeDependents?: boolean;
209
+ }): {
210
+ order: string[];
211
+ cyclic: Set<string>;
212
+ };
188
213
  }
189
214
 
215
+ /**
216
+ * A row or column insertion/deletion. `index` is the 0-based position of the
217
+ * inserted or deleted row/column.
218
+ */
219
+ interface StructuralChange {
220
+ axis: 'row' | 'col';
221
+ index: number;
222
+ type: 'insert' | 'delete';
223
+ }
224
+ /**
225
+ * Rewrites the cell and range references in `formula` after a row or column
226
+ * is inserted or deleted, the way spreadsheet apps do: references after the
227
+ * change shift, ranges grow or shrink, and references to a deleted row/column
228
+ * become `#REF!`. Absolute (`$`) markers are kept. String literals, property
229
+ * references and function names are left untouched.
230
+ */
231
+ declare function shiftFormulaReferences(formula: string, change: StructuralChange): string;
232
+
190
233
  declare class DataSourceBinder {
191
234
  private dataSources;
192
235
  register(dataSource: SpreadsheetDataSource): void;
@@ -230,6 +273,9 @@ declare class SpreadsheetController {
230
273
  private undoStack;
231
274
  private redoStack;
232
275
  private isApplyingHistory;
276
+ private batching;
277
+ private pendingCommits;
278
+ private evaluatingRowIndex;
233
279
  constructor(options?: SpreadsheetControllerOptions);
234
280
  loadDocument(document: SpreadsheetDocument): void;
235
281
  registerDataSource(ds: SpreadsheetDataSource): void;
@@ -239,10 +285,23 @@ declare class SpreadsheetController {
239
285
  getColumn(colIdOrKey: string): SpreadsheetColumn | undefined;
240
286
  getRow(rowIdOrIndex: string | number): SpreadsheetRow | undefined;
241
287
  getCell(rowId: string, colId: string): CellData;
288
+ /** Value of a cell with no stored data: bound record value, column formula, or blank. */
289
+ private getDerivedCell;
242
290
  setCellValue(rowId: string, colId: string, raw: string): void;
243
291
  private getCellKey;
292
+ /**
293
+ * Recalculates a cell and everything that depends on it, in dependency
294
+ * order. Every cell in a reference cycle is marked `#CYCLE!`.
295
+ */
244
296
  recalculateCell(rowId: string, colId: string): void;
297
+ /**
298
+ * Rebuilds the dependency graph from scratch and recalculates every formula
299
+ * in dependency order.
300
+ */
245
301
  recalculateAll(): void;
302
+ private runEvaluationPlan;
303
+ private locateCellKey;
304
+ private markCycle;
246
305
  private evalSingleCellByKey;
247
306
  private resolveCellByCoord;
248
307
  private resolveRangeByCoords;
@@ -257,9 +316,23 @@ declare class SpreadsheetController {
257
316
  deleteColumn(colId: string): void;
258
317
  insertRow(index: number, config?: Partial<SpreadsheetRow>): void;
259
318
  deleteRow(rowId: string): void;
319
+ private generateId;
320
+ /**
321
+ * Rewrites references in every cell formula (and, for column changes, every
322
+ * column formula) so they keep pointing at the same cells.
323
+ */
324
+ private shiftReferences;
260
325
  setColumnWidth(colId: string, width: number): void;
261
326
  autoFitColumnWidth(colId: string, customWidth?: number): void;
262
- exportToTsv(): string;
327
+ /**
328
+ * Exports cell values as TSV: the whole sheet, or only `range` (e.g. the
329
+ * current selection) when given.
330
+ */
331
+ exportToTsv(range?: CellRange | null): string;
332
+ /**
333
+ * Pastes tab-separated values starting at (startRow, startCol), adding rows
334
+ * and columns as needed. The whole paste is a single undo step.
335
+ */
263
336
  importFromTsv(tsvData: string, startRow?: number, startCol?: number): void;
264
337
  private recordSnapshot;
265
338
  undo(): void;
@@ -267,4 +340,4 @@ declare class SpreadsheetController {
267
340
  private notifyChange;
268
341
  }
269
342
 
270
- export { type ASTNode, type BinaryOpNode, type CellCoordinate, type CellData, type CellDep, type CellFormat, type CellRange, type CellRefNode, type ColumnBinding, type ColumnType, DataSourceBinder, DependencyGraph, type EvaluationContext, FormulaEvaluator, FormulaParser, type FormulaToken, type FormulaTokenType, type FunctionCallNode, type LiteralNode, type PropertyRefNode, type RangeNode, type RowType, type SpreadsheetColumn, SpreadsheetController, type SpreadsheetControllerOptions, type SpreadsheetDataSource, type SpreadsheetDataSourceField, type SpreadsheetDocument, type SpreadsheetRow, type UnaryOpNode, colNameToIndex, extractCellDependencies, indexToColName, parseCellReference, parseFormula, parseRange, tokenizeFormula };
343
+ export { type ASTNode, type BinaryOpNode, type CellCoordinate, type CellData, type CellDep, type CellFormat, type CellRange, type CellRefNode, type ColumnBinding, type ColumnType, DataSourceBinder, DependencyGraph, type EvaluationContext, FORMULA_ERRORS, FormulaEvaluator, FormulaParser, type FormulaToken, type FormulaTokenType, type FunctionCallNode, type LiteralNode, type PropertyRefNode, type RangeNode, type RowType, type SpreadsheetColumn, SpreadsheetController, type SpreadsheetControllerOptions, type SpreadsheetDataSource, type SpreadsheetDataSourceField, type SpreadsheetDocument, type SpreadsheetRow, type StructuralChange, type UnaryOpNode, WHOLE_COLUMN_END_ROW, colNameToIndex, columnDependencyKey, extractCellDependencies, indexToColName, isFormulaError, parseCellReference, parseFormula, parseRange, shiftFormulaReferences, tokenizeFormula };
@@ -1,29 +1,39 @@
1
1
  import {
2
2
  DataSourceBinder,
3
3
  DependencyGraph,
4
+ FORMULA_ERRORS,
4
5
  FormulaEvaluator,
5
6
  FormulaParser,
6
7
  SpreadsheetController,
8
+ WHOLE_COLUMN_END_ROW,
7
9
  colNameToIndex,
10
+ columnDependencyKey,
8
11
  extractCellDependencies,
9
12
  indexToColName,
13
+ isFormulaError,
10
14
  parseCellReference,
11
15
  parseFormula,
12
16
  parseRange,
17
+ shiftFormulaReferences,
13
18
  tokenizeFormula
14
- } from "../chunk-NNJRJJSC.js";
19
+ } from "../chunk-AIZUKWTL.js";
15
20
  export {
16
21
  DataSourceBinder,
17
22
  DependencyGraph,
23
+ FORMULA_ERRORS,
18
24
  FormulaEvaluator,
19
25
  FormulaParser,
20
26
  SpreadsheetController,
27
+ WHOLE_COLUMN_END_ROW,
21
28
  colNameToIndex,
29
+ columnDependencyKey,
22
30
  extractCellDependencies,
23
31
  indexToColName,
32
+ isFormulaError,
24
33
  parseCellReference,
25
34
  parseFormula,
26
35
  parseRange,
36
+ shiftFormulaReferences,
27
37
  tokenizeFormula
28
38
  };
29
39
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/chunk-CYKQLDYH.cjs","../src/spreadsheet/formula/lexer.ts","../src/spreadsheet/formula/parser.ts","../src/spreadsheet/formula/evaluator.ts","../src/spreadsheet/formula/dependency-graph.ts","../src/spreadsheet/datasource/index.ts","../src/spreadsheet/controller.ts"],"names":[],"mappings":"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","file":"/home/runner/work/editor/editor/packages/core/dist/chunk-CYKQLDYH.cjs","sourcesContent":[null,"export type FormulaTokenType =\n | 'NUMBER'\n | 'STRING'\n | 'BOOLEAN'\n | 'CELL_REF'\n | 'RANGE'\n | 'PROPERTY_REF'\n | 'IDENTIFIER'\n | 'OPERATOR'\n | 'LPAREN'\n | 'RPAREN'\n | 'COMMA'\n | 'EOF';\n\nexport interface FormulaToken {\n type: FormulaTokenType;\n value: string;\n position: number;\n}\n\nexport function tokenizeFormula(formula: string): FormulaToken[] {\n const tokens: FormulaToken[] = [];\n // Strip leading '=' if present\n let input = formula.trim();\n let offset = 0;\n if (input.startsWith('=')) {\n input = input.slice(1);\n offset = 1;\n }\n\n let i = 0;\n while (i < input.length) {\n const char = input[i];\n\n // Skip whitespace\n if (/\\s/.test(char)) {\n i++;\n continue;\n }\n\n // Number literal (int or float)\n if (/\\d/.test(char) || (char === '.' && i + 1 < input.length && /\\d/.test(input[i + 1]))) {\n const start = i;\n let hasDot = char === '.';\n i++;\n while (i < input.length && (/\\d/.test(input[i]) || (input[i] === '.' && !hasDot))) {\n if (input[i] === '.') hasDot = true;\n i++;\n }\n tokens.push({\n type: 'NUMBER',\n value: input.slice(start, i),\n position: start + offset\n });\n continue;\n }\n\n // String literal (\"...\" or '...')\n if (char === '\"' || char === \"'\") {\n const quote = char;\n const start = i;\n i++;\n let str = '';\n while (i < input.length && input[i] !== quote) {\n if (input[i] === '\\\\' && i + 1 < input.length) {\n i++;\n }\n str += input[i];\n i++;\n }\n if (i < input.length && input[i] === quote) {\n i++; // skip closing quote\n }\n tokens.push({\n type: 'STRING',\n value: str,\n position: start + offset\n });\n continue;\n }\n\n // Property reference: [property_name] e.g. [status], [title], [effortHours]\n if (char === '[') {\n const start = i;\n i++;\n let prop = '';\n while (i < input.length && input[i] !== ']') {\n prop += input[i];\n i++;\n }\n if (i < input.length && input[i] === ']') {\n i++;\n }\n tokens.push({\n type: 'PROPERTY_REF',\n value: prop.trim(),\n position: start + offset\n });\n continue;\n }\n\n // Single character delimiters\n if (char === '(') {\n tokens.push({ type: 'LPAREN', value: '(', position: i + offset });\n i++;\n continue;\n }\n if (char === ')') {\n tokens.push({ type: 'RPAREN', value: ')', position: i + offset });\n i++;\n continue;\n }\n if (char === ',') {\n tokens.push({ type: 'COMMA', value: ',', position: i + offset });\n i++;\n continue;\n }\n\n // Multi-character comparison operators: <=, >=, <>, !=\n const twoChars = input.slice(i, i + 2);\n if (['<=', '>=', '<>', '!='].includes(twoChars)) {\n tokens.push({\n type: 'OPERATOR',\n value: twoChars === '!=' ? '<>' : twoChars,\n position: i + offset\n });\n i += 2;\n continue;\n }\n\n // Single character operators: +, -, *, /, ^, &, =, <, >\n if (['+', '-', '*', '/', '^', '&', '=', '<', '>'].includes(char)) {\n tokens.push({ type: 'OPERATOR', value: char, position: i + offset });\n i++;\n continue;\n }\n\n // Identifiers, Cell references, and Ranges (e.g. SUM, A1, B2:B10, A:A)\n if (/[a-zA-Z_$]/.test(char)) {\n const start = i;\n while (i < input.length && /[a-zA-Z0-9_$.]/.test(input[i])) {\n i++;\n }\n const rawWord = input.slice(start, i);\n\n // Check if followed by colon indicating a range e.g. A1:B10 or A:A\n if (i < input.length && input[i] === ':') {\n const colonPos = i;\n i++; // skip colon\n const secondStart = i;\n while (i < input.length && /[a-zA-Z0-9_$.]/.test(input[i])) {\n i++;\n }\n const secondWord = input.slice(secondStart, i);\n if (secondWord.length > 0) {\n tokens.push({\n type: 'RANGE',\n value: `${rawWord}:${secondWord}`.toUpperCase(),\n position: start + offset\n });\n continue;\n } else {\n // Revert back\n i = colonPos;\n }\n }\n\n // Check for boolean literal\n const upper = rawWord.toUpperCase();\n if (upper === 'TRUE' || upper === 'FALSE') {\n tokens.push({\n type: 'BOOLEAN',\n value: upper,\n position: start + offset\n });\n continue;\n }\n\n // Check for Cell Reference (e.g. A1, $B$12, AA100)\n if (/^\\$?[A-Za-z]+\\$?[0-9]+$/.test(rawWord)) {\n tokens.push({\n type: 'CELL_REF',\n value: upper.replace(/\\$/g, ''),\n position: start + offset\n });\n continue;\n }\n\n // Function or generic identifier\n tokens.push({\n type: 'IDENTIFIER',\n value: upper,\n position: start + offset\n });\n continue;\n }\n\n // Skip unknown character\n i++;\n }\n\n tokens.push({ type: 'EOF', value: '', position: input.length + offset });\n return tokens;\n}\n","import { FormulaToken, tokenizeFormula, FormulaTokenType } from './lexer';\n\nexport type ASTNode =\n | LiteralNode\n | CellRefNode\n | RangeNode\n | PropertyRefNode\n | BinaryOpNode\n | UnaryOpNode\n | FunctionCallNode;\n\nexport interface LiteralNode {\n type: 'Literal';\n value: number | string | boolean;\n}\n\nexport interface CellRefNode {\n type: 'CellRef';\n ref: string;\n col: string;\n row: number;\n}\n\nexport interface RangeNode {\n type: 'Range';\n raw: string;\n startCol: string;\n startRow: number;\n endCol: string;\n endRow: number;\n}\n\nexport interface PropertyRefNode {\n type: 'PropertyRef';\n property: string;\n}\n\nexport interface BinaryOpNode {\n type: 'BinaryOp';\n op: string;\n left: ASTNode;\n right: ASTNode;\n}\n\nexport interface UnaryOpNode {\n type: 'UnaryOp';\n op: string;\n argument: ASTNode;\n}\n\nexport interface FunctionCallNode {\n type: 'FunctionCall';\n name: string;\n args: ASTNode[];\n}\n\nexport function parseCellReference(ref: string): { col: string; row: number } {\n const match = ref.match(/^([A-Za-z]+)(\\d+)$/);\n if (!match) {\n throw new Error(`Invalid cell reference: ${ref}`);\n }\n return {\n col: match[1].toUpperCase(),\n row: parseInt(match[2], 10)\n };\n}\n\nexport function parseRange(rangeStr: string): RangeNode {\n const [start, end] = rangeStr.split(':');\n const startRef = parseCellReference(start);\n const endRef = parseCellReference(end);\n return {\n type: 'Range',\n raw: rangeStr.toUpperCase(),\n startCol: startRef.col,\n startRow: startRef.row,\n endCol: endRef.col,\n endRow: endRef.row\n };\n}\n\nexport class FormulaParser {\n private tokens: FormulaToken[] = [];\n private current = 0;\n\n constructor(tokens: FormulaToken[]) {\n this.tokens = tokens;\n }\n\n public parse(): ASTNode {\n if (this.peek().type === 'EOF') {\n return { type: 'Literal', value: '' };\n }\n const expr = this.expression();\n if (this.peek().type !== 'EOF') {\n throw new Error(`Unexpected token at position ${this.peek().position}: ${this.peek().value}`);\n }\n return expr;\n }\n\n private expression(): ASTNode {\n return this.comparison();\n }\n\n private comparison(): ASTNode {\n let expr = this.concatenation();\n\n while (\n this.matchOperator('=', '<>', '<', '>', '<=', '>=')\n ) {\n const op = this.previous().value;\n const right = this.concatenation();\n expr = { type: 'BinaryOp', op, left: expr, right };\n }\n\n return expr;\n }\n\n private concatenation(): ASTNode {\n let expr = this.addition();\n\n while (this.matchOperator('&')) {\n const op = this.previous().value;\n const right = this.addition();\n expr = { type: 'BinaryOp', op, left: expr, right };\n }\n\n return expr;\n }\n\n private addition(): ASTNode {\n let expr = this.multiplication();\n\n while (this.matchOperator('+', '-')) {\n const op = this.previous().value;\n const right = this.multiplication();\n expr = { type: 'BinaryOp', op, left: expr, right };\n }\n\n return expr;\n }\n\n private multiplication(): ASTNode {\n let expr = this.exponentiation();\n\n while (this.matchOperator('*', '/')) {\n const op = this.previous().value;\n const right = this.exponentiation();\n expr = { type: 'BinaryOp', op, left: expr, right };\n }\n\n return expr;\n }\n\n private exponentiation(): ASTNode {\n let expr = this.unary();\n\n while (this.matchOperator('^')) {\n const op = this.previous().value;\n const right = this.unary();\n expr = { type: 'BinaryOp', op, left: expr, right };\n }\n\n return expr;\n }\n\n private unary(): ASTNode {\n if (this.matchOperator('-', '+')) {\n const op = this.previous().value;\n const argument = this.unary();\n return { type: 'UnaryOp', op, argument };\n }\n return this.primary();\n }\n\n private primary(): ASTNode {\n const token = this.peek();\n\n if (token.type === 'NUMBER') {\n this.advance();\n return { type: 'Literal', value: parseFloat(token.value) };\n }\n\n if (token.type === 'STRING') {\n this.advance();\n return { type: 'Literal', value: token.value };\n }\n\n if (token.type === 'BOOLEAN') {\n this.advance();\n return { type: 'Literal', value: token.value === 'TRUE' };\n }\n\n if (token.type === 'CELL_REF') {\n this.advance();\n const parsed = parseCellReference(token.value);\n return {\n type: 'CellRef',\n ref: token.value,\n col: parsed.col,\n row: parsed.row\n };\n }\n\n if (token.type === 'RANGE') {\n this.advance();\n return parseRange(token.value);\n }\n\n if (token.type === 'PROPERTY_REF') {\n this.advance();\n return {\n type: 'PropertyRef',\n property: token.value\n };\n }\n\n // Function call e.g. SUM(...)\n if (token.type === 'IDENTIFIER') {\n const name = token.value;\n this.advance();\n\n if (this.check('LPAREN')) {\n this.consume('LPAREN', `Expected '(' after function ${name}`);\n const args: ASTNode[] = [];\n if (!this.check('RPAREN')) {\n do {\n args.push(this.expression());\n } while (this.match('COMMA'));\n }\n this.consume('RPAREN', `Expected ')' after function arguments`);\n return {\n type: 'FunctionCall',\n name,\n args\n };\n }\n\n // Standalone identifier treated as string literal or reference\n return { type: 'Literal', value: name };\n }\n\n if (this.match('LPAREN')) {\n const expr = this.expression();\n this.consume('RPAREN', `Expected ')' after expression`);\n return expr;\n }\n\n throw new Error(`Unexpected token at position ${token.position}: ${token.value || token.type}`);\n }\n\n private match(...types: string[]): boolean {\n for (const type of types) {\n if (this.check(type)) {\n this.advance();\n return true;\n }\n }\n return false;\n }\n\n private matchOperator(...ops: string[]): boolean {\n if (this.check('OPERATOR') && ops.includes(this.peek().value)) {\n this.advance();\n return true;\n }\n return false;\n }\n\n private check(type: string): boolean {\n if (this.isAtEnd()) return false;\n return this.peek().type === type;\n }\n\n private advance(): FormulaToken {\n if (!this.isAtEnd()) this.current++;\n return this.previous();\n }\n\n private isAtEnd(): boolean {\n return this.peek().type === 'EOF';\n }\n\n private peek(): FormulaToken {\n return this.tokens[this.current];\n }\n\n private previous(): FormulaToken {\n return this.tokens[this.current - 1];\n }\n\n private consume(type: string, message: string): FormulaToken {\n if (this.check(type)) return this.advance();\n throw new Error(message);\n }\n}\n\nexport function parseFormula(formula: string): ASTNode {\n const tokens = tokenizeFormula(formula);\n const parser = new FormulaParser(tokens);\n return parser.parse();\n}\n","import { ASTNode, parseFormula } from './parser';\n\nexport interface EvaluationContext {\n resolveCell?: (col: string, row: number) => any;\n resolveRange?: (startCol: string, startRow: number, endCol: string, endRow: number) => any[];\n resolveProperty?: (property: string) => any;\n}\n\nexport function colNameToIndex(col: string): number {\n let index = 0;\n const upper = col.toUpperCase();\n for (let i = 0; i < upper.length; i++) {\n index = index * 26 + (upper.charCodeAt(i) - 65 + 1);\n }\n return index - 1;\n}\n\nexport function indexToColName(index: number): string {\n let num = index + 1;\n let name = '';\n while (num > 0) {\n const mod = (num - 1) % 26;\n name = String.fromCharCode(65 + mod) + name;\n num = Math.floor((num - mod) / 26);\n }\n return name;\n}\n\nexport class FormulaEvaluator {\n private context: EvaluationContext;\n\n constructor(context: EvaluationContext = {}) {\n this.context = context;\n }\n\n public evaluate(astOrFormula: ASTNode | string): any {\n const ast = typeof astOrFormula === 'string' ? parseFormula(astOrFormula) : astOrFormula;\n return this.evaluateNode(ast);\n }\n\n private evaluateNode(node: ASTNode): any {\n switch (node.type) {\n case 'Literal':\n return node.value;\n\n case 'CellRef': {\n if (!this.context.resolveCell) {\n throw new Error(`Cannot resolve cell reference ${node.ref}: no cell resolver provided`);\n }\n return this.context.resolveCell(node.col, node.row);\n }\n\n case 'Range': {\n if (!this.context.resolveRange) {\n throw new Error(`Cannot resolve range ${node.raw}: no range resolver provided`);\n }\n return this.context.resolveRange(node.startCol, node.startRow, node.endCol, node.endRow);\n }\n\n case 'PropertyRef': {\n if (!this.context.resolveProperty) {\n throw new Error(`Cannot resolve property [${node.property}]: no property resolver provided`);\n }\n return this.context.resolveProperty(node.property);\n }\n\n case 'UnaryOp': {\n const val = this.evaluateNode(node.argument);\n if (node.op === '-') return -Number(val);\n if (node.op === '+') return +Number(val);\n throw new Error(`Unsupported unary operator: ${node.op}`);\n }\n\n case 'BinaryOp': {\n const left = this.evaluateNode(node.left);\n const right = this.evaluateNode(node.right);\n\n switch (node.op) {\n case '+':\n return Number(left) + Number(right);\n case '-':\n return Number(left) - Number(right);\n case '*':\n return Number(left) * Number(right);\n case '/': {\n const denom = Number(right);\n if (denom === 0) return '#DIV/0!';\n return Number(left) / denom;\n }\n case '^':\n return Math.pow(Number(left), Number(right));\n case '&':\n return `${left ?? ''}${right ?? ''}`;\n case '=':\n // Relaxed equality\n return String(left).toLowerCase() === String(right).toLowerCase();\n case '<>':\n return String(left).toLowerCase() !== String(right).toLowerCase();\n case '<':\n return Number(left) < Number(right);\n case '<=':\n return Number(left) <= Number(right);\n case '>':\n return Number(left) > Number(right);\n case '>=':\n return Number(left) >= Number(right);\n default:\n throw new Error(`Unsupported binary operator: ${node.op}`);\n }\n }\n\n case 'FunctionCall': {\n return this.callFunction(node.name, node.args);\n }\n\n default:\n throw new Error(`Unknown AST node: ${(node as any).type}`);\n }\n }\n\n private callFunction(name: string, args: ASTNode[]): any {\n const fnName = name.toUpperCase();\n\n // Flatten helper for range/array args\n const evaluateAndFlatten = (): any[] => {\n const items: any[] = [];\n for (const arg of args) {\n const val = this.evaluateNode(arg);\n if (Array.isArray(val)) {\n items.push(...val);\n } else {\n items.push(val);\n }\n }\n return items;\n };\n\n switch (fnName) {\n case 'SUM': {\n const items = evaluateAndFlatten();\n let total = 0;\n for (const item of items) {\n const num = Number(item);\n if (!isNaN(num)) total += num;\n }\n return total;\n }\n\n case 'AVERAGE': {\n const items = evaluateAndFlatten();\n let total = 0;\n let count = 0;\n for (const item of items) {\n const num = Number(item);\n if (!isNaN(num) && item !== '' && item !== null && item !== undefined) {\n total += num;\n count++;\n }\n }\n return count === 0 ? 0 : total / count;\n }\n\n case 'COUNT': {\n const items = evaluateAndFlatten();\n let count = 0;\n for (const item of items) {\n const num = Number(item);\n if (!isNaN(num) && item !== '' && item !== null && item !== undefined) {\n count++;\n }\n }\n return count;\n }\n\n case 'COUNTA': {\n const items = evaluateAndFlatten();\n let count = 0;\n for (const item of items) {\n if (item !== '' && item !== null && item !== undefined) {\n count++;\n }\n }\n return count;\n }\n\n case 'MIN': {\n const items = evaluateAndFlatten().map(Number).filter((n) => !isNaN(n));\n return items.length === 0 ? 0 : Math.min(...items);\n }\n\n case 'MAX': {\n const items = evaluateAndFlatten().map(Number).filter((n) => !isNaN(n));\n return items.length === 0 ? 0 : Math.max(...items);\n }\n\n case 'IF': {\n if (args.length < 2) throw new Error('IF requires at least 2 arguments');\n const condition = Boolean(this.evaluateNode(args[0]));\n if (condition) {\n return this.evaluateNode(args[1]);\n } else {\n return args.length > 2 ? this.evaluateNode(args[2]) : false;\n }\n }\n\n case 'CONCAT': {\n const items = evaluateAndFlatten();\n return items.map((i) => (i === null || i === undefined ? '' : String(i))).join('');\n }\n\n case 'UPPER': {\n if (args.length !== 1) throw new Error('UPPER requires 1 argument');\n const val = this.evaluateNode(args[0]);\n return String(val ?? '').toUpperCase();\n }\n\n case 'LOWER': {\n if (args.length !== 1) throw new Error('LOWER requires 1 argument');\n const val = this.evaluateNode(args[0]);\n return String(val ?? '').toLowerCase();\n }\n\n case 'TRIM': {\n if (args.length !== 1) throw new Error('TRIM requires 1 argument');\n const val = this.evaluateNode(args[0]);\n return String(val ?? '').trim();\n }\n\n case 'LEN': {\n if (args.length !== 1) throw new Error('LEN requires 1 argument');\n const val = this.evaluateNode(args[0]);\n return String(val ?? '').length;\n }\n\n case 'ROUND': {\n if (args.length < 1) throw new Error('ROUND requires at least 1 argument');\n const num = Number(this.evaluateNode(args[0]));\n const decimals = args.length > 1 ? Number(this.evaluateNode(args[1])) : 0;\n const factor = Math.pow(10, decimals);\n return Math.round(num * factor) / factor;\n }\n\n case 'ABS': {\n if (args.length !== 1) throw new Error('ABS requires 1 argument');\n return Math.abs(Number(this.evaluateNode(args[0])));\n }\n\n case 'TODAY': {\n return new Date().toISOString().split('T')[0];\n }\n\n case 'DAYS': {\n if (args.length !== 2) throw new Error('DAYS requires 2 arguments (end_date, start_date)');\n const d1 = new Date(this.evaluateNode(args[0]));\n const d2 = new Date(this.evaluateNode(args[1]));\n const diffTime = d1.getTime() - d2.getTime();\n return Math.round(diffTime / (1000 * 60 * 60 * 24));\n }\n\n default:\n throw new Error(`Unknown function: ${fnName}`);\n }\n }\n}\n","import { ASTNode, parseFormula } from './parser';\nimport { colNameToIndex, indexToColName } from './evaluator';\n\nexport interface CellDep {\n col: string;\n row: number;\n}\n\nexport function extractCellDependencies(node: ASTNode): CellDep[] {\n const deps: CellDep[] = [];\n\n const visit = (curr: ASTNode) => {\n if (curr.type === 'CellRef') {\n deps.push({ col: curr.col, row: curr.row });\n } else if (curr.type === 'Range') {\n const startC = colNameToIndex(curr.startCol);\n const endC = colNameToIndex(curr.endCol);\n const minC = Math.min(startC, endC);\n const maxC = Math.max(startC, endC);\n const minR = Math.min(curr.startRow, curr.endRow);\n const maxR = Math.max(curr.startRow, curr.endRow);\n\n for (let c = minC; c <= maxC; c++) {\n for (let r = minR; r <= maxR; r++) {\n deps.push({ col: indexToColName(c), row: r });\n }\n }\n } else if (curr.type === 'BinaryOp') {\n visit(curr.left);\n visit(curr.right);\n } else if (curr.type === 'UnaryOp') {\n visit(curr.argument);\n } else if (curr.type === 'FunctionCall') {\n curr.args.forEach(visit);\n }\n };\n\n visit(node);\n return deps;\n}\n\nexport class DependencyGraph {\n // Map of cellKey (e.g. \"B2\") -> Set of cellKeys that this cell depends ON\n private dependencies = new Map<string, Set<string>>();\n // Map of cellKey (e.g. \"A1\") -> Set of cellKeys that depend ON this cell (dependents)\n private dependents = new Map<string, Set<string>>();\n\n public setCellFormula(cellKey: string, formulaStr: string | null): void {\n const key = cellKey.toUpperCase();\n\n // Clear existing dependencies\n const oldDeps = this.dependencies.get(key) || new Set();\n for (const dep of oldDeps) {\n this.dependents.get(dep)?.delete(key);\n }\n this.dependencies.delete(key);\n\n if (!formulaStr || !formulaStr.startsWith('=')) {\n return;\n }\n\n try {\n const ast = parseFormula(formulaStr);\n const deps = extractCellDependencies(ast);\n const newDeps = new Set<string>();\n\n for (const d of deps) {\n const depKey = `${d.col}${d.row}`.toUpperCase();\n newDeps.add(depKey);\n\n if (!this.dependents.has(depKey)) {\n this.dependents.set(depKey, new Set());\n }\n this.dependents.get(depKey)!.add(key);\n }\n\n this.dependencies.set(key, newDeps);\n } catch {\n // Syntax errors in formula have no valid dependencies\n }\n }\n\n public getDependencies(cellKey: string): string[] {\n return Array.from(this.dependencies.get(cellKey.toUpperCase()) || []);\n }\n\n public getDependents(cellKey: string): string[] {\n return Array.from(this.dependents.get(cellKey.toUpperCase()) || []);\n }\n\n /**\n * Detects if there is a cycle containing cellKey.\n */\n public hasCycle(cellKey: string): boolean {\n const key = cellKey.toUpperCase();\n const visited = new Set<string>();\n const recStack = new Set<string>();\n\n const dfs = (node: string): boolean => {\n visited.add(node);\n recStack.add(node);\n\n const deps = this.dependencies.get(node) || new Set();\n for (const neighbor of deps) {\n if (!visited.has(neighbor)) {\n if (dfs(neighbor)) return true;\n } else if (recStack.has(neighbor)) {\n return true;\n }\n }\n\n recStack.delete(node);\n return false;\n };\n\n return dfs(key);\n }\n\n /**\n * Returns cells affected by updating cellKey in topological order.\n */\n public getEvaluationOrder(startCellKey: string): { order: string[]; hasCycle: boolean } {\n const start = startCellKey.toUpperCase();\n const visited = new Set<string>();\n const recStack = new Set<string>();\n const order: string[] = [];\n let cycleDetected = false;\n\n // Collect all downstream affected cells (reachability via dependents)\n const affected = new Set<string>();\n const collectAffected = (node: string) => {\n affected.add(node);\n const directDependents = this.dependents.get(node) || new Set();\n for (const dep of directDependents) {\n if (!affected.has(dep)) {\n collectAffected(dep);\n }\n }\n };\n collectAffected(start);\n\n // Topological sort among affected cells\n const visit = (node: string) => {\n if (recStack.has(node)) {\n cycleDetected = true;\n return;\n }\n if (visited.has(node)) return;\n\n recStack.add(node);\n visited.add(node);\n\n // Visit nodes that this node depends on, IF they are in the affected set\n const deps = this.dependencies.get(node) || new Set();\n for (const neighbor of deps) {\n if (affected.has(neighbor)) {\n visit(neighbor);\n }\n }\n\n recStack.delete(node);\n order.push(node);\n };\n\n for (const node of affected) {\n if (!visited.has(node)) {\n visit(node);\n }\n }\n\n return { order, hasCycle: cycleDetected };\n }\n}\n","import { SpreadsheetDataSource, SpreadsheetRow, SpreadsheetColumn } from '../types';\n\nexport class DataSourceBinder {\n private dataSources = new Map<string, SpreadsheetDataSource>();\n\n public register(dataSource: SpreadsheetDataSource): void {\n this.dataSources.set(dataSource.id, dataSource);\n }\n\n public unregister(dataSourceId: string): void {\n this.dataSources.delete(dataSourceId);\n }\n\n public getDataSource(id: string): SpreadsheetDataSource | undefined {\n return this.dataSources.get(id);\n }\n\n /**\n * Generates or reconciles spreadsheet rows from a primary data source.\n */\n public syncRows(\n currentRows: SpreadsheetRow[],\n dataSourceId: string\n ): SpreadsheetRow[] {\n const ds = this.dataSources.get(dataSourceId);\n if (!ds) return currentRows;\n\n const records = ds.getRecords();\n const existingRecordRowMap = new Map<string, SpreadsheetRow>();\n const freeformRows: SpreadsheetRow[] = [];\n\n for (const row of currentRows) {\n if (row.type === 'record' && row.recordId) {\n existingRecordRowMap.set(row.recordId, row);\n } else {\n freeformRows.push(row);\n }\n }\n\n const nextRows: SpreadsheetRow[] = [];\n let rowIndex = 0;\n\n for (const record of records) {\n const existing = existingRecordRowMap.get(record.id);\n nextRows.push({\n id: existing?.id || `row_${record.id}`,\n index: rowIndex++,\n height: existing?.height || 32,\n recordId: record.id,\n type: 'record'\n });\n existingRecordRowMap.delete(record.id);\n }\n\n // Append preserved freeform rows\n for (const freeform of freeformRows) {\n nextRows.push({\n ...freeform,\n index: rowIndex++\n });\n }\n\n return nextRows;\n }\n\n /**\n * Resolves a record's bound field value.\n */\n public resolveBoundValue(\n dataSourceId: string,\n recordId: string,\n field: string\n ): any {\n const ds = this.dataSources.get(dataSourceId);\n if (!ds) return undefined;\n\n const records = ds.getRecords();\n const record = records.find((r) => r.id === recordId);\n return record ? record[field] : undefined;\n }\n\n /**\n * Dispatches an update to the underlying data source.\n */\n public async commitBoundValue(\n dataSourceId: string,\n recordId: string,\n field: string,\n value: any\n ): Promise<void> {\n const ds = this.dataSources.get(dataSourceId);\n if (!ds) return;\n await ds.updateRecord(recordId, field, value);\n }\n}\n","import {\n SpreadsheetDocument,\n SpreadsheetColumn,\n SpreadsheetRow,\n CellCoordinate,\n CellRange,\n CellData,\n SpreadsheetDataSource\n} from './types';\nimport {\n FormulaEvaluator,\n colNameToIndex,\n indexToColName,\n DependencyGraph\n} from './formula';\nimport { DataSourceBinder } from './datasource';\n\nexport interface SpreadsheetControllerOptions {\n document?: Partial<SpreadsheetDocument>;\n dataSources?: SpreadsheetDataSource[];\n primaryDataSourceId?: string;\n onDocumentChange?: (doc: SpreadsheetDocument) => void;\n onSelectionChange?: (activeCell: CellCoordinate | null, range: CellRange | null) => void;\n onCellCommit?: (rowId: string, colId: string, raw: string, value: any) => void;\n}\n\nexport class SpreadsheetController {\n public document: SpreadsheetDocument;\n public activeCell: CellCoordinate | null = null;\n public selectedRange: CellRange | null = null;\n public editingCell: CellCoordinate | null = null;\n public draftValue = '';\n\n public readonly binder = new DataSourceBinder();\n public readonly depGraph = new DependencyGraph();\n public readonly evaluator: FormulaEvaluator;\n\n private primaryDataSourceId?: string;\n private onDocumentChange?: (doc: SpreadsheetDocument) => void;\n private onSelectionChange?: (activeCell: CellCoordinate | null, range: CellRange | null) => void;\n private onCellCommit?: (rowId: string, colId: string, raw: string, value: any) => void;\n\n // History stack for undo/redo\n private undoStack: string[] = [];\n private redoStack: string[] = [];\n private isApplyingHistory = false;\n\n constructor(options: SpreadsheetControllerOptions = {}) {\n const defaultCols: SpreadsheetColumn[] = [\n { id: 'col_a', key: 'A', title: 'A', width: 120, type: 'freeform' },\n { id: 'col_b', key: 'B', title: 'B', width: 120, type: 'freeform' },\n { id: 'col_c', key: 'C', title: 'C', width: 120, type: 'freeform' },\n { id: 'col_d', key: 'D', title: 'D', width: 120, type: 'freeform' }\n ];\n\n const defaultRows: SpreadsheetRow[] = options.primaryDataSourceId ? [] : Array.from({ length: 10 }, (_, i) => ({\n id: `row_${i + 1}`,\n index: i,\n height: 32,\n type: 'freeform'\n }));\n\n this.document = {\n id: options.document?.id || `sheet_${Date.now()}`,\n name: options.document?.name || 'Sheet 1',\n columns: options.document?.columns || defaultCols,\n rows: options.document?.rows || defaultRows,\n cells: options.document?.cells || {},\n createdAt: options.document?.createdAt || Date.now(),\n updatedAt: options.document?.updatedAt || Date.now()\n };\n\n this.primaryDataSourceId = options.primaryDataSourceId;\n this.onDocumentChange = options.onDocumentChange;\n this.onSelectionChange = options.onSelectionChange;\n this.onCellCommit = options.onCellCommit;\n\n if (options.dataSources) {\n options.dataSources.forEach((ds) => this.binder.register(ds));\n }\n\n this.evaluator = new FormulaEvaluator({\n resolveCell: (colStr, rowNum) => this.resolveCellByCoord(colStr, rowNum),\n resolveRange: (sCol, sRow, eCol, eRow) => this.resolveRangeByCoords(sCol, sRow, eCol, eRow),\n resolveProperty: (prop) => this.resolveCurrentRowProperty(prop)\n });\n\n this.syncWithDataSource();\n this.recalculateAll();\n }\n\n public loadDocument(document: SpreadsheetDocument): void {\n this.document = JSON.parse(JSON.stringify(document));\n this.activeCell = null;\n this.selectedRange = null;\n this.editingCell = null;\n this.draftValue = '';\n this.undoStack = [];\n this.redoStack = [];\n this.syncWithDataSource();\n this.recalculateAll();\n this.notifyChange();\n }\n\n public registerDataSource(ds: SpreadsheetDataSource): void {\n this.binder.register(ds);\n this.syncWithDataSource();\n this.recalculateAll();\n }\n\n public setPrimaryDataSource(dataSourceId: string | undefined): void {\n this.primaryDataSourceId = dataSourceId;\n this.syncWithDataSource();\n this.recalculateAll();\n }\n\n public syncWithDataSource(): void {\n if (!this.primaryDataSourceId) return;\n this.document.rows = this.binder.syncRows(this.document.rows, this.primaryDataSourceId);\n this.recomputeRowIndices();\n }\n\n private recomputeRowIndices(): void {\n this.document.rows.forEach((r, idx) => {\n r.index = idx;\n });\n this.document.columns.forEach((c, idx) => {\n c.key = indexToColName(idx);\n });\n }\n\n public getColumn(colIdOrKey: string): SpreadsheetColumn | undefined {\n return this.document.columns.find(\n (c) => c.id === colIdOrKey || c.key.toUpperCase() === colIdOrKey.toUpperCase()\n );\n }\n\n public getRow(rowIdOrIndex: string | number): SpreadsheetRow | undefined {\n if (typeof rowIdOrIndex === 'number') {\n return this.document.rows[rowIdOrIndex];\n }\n return this.document.rows.find((r) => r.id === rowIdOrIndex);\n }\n\n public getCell(rowId: string, colId: string): CellData {\n const existing = this.document.cells[rowId]?.[colId];\n if (existing) return existing;\n\n // Check if column is bound to record\n const col = this.document.columns.find((c) => c.id === colId);\n const row = this.document.rows.find((r) => r.id === rowId);\n\n if (col && row && col.type === 'bound' && col.binding && row.recordId) {\n const boundVal = this.binder.resolveBoundValue(\n col.binding.dataSource,\n row.recordId,\n col.binding.field\n );\n return {\n raw: boundVal !== undefined ? String(boundVal) : '',\n value: boundVal ?? '',\n format: col.format\n };\n }\n\n // Check if column has a column-level formula\n if (col && col.type === 'formula' && col.formula) {\n return {\n raw: col.formula,\n value: '',\n format: col.format\n };\n }\n\n return { raw: '', value: '', format: col?.format };\n }\n\n public setCellValue(rowId: string, colId: string, raw: string): void {\n this.recordSnapshot();\n\n if (!this.document.cells[rowId]) {\n this.document.cells[rowId] = {};\n }\n\n const col = this.document.columns.find((c) => c.id === colId);\n const row = this.document.rows.find((r) => r.id === rowId);\n\n // If this is a bound column and row is bound to record, dispatch back to data source\n if (col?.type === 'bound' && col.binding && row?.recordId) {\n this.binder.commitBoundValue(\n col.binding.dataSource,\n row.recordId,\n col.binding.field,\n raw\n );\n }\n\n const cellKey = this.getCellKey(rowId, colId);\n this.depGraph.setCellFormula(cellKey, raw.startsWith('=') ? raw : null);\n\n this.document.cells[rowId][colId] = {\n raw,\n value: raw,\n format: col?.format\n };\n\n this.document.updatedAt = Date.now();\n this.recalculateCell(rowId, colId);\n this.notifyChange();\n\n const evaluatedVal = this.document.cells[rowId][colId]?.value;\n this.onCellCommit?.(rowId, colId, raw, evaluatedVal);\n }\n\n private getCellKey(rowId: string, colId: string): string {\n const row = this.document.rows.find((r) => r.id === rowId);\n const col = this.document.columns.find((c) => c.id === colId);\n if (!row || !col) return `${colId}_${rowId}`;\n return `${col.key}${row.index + 1}`.toUpperCase();\n }\n\n public recalculateCell(rowId: string, colId: string): void {\n const cellKey = this.getCellKey(rowId, colId);\n const { order, hasCycle } = this.depGraph.getEvaluationOrder(cellKey);\n\n if (hasCycle) {\n if (this.document.cells[rowId]?.[colId]) {\n this.document.cells[rowId][colId].error = '#CYCLE!';\n this.document.cells[rowId][colId].value = '#CYCLE!';\n }\n return;\n }\n\n for (const key of order) {\n this.evalSingleCellByKey(key);\n }\n }\n\n public recalculateAll(): void {\n for (const row of this.document.rows) {\n for (const col of this.document.columns) {\n const cell = this.getCell(row.id, col.id);\n const cellKey = `${col.key}${row.index + 1}`.toUpperCase();\n if (cell.raw.startsWith('=')) {\n this.depGraph.setCellFormula(cellKey, cell.raw);\n }\n }\n }\n\n for (const row of this.document.rows) {\n for (const col of this.document.columns) {\n const cellKey = `${col.key}${row.index + 1}`.toUpperCase();\n this.evalSingleCellByKey(cellKey);\n }\n }\n }\n\n private evalSingleCellByKey(cellKey: string): void {\n const match = cellKey.match(/^([A-Za-z]+)(\\d+)$/);\n if (!match) return;\n const colName = match[1].toUpperCase();\n const rowNum = parseInt(match[2], 10);\n\n const col = this.document.columns.find((c) => c.key.toUpperCase() === colName);\n const row = this.document.rows[rowNum - 1];\n if (!col || !row) return;\n\n const cell = this.getCell(row.id, col.id);\n if (!cell.raw.startsWith('=')) {\n if (cell.error === '#CYCLE!') {\n cell.error = undefined;\n }\n return;\n }\n\n try {\n const computed = this.evaluator.evaluate(cell.raw);\n if (!this.document.cells[row.id]) {\n this.document.cells[row.id] = {};\n }\n this.document.cells[row.id][col.id] = {\n ...cell,\n value: computed,\n error: undefined\n };\n } catch (err: any) {\n if (!this.document.cells[row.id]) {\n this.document.cells[row.id] = {};\n }\n this.document.cells[row.id][col.id] = {\n ...cell,\n value: '#ERROR!',\n error: err?.message || 'Formula error'\n };\n }\n }\n\n private resolveCellByCoord(colStr: string, rowNum: number): any {\n const col = this.document.columns.find((c) => c.key.toUpperCase() === colStr.toUpperCase());\n const row = this.document.rows[rowNum - 1];\n if (!col || !row) return 0;\n\n const cell = this.getCell(row.id, col.id);\n return cell.value !== undefined && cell.value !== '' ? cell.value : 0;\n }\n\n private resolveRangeByCoords(sCol: string, sRow: number, eCol: string, eRow: number): any[] {\n const startC = colNameToIndex(sCol);\n const endC = colNameToIndex(eCol);\n const minC = Math.min(startC, endC);\n const maxC = Math.max(startC, endC);\n const minR = Math.min(sRow, eRow);\n const maxR = Math.max(sRow, eRow);\n\n const results: any[] = [];\n for (let c = minC; c <= maxC; c++) {\n const colName = indexToColName(c);\n const col = this.document.columns.find((colDef) => colDef.key.toUpperCase() === colName);\n if (!col) continue;\n\n for (let r = minR; r <= maxR; r++) {\n const row = this.document.rows[r - 1];\n if (!row) continue;\n const cell = this.getCell(row.id, col.id);\n results.push(cell.value !== undefined && cell.value !== '' ? cell.value : 0);\n }\n }\n return results;\n }\n\n private resolveCurrentRowProperty(prop: string): any {\n if (!this.activeCell) return 0;\n const row = this.document.rows[this.activeCell.row];\n if (!row || !row.recordId || !this.primaryDataSourceId) return 0;\n return this.binder.resolveBoundValue(this.primaryDataSourceId, row.recordId, prop) ?? 0;\n }\n\n // --- Selection and Navigation ---\n\n public selectCell(row: number, col: number, extendSelection = false): void {\n const clampedRow = Math.max(0, Math.min(row, this.document.rows.length - 1));\n const clampedCol = Math.max(0, Math.min(col, this.document.columns.length - 1));\n\n const rowObj = this.document.rows[clampedRow];\n const colObj = this.document.columns[clampedCol];\n\n this.activeCell = {\n row: clampedRow,\n col: clampedCol,\n rowId: rowObj?.id,\n colId: colObj?.id\n };\n\n if (extendSelection && this.selectedRange) {\n this.selectedRange = {\n startRow: this.selectedRange.startRow,\n startCol: this.selectedRange.startCol,\n endRow: clampedRow,\n endCol: clampedCol\n };\n } else {\n this.selectedRange = {\n startRow: clampedRow,\n startCol: clampedCol,\n endRow: clampedRow,\n endCol: clampedCol\n };\n }\n\n this.onSelectionChange?.(this.activeCell, this.selectedRange);\n }\n\n public selectRange(range: CellRange): void {\n this.selectedRange = range;\n this.onSelectionChange?.(this.activeCell, this.selectedRange);\n }\n\n public startEditing(row?: number, col?: number, initialValue?: string): void {\n const targetRow = row ?? this.activeCell?.row ?? 0;\n const targetCol = col ?? this.activeCell?.col ?? 0;\n this.selectCell(targetRow, targetCol);\n\n const rowObj = this.document.rows[targetRow];\n const colObj = this.document.columns[targetCol];\n if (!rowObj || !colObj) return;\n\n this.editingCell = {\n row: targetRow,\n col: targetCol,\n rowId: rowObj.id,\n colId: colObj.id\n };\n\n const cell = this.getCell(rowObj.id, colObj.id);\n this.draftValue = initialValue !== undefined ? initialValue : cell.raw;\n }\n\n public commitEditing(valueToCommit?: string): void {\n if (!this.editingCell) return;\n const val = valueToCommit !== undefined ? valueToCommit : this.draftValue;\n if (this.editingCell.rowId && this.editingCell.colId) {\n this.setCellValue(this.editingCell.rowId, this.editingCell.colId, val);\n }\n this.editingCell = null;\n this.draftValue = '';\n }\n\n public cancelEditing(): void {\n this.editingCell = null;\n this.draftValue = '';\n }\n\n public moveCursor(\n direction: 'up' | 'down' | 'left' | 'right',\n extendSelection = false\n ): void {\n if (!this.activeCell) {\n this.selectCell(0, 0);\n return;\n }\n\n let nextRow = this.activeCell.row;\n let nextCol = this.activeCell.col;\n\n switch (direction) {\n case 'up':\n nextRow = Math.max(0, nextRow - 1);\n break;\n case 'down':\n nextRow = Math.min(this.document.rows.length - 1, nextRow + 1);\n break;\n case 'left':\n nextCol = Math.max(0, nextCol - 1);\n break;\n case 'right':\n nextCol = Math.min(this.document.columns.length - 1, nextCol + 1);\n break;\n }\n\n this.selectCell(nextRow, nextCol, extendSelection);\n }\n\n // --- Grid Structure Mutations ---\n\n public insertColumn(index: number, config?: Partial<SpreadsheetColumn>): void {\n this.recordSnapshot();\n const newId = config?.id || `col_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const newCol: SpreadsheetColumn = {\n id: newId,\n key: indexToColName(index),\n title: config?.title || indexToColName(index),\n width: config?.width || 130,\n type: config?.type || 'freeform',\n binding: config?.binding,\n formula: config?.formula,\n format: config?.format || 'text',\n readOnly: config?.readOnly\n };\n\n this.document.columns.splice(index, 0, newCol);\n this.recomputeRowIndices();\n this.recalculateAll();\n this.notifyChange();\n }\n\n public deleteColumn(colId: string): void {\n this.recordSnapshot();\n this.document.columns = this.document.columns.filter((c) => c.id !== colId);\n for (const rowId of Object.keys(this.document.cells)) {\n delete this.document.cells[rowId][colId];\n }\n this.recomputeRowIndices();\n this.recalculateAll();\n this.notifyChange();\n }\n\n public insertRow(index: number, config?: Partial<SpreadsheetRow>): void {\n this.recordSnapshot();\n const newId = config?.id || `row_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;\n const newRow: SpreadsheetRow = {\n id: newId,\n index,\n height: config?.height || 32,\n recordId: config?.recordId,\n type: config?.type || 'freeform'\n };\n\n this.document.rows.splice(index, 0, newRow);\n this.recomputeRowIndices();\n this.recalculateAll();\n this.notifyChange();\n }\n\n public deleteRow(rowId: string): void {\n this.recordSnapshot();\n this.document.rows = this.document.rows.filter((r) => r.id !== rowId);\n delete this.document.cells[rowId];\n this.recomputeRowIndices();\n this.recalculateAll();\n this.notifyChange();\n }\n\n public setColumnWidth(colId: string, width: number): void {\n const col = this.document.columns.find((c) => c.id === colId);\n if (col) {\n col.width = Math.max(30, width);\n this.notifyChange();\n }\n }\n\n public autoFitColumnWidth(colId: string, customWidth?: number): void {\n if (customWidth !== undefined) {\n this.setColumnWidth(colId, customWidth);\n return;\n }\n const col = this.document.columns.find((c) => c.id === colId);\n if (!col) return;\n\n let maxLen = (col.title || '').length + (col.key ? col.key.length + 3 : 0);\n for (const row of this.document.rows) {\n const cell = this.getCell(row.id, col.id);\n const valStr = cell.value != null ? String(cell.value) : '';\n if (valStr.length > maxLen) {\n maxLen = valStr.length;\n }\n }\n const estimatedWidth = Math.min(600, Math.max(50, Math.ceil(maxLen * 8.5 + 32)));\n this.setColumnWidth(colId, estimatedWidth);\n }\n\n // --- Clipboard TSV/CSV ---\n\n public exportToTsv(): string {\n return this.document.rows\n .map((row) =>\n this.document.columns\n .map((col) => {\n const cell = this.getCell(row.id, col.id);\n return String(cell.value ?? '').replace(/\\t/g, ' ');\n })\n .join('\\t')\n )\n .join('\\n');\n }\n\n public importFromTsv(tsvData: string, startRow = 0, startCol = 0): void {\n this.recordSnapshot();\n const lines = tsvData.trim().split('\\n');\n\n lines.forEach((line, rIdx) => {\n const cells = line.split('\\t');\n const targetR = startRow + rIdx;\n\n while (targetR >= this.document.rows.length) {\n this.insertRow(this.document.rows.length);\n }\n const row = this.document.rows[targetR];\n\n cells.forEach((val, cIdx) => {\n const targetC = startCol + cIdx;\n while (targetC >= this.document.columns.length) {\n this.insertColumn(this.document.columns.length);\n }\n const col = this.document.columns[targetC];\n this.setCellValue(row.id, col.id, val);\n });\n });\n\n this.recalculateAll();\n this.notifyChange();\n }\n\n // --- Undo/Redo Snapshots ---\n\n private recordSnapshot(): void {\n if (this.isApplyingHistory) return;\n this.undoStack.push(JSON.stringify(this.document));\n if (this.undoStack.length > 50) this.undoStack.shift();\n this.redoStack = [];\n }\n\n public undo(): void {\n if (this.undoStack.length === 0) return;\n this.isApplyingHistory = true;\n this.redoStack.push(JSON.stringify(this.document));\n const previous = JSON.parse(this.undoStack.pop()!);\n this.document = previous;\n this.recalculateAll();\n this.notifyChange();\n this.isApplyingHistory = false;\n }\n\n public redo(): void {\n if (this.redoStack.length === 0) return;\n this.isApplyingHistory = true;\n this.undoStack.push(JSON.stringify(this.document));\n const next = JSON.parse(this.redoStack.pop()!);\n this.document = next;\n this.recalculateAll();\n this.notifyChange();\n this.isApplyingHistory = false;\n }\n\n private notifyChange(): void {\n this.onDocumentChange?.(this.document);\n }\n}\n"]}