@lotics/xlsx 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/package.json +30 -0
  2. package/src/auto_sum.test.ts +71 -0
  3. package/src/auto_sum.ts +83 -0
  4. package/src/canvas_cf.ts +135 -0
  5. package/src/canvas_chart.ts +687 -0
  6. package/src/canvas_fill.ts +156 -0
  7. package/src/canvas_images.ts +99 -0
  8. package/src/canvas_layout.test.ts +159 -0
  9. package/src/canvas_layout.ts +239 -0
  10. package/src/canvas_renderer.ts +1010 -0
  11. package/src/canvas_shape.ts +242 -0
  12. package/src/canvas_sparkline.ts +163 -0
  13. package/src/canvas_text.ts +454 -0
  14. package/src/cell_editor.ts +558 -0
  15. package/src/clipboard.test.ts +145 -0
  16. package/src/clipboard.ts +341 -0
  17. package/src/command_history.ts +102 -0
  18. package/src/csv_parser.test.ts +130 -0
  19. package/src/csv_parser.ts +172 -0
  20. package/src/data_validation.test.ts +95 -0
  21. package/src/data_validation.ts +114 -0
  22. package/src/editing_layer.test.ts +309 -0
  23. package/src/excel_cf_evaluate.test.ts +293 -0
  24. package/src/excel_cf_evaluate.ts +719 -0
  25. package/src/excel_cf_icons.ts +108 -0
  26. package/src/excel_cf_types.ts +67 -0
  27. package/src/excel_numfmt.test.ts +607 -0
  28. package/src/excel_numfmt.ts +1033 -0
  29. package/src/excel_parser.test.ts +1061 -0
  30. package/src/excel_parser.ts +393 -0
  31. package/src/excel_pattern_fills.ts +64 -0
  32. package/src/excel_table_styles.ts +160 -0
  33. package/src/excel_utils.test.ts +108 -0
  34. package/src/excel_utils.ts +162 -0
  35. package/src/fast-formula-parser.d.ts +53 -0
  36. package/src/fill_logic.ts +257 -0
  37. package/src/fill_patterns.test.ts +90 -0
  38. package/src/fill_patterns.ts +71 -0
  39. package/src/flash_fill.test.ts +75 -0
  40. package/src/flash_fill.ts +221 -0
  41. package/src/formula_deps.ts +189 -0
  42. package/src/formula_engine.test.ts +348 -0
  43. package/src/formula_engine.ts +401 -0
  44. package/src/formula_highlight.test.ts +81 -0
  45. package/src/formula_highlight.ts +139 -0
  46. package/src/formula_suggest.ts +100 -0
  47. package/src/hidden_sheets.test.ts +49 -0
  48. package/src/index.ts +149 -0
  49. package/src/keyboard_nav.test.ts +394 -0
  50. package/src/keyboard_nav.ts +891 -0
  51. package/src/move_logic.ts +63 -0
  52. package/src/numfmt_type.test.ts +158 -0
  53. package/src/numfmt_type.ts +231 -0
  54. package/src/ooxml_cell_format.ts +70 -0
  55. package/src/ooxml_chart.test.ts +85 -0
  56. package/src/ooxml_chart.ts +347 -0
  57. package/src/ooxml_chart_types.ts +207 -0
  58. package/src/ooxml_color.ts +339 -0
  59. package/src/ooxml_drawing.test.ts +87 -0
  60. package/src/ooxml_drawing.ts +287 -0
  61. package/src/ooxml_pivot.test.ts +195 -0
  62. package/src/ooxml_pivot.ts +468 -0
  63. package/src/ooxml_pivot_types.ts +165 -0
  64. package/src/ooxml_shape.ts +355 -0
  65. package/src/ooxml_sheet.ts +1271 -0
  66. package/src/ooxml_styles.ts +556 -0
  67. package/src/ooxml_table.test.ts +70 -0
  68. package/src/ooxml_table.ts +131 -0
  69. package/src/ooxml_workbook.test.ts +40 -0
  70. package/src/ooxml_workbook.ts +259 -0
  71. package/src/ooxml_zip.ts +33 -0
  72. package/src/pivot_model.ts +237 -0
  73. package/src/pivot_recompute.test.ts +210 -0
  74. package/src/pivot_recompute.ts +413 -0
  75. package/src/selection_model.ts +364 -0
  76. package/src/spreadsheet_commands.test.ts +772 -0
  77. package/src/spreadsheet_commands.ts +1805 -0
  78. package/src/structured_refs.test.ts +128 -0
  79. package/src/structured_refs.ts +160 -0
  80. package/src/style_helpers.test.ts +33 -0
  81. package/src/style_helpers.ts +30 -0
  82. package/src/template_builder.test.ts +139 -0
  83. package/src/template_builder.ts +36 -0
  84. package/src/types.ts +239 -0
  85. package/src/workbook_model.test.ts +536 -0
  86. package/src/workbook_model.ts +911 -0
  87. package/src/xlsx_round_trip.test.ts +279 -0
  88. package/src/xlsx_writer.test.ts +488 -0
  89. package/src/xlsx_writer.ts +1549 -0
  90. package/src/xml_entities.ts +23 -0
@@ -0,0 +1,348 @@
1
+ import { DependencyGraph } from "./formula_deps";
2
+ import { FormulaEngine, recomputeAll, recomputeIfStale } from "./formula_engine";
3
+ import { WorkbookModel, SheetModel, StyleRegistry } from "./workbook_model";
4
+
5
+ // =============================================================================
6
+ // DependencyGraph
7
+ // =============================================================================
8
+
9
+ describe("DependencyGraph", () => {
10
+ test("tracks direct dependents", () => {
11
+ const g = new DependencyGraph();
12
+ // B1 depends on A1
13
+ g.setDependencies("Sheet1!B1", ["Sheet1!A1"]);
14
+ expect(g.getDirectDependents("Sheet1!A1")).toEqual(["Sheet1!B1"]);
15
+ });
16
+
17
+ test("tracks multiple dependents", () => {
18
+ const g = new DependencyGraph();
19
+ g.setDependencies("Sheet1!B1", ["Sheet1!A1"]);
20
+ g.setDependencies("Sheet1!C1", ["Sheet1!A1"]);
21
+ const deps = g.getDirectDependents("Sheet1!A1");
22
+ expect(deps).toHaveLength(2);
23
+ expect(deps).toContain("Sheet1!B1");
24
+ expect(deps).toContain("Sheet1!C1");
25
+ });
26
+
27
+ test("removeCell cleans up edges", () => {
28
+ const g = new DependencyGraph();
29
+ g.setDependencies("Sheet1!B1", ["Sheet1!A1"]);
30
+ g.removeCell("Sheet1!B1");
31
+ expect(g.getDirectDependents("Sheet1!A1")).toEqual([]);
32
+ });
33
+
34
+ test("setDependencies replaces old edges", () => {
35
+ const g = new DependencyGraph();
36
+ g.setDependencies("Sheet1!B1", ["Sheet1!A1"]);
37
+ g.setDependencies("Sheet1!B1", ["Sheet1!C1"]);
38
+ expect(g.getDirectDependents("Sheet1!A1")).toEqual([]);
39
+ expect(g.getDirectDependents("Sheet1!C1")).toEqual(["Sheet1!B1"]);
40
+ });
41
+
42
+ test("getRecalcOrder returns topological order", () => {
43
+ const g = new DependencyGraph();
44
+ // A1 → B1 → C1 (C1 depends on B1 which depends on A1)
45
+ g.setDependencies("Sheet1!B1", ["Sheet1!A1"]);
46
+ g.setDependencies("Sheet1!C1", ["Sheet1!B1"]);
47
+
48
+ const order = g.getRecalcOrder(["Sheet1!A1"]);
49
+ expect(order).toEqual(["Sheet1!B1", "Sheet1!C1"]);
50
+ });
51
+
52
+ test("getRecalcOrder handles diamond dependencies", () => {
53
+ const g = new DependencyGraph();
54
+ // A1 → B1, A1 → C1, B1 → D1, C1 → D1
55
+ g.setDependencies("Sheet1!B1", ["Sheet1!A1"]);
56
+ g.setDependencies("Sheet1!C1", ["Sheet1!A1"]);
57
+ g.setDependencies("Sheet1!D1", ["Sheet1!B1", "Sheet1!C1"]);
58
+
59
+ const order = g.getRecalcOrder(["Sheet1!A1"]);
60
+ expect(order).toHaveLength(3);
61
+ // D1 must come after both B1 and C1
62
+ expect(order.indexOf("Sheet1!D1")).toBeGreaterThan(order.indexOf("Sheet1!B1"));
63
+ expect(order.indexOf("Sheet1!D1")).toBeGreaterThan(order.indexOf("Sheet1!C1"));
64
+ });
65
+
66
+ test("getRecalcOrder returns empty for no dependents", () => {
67
+ const g = new DependencyGraph();
68
+ g.setDependencies("Sheet1!B1", ["Sheet1!A1"]);
69
+ expect(g.getRecalcOrder(["Sheet1!C1"])).toEqual([]);
70
+ });
71
+
72
+ test("detectCycle finds simple cycle", () => {
73
+ const g = new DependencyGraph();
74
+ // A1 → B1 → A1 (circular)
75
+ g.setDependencies("Sheet1!A1", ["Sheet1!B1"]);
76
+ g.setDependencies("Sheet1!B1", ["Sheet1!A1"]);
77
+
78
+ const cycle = g.detectCycle("Sheet1!A1");
79
+ expect(cycle).not.toBeNull();
80
+ expect(cycle).toContain("Sheet1!A1");
81
+ });
82
+
83
+ test("detectCycle returns null for no cycle", () => {
84
+ const g = new DependencyGraph();
85
+ g.setDependencies("Sheet1!B1", ["Sheet1!A1"]);
86
+ expect(g.detectCycle("Sheet1!B1")).toBeNull();
87
+ });
88
+
89
+ test("clear resets the graph", () => {
90
+ const g = new DependencyGraph();
91
+ g.setDependencies("Sheet1!B1", ["Sheet1!A1"]);
92
+ g.clear();
93
+ expect(g.size).toBe(0);
94
+ expect(g.getDirectDependents("Sheet1!A1")).toEqual([]);
95
+ });
96
+ });
97
+
98
+ // =============================================================================
99
+ // FormulaEngine integration
100
+ // =============================================================================
101
+
102
+ function makeWorkbook(): WorkbookModel {
103
+ const styles = new StyleRegistry();
104
+ const sheet = new SheetModel("Sheet1");
105
+ return new WorkbookModel({ sheets: [sheet], activeSheetIndex: 0, styles });
106
+ }
107
+
108
+ describe("FormulaEngine", () => {
109
+ test("evaluates SUM formula", () => {
110
+ const wb = makeWorkbook();
111
+ const sheet = wb.sheets[0];
112
+ sheet.set("A1", 10);
113
+ sheet.set("A2", 20);
114
+ sheet.set("A3", null, undefined, undefined, "SUM(A1:A2)");
115
+
116
+ const engine = new FormulaEngine(wb);
117
+ engine.buildGraph();
118
+ engine.evaluateMissing();
119
+
120
+ expect(sheet.getCell("A3")?.value).toBe(30);
121
+ expect(sheet.getCell("A3")?.displayValue).toBe("30");
122
+ });
123
+
124
+ test("evaluates IF formula", () => {
125
+ const wb = makeWorkbook();
126
+ const sheet = wb.sheets[0];
127
+ sheet.set("A1", 10);
128
+ sheet.set("B1", null, undefined, undefined, 'IF(A1>5,"big","small")');
129
+
130
+ const engine = new FormulaEngine(wb);
131
+ engine.buildGraph();
132
+ engine.evaluateMissing();
133
+
134
+ expect(sheet.getCell("B1")?.value).toBe("big");
135
+ });
136
+
137
+ test("recalculates dependents on cell change", () => {
138
+ const wb = makeWorkbook();
139
+ const sheet = wb.sheets[0];
140
+ sheet.set("A1", 10);
141
+ sheet.set("B1", null, undefined, undefined, "A1*2");
142
+
143
+ const engine = new FormulaEngine(wb);
144
+ engine.buildGraph();
145
+ engine.evaluateMissing();
146
+
147
+ expect(sheet.getCell("B1")?.value).toBe(20);
148
+
149
+ // Change A1
150
+ sheet.set("A1", 50);
151
+ engine.onCellChanged("Sheet1", "A1");
152
+
153
+ expect(sheet.getCell("B1")?.value).toBe(100);
154
+ });
155
+
156
+ test("builds dependency graph from formula cells", () => {
157
+ const wb = makeWorkbook();
158
+ const sheet = wb.sheets[0];
159
+ sheet.set("A1", 1);
160
+ sheet.set("A2", 2);
161
+ sheet.set("A3", null, undefined, undefined, "SUM(A1:A2)");
162
+
163
+ const engine = new FormulaEngine(wb);
164
+ engine.buildGraph();
165
+
166
+ expect(engine.deps.size).toBeGreaterThan(0);
167
+ });
168
+
169
+ test("handles formula errors gracefully", () => {
170
+ const wb = makeWorkbook();
171
+ const sheet = wb.sheets[0];
172
+ sheet.set("A1", null, undefined, undefined, "1/0");
173
+
174
+ const engine = new FormulaEngine(wb);
175
+ engine.buildGraph();
176
+ engine.evaluateMissing();
177
+
178
+ // Should not throw. fast-formula-parser returns #DIV/0! FormulaError for 1/0.
179
+ const cell = sheet.getCell("A1");
180
+ expect(cell).toBeDefined();
181
+ expect(cell?.error).toBe("#DIV/0!");
182
+ expect(cell?.displayValue).toBe("#DIV/0!");
183
+ });
184
+
185
+ test("chain recalculation: A1 → B1 → C1", () => {
186
+ const wb = makeWorkbook();
187
+ const sheet = wb.sheets[0];
188
+ sheet.set("A1", 5);
189
+ sheet.set("B1", null, undefined, undefined, "A1+10");
190
+ sheet.set("C1", null, undefined, undefined, "B1*2");
191
+
192
+ const engine = new FormulaEngine(wb);
193
+ engine.buildGraph();
194
+ engine.evaluateMissing();
195
+
196
+ expect(sheet.getCell("B1")?.value).toBe(15);
197
+ expect(sheet.getCell("C1")?.value).toBe(30);
198
+
199
+ // Change A1
200
+ sheet.set("A1", 100);
201
+ engine.onCellChanged("Sheet1", "A1");
202
+
203
+ expect(sheet.getCell("B1")?.value).toBe(110);
204
+ expect(sheet.getCell("C1")?.value).toBe(220);
205
+ });
206
+
207
+ test("new formula cell gets dependency tracking", () => {
208
+ const wb = makeWorkbook();
209
+ const sheet = wb.sheets[0];
210
+ sheet.set("A1", 10);
211
+
212
+ const engine = new FormulaEngine(wb);
213
+ engine.buildGraph();
214
+
215
+ // Add a new formula
216
+ sheet.set("B1", null, undefined, undefined, "A1+5");
217
+ engine.onCellChanged("Sheet1", "B1", "A1+5");
218
+
219
+ expect(sheet.getCell("B1")?.value).toBe(15);
220
+
221
+ // Change A1 — B1 should recalculate
222
+ sheet.set("A1", 20);
223
+ engine.onCellChanged("Sheet1", "A1");
224
+
225
+ expect(sheet.getCell("B1")?.value).toBe(25);
226
+ });
227
+
228
+ test("preserves cached values for cells with values", () => {
229
+ const wb = makeWorkbook();
230
+ const sheet = wb.sheets[0];
231
+ // Cell with both a formula and a cached value
232
+ sheet.set("A1", 10);
233
+ sheet.set("B1", 42, undefined, undefined, "A1+32");
234
+
235
+ const engine = new FormulaEngine(wb);
236
+ engine.buildGraph();
237
+ engine.evaluateMissing(); // Should NOT re-evaluate B1 since it has a cached value
238
+
239
+ // B1 should keep its cached value
240
+ expect(sheet.getCell("B1")?.value).toBe(42);
241
+ });
242
+
243
+ test("evaluateAll overwrites stale cached values — template-generation case", () => {
244
+ // Regression: template placeholder substitution rewrites the cells a
245
+ // formula references but leaves the formula's own `<v>` cache
246
+ // untouched. Real Excel recomputes on open; our renderer trusts the
247
+ // cache and shows the stale value (often a zero rendered via the
248
+ // zero-section of the numFmt, e.g. `"-"`). evaluateAll brings every
249
+ // formula back into sync with current cell values.
250
+ const wb = makeWorkbook();
251
+ const sheet = wb.sheets[0];
252
+ sheet.set("A1", 100);
253
+ sheet.set("A2", 200);
254
+ // Stale cache: the formula reads A1+A2 (=300) but the writer left an
255
+ // old value from a different input set.
256
+ sheet.set("B1", 0, undefined, undefined, "A1+A2");
257
+
258
+ const engine = new FormulaEngine(wb);
259
+ engine.buildGraph();
260
+ engine.evaluateAll();
261
+
262
+ expect(sheet.getCell("B1")?.value).toBe(300);
263
+ });
264
+
265
+ test("recomputeAll always overwrites cached values", () => {
266
+ const wb = makeWorkbook();
267
+ wb.sheets[0].set("A1", 10);
268
+ wb.sheets[0].set("B1", 99, undefined, undefined, "A1*2");
269
+
270
+ // No flag needed — recomputeAll is unconditional.
271
+ recomputeAll(wb);
272
+ expect(wb.sheets[0].getCell("B1")?.value).toBe(20);
273
+ });
274
+
275
+ test("recomputeIfStale fires iff workbook.fullCalcOnLoad is set", () => {
276
+ // No flag → cached stale value preserved (engine never runs).
277
+ const wbA = makeWorkbook();
278
+ wbA.sheets[0].set("A1", 10);
279
+ wbA.sheets[0].set("B1", 99, undefined, undefined, "A1*2");
280
+ wbA.fullCalcOnLoad = false;
281
+ recomputeIfStale(wbA);
282
+ expect(wbA.sheets[0].getCell("B1")?.value).toBe(99);
283
+
284
+ // Flag set → cached value overwritten by live computation.
285
+ const wbB = makeWorkbook();
286
+ wbB.sheets[0].set("A1", 10);
287
+ wbB.sheets[0].set("B1", 99, undefined, undefined, "A1*2");
288
+ wbB.fullCalcOnLoad = true;
289
+ recomputeIfStale(wbB);
290
+ expect(wbB.sheets[0].getCell("B1")?.value).toBe(20);
291
+ });
292
+
293
+ test("evaluateAll recomputes SUM over numeric cells after range expansion", () => {
294
+ // Matches the VN báo giá pipeline: template row `SUM(H8:H8)` is copied
295
+ // to `SUM(H19:H19)` when the items loop expands, then the data cells
296
+ // get numeric values from the agent's payload. The formula's cached
297
+ // value stays `0` until someone evaluates it.
298
+ const wb = makeWorkbook();
299
+ const sheet = wb.sheets[0];
300
+ sheet.set("H19", 15425000);
301
+ sheet.set("H20", 0, undefined, undefined, "SUM(H19:H19)");
302
+
303
+ const engine = new FormulaEngine(wb);
304
+ engine.buildGraph();
305
+ engine.evaluateAll();
306
+
307
+ expect(sheet.getCell("H20")?.value).toBe(15425000);
308
+ });
309
+
310
+ test("evaluateExpression resolves cell references", () => {
311
+ const wb = makeWorkbook();
312
+ const sheet = wb.sheets[0];
313
+ sheet.set("C5", 999);
314
+
315
+ const engine = new FormulaEngine(wb);
316
+ const result = engine.evaluateExpression("C5+1", "Sheet1", 1, 1);
317
+ expect(result).toBe(1000);
318
+ });
319
+
320
+ test("evaluateExpression handles SUM over range", () => {
321
+ const wb = makeWorkbook();
322
+ const sheet = wb.sheets[0];
323
+ sheet.set("A1", 10);
324
+ sheet.set("A2", 20);
325
+ sheet.set("A3", 30);
326
+
327
+ const engine = new FormulaEngine(wb);
328
+ const result = engine.evaluateExpression("SUM(A1:A3)", "Sheet1", 1, 1);
329
+ expect(result).toBe(60);
330
+ });
331
+
332
+ test("evaluateExpression returns formula result", () => {
333
+ const wb = makeWorkbook();
334
+ const sheet = wb.sheets[0];
335
+ sheet.set("A1", 150);
336
+
337
+ const engine = new FormulaEngine(wb);
338
+ const result = engine.evaluateExpression("A1>100", "Sheet1", 1, 1);
339
+ expect(result).toBe(true);
340
+ });
341
+
342
+ test("evaluateExpression returns null for errors", () => {
343
+ const wb = makeWorkbook();
344
+ const engine = new FormulaEngine(wb);
345
+ const result = engine.evaluateExpression("NONEXISTENT()", "Sheet1", 1, 1);
346
+ expect(result).toBeNull();
347
+ });
348
+ });