@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.
@@ -41,9 +41,10 @@ function tokenizeFormula(formula) {
41
41
  str += input[i];
42
42
  i++;
43
43
  }
44
- if (i < input.length && input[i] === quote) {
45
- i++;
44
+ if (i >= input.length) {
45
+ throw new Error(`Unterminated string starting at position ${start + offset}`);
46
46
  }
47
+ i++;
47
48
  tokens.push({
48
49
  type: "STRING",
49
50
  value: str,
@@ -51,6 +52,15 @@ function tokenizeFormula(formula) {
51
52
  });
52
53
  continue;
53
54
  }
55
+ if (char === "#") {
56
+ const match = /^#(?:REF!|DIV\/0!|VALUE!|NAME\?|N\/A|NUM!|CYCLE!|ERROR!)/.exec(input.slice(i));
57
+ if (!match) {
58
+ throw new Error(`Unexpected character '#' at position ${i + offset}`);
59
+ }
60
+ tokens.push({ type: "ERROR", value: match[0], position: i + offset });
61
+ i += match[0].length;
62
+ continue;
63
+ }
54
64
  if (char === "[") {
55
65
  const start = i;
56
66
  i++;
@@ -148,13 +158,14 @@ function tokenizeFormula(formula) {
148
158
  });
149
159
  continue;
150
160
  }
151
- i++;
161
+ throw new Error(`Unexpected character '${char}' at position ${i + offset}`);
152
162
  }
153
163
  tokens.push({ type: "EOF", value: "", position: input.length + offset });
154
164
  return tokens;
155
165
  }
156
166
 
157
167
  // src/spreadsheet/formula/parser.ts
168
+ var WHOLE_COLUMN_END_ROW = 1048576;
158
169
  function parseCellReference(ref) {
159
170
  const match = ref.match(/^([A-Za-z]+)(\d+)$/);
160
171
  if (!match) {
@@ -166,12 +177,23 @@ function parseCellReference(ref) {
166
177
  };
167
178
  }
168
179
  function parseRange(rangeStr) {
169
- const [start, end] = rangeStr.split(":");
180
+ const [start, end] = rangeStr.replace(/\$/g, "").split(":");
181
+ const raw = rangeStr.toUpperCase();
182
+ if (/^[A-Za-z]+$/.test(start) && /^[A-Za-z]+$/.test(end)) {
183
+ return {
184
+ type: "Range",
185
+ raw,
186
+ startCol: start.toUpperCase(),
187
+ startRow: 1,
188
+ endCol: end.toUpperCase(),
189
+ endRow: WHOLE_COLUMN_END_ROW
190
+ };
191
+ }
170
192
  const startRef = parseCellReference(start);
171
193
  const endRef = parseCellReference(end);
172
194
  return {
173
195
  type: "Range",
174
- raw: rangeStr.toUpperCase(),
196
+ raw,
175
197
  startCol: startRef.col,
176
198
  startRow: startRef.row,
177
199
  endCol: endRef.col,
@@ -264,6 +286,10 @@ var FormulaParser = (_class = class {
264
286
  this.advance();
265
287
  return { type: "Literal", value: token.value === "TRUE" };
266
288
  }
289
+ if (token.type === "ERROR") {
290
+ this.advance();
291
+ return { type: "Literal", value: token.value };
292
+ }
267
293
  if (token.type === "CELL_REF") {
268
294
  this.advance();
269
295
  const parsed = parseCellReference(token.value);
@@ -303,7 +329,7 @@ var FormulaParser = (_class = class {
303
329
  args
304
330
  };
305
331
  }
306
- return { type: "Literal", value: name };
332
+ return { type: "Literal", value: "#NAME?" };
307
333
  }
308
334
  if (this.match("LPAREN")) {
309
335
  const expr = this.expression();
@@ -357,6 +383,57 @@ function parseFormula(formula) {
357
383
  }
358
384
 
359
385
  // src/spreadsheet/formula/evaluator.ts
386
+ var FORMULA_ERRORS = /* @__PURE__ */ new Set([
387
+ "#DIV/0!",
388
+ "#VALUE!",
389
+ "#REF!",
390
+ "#NAME?",
391
+ "#N/A",
392
+ "#NUM!",
393
+ "#CYCLE!",
394
+ "#ERROR!"
395
+ ]);
396
+ function isFormulaError(value) {
397
+ return typeof value === "string" && FORMULA_ERRORS.has(value);
398
+ }
399
+ var isBlank = (value) => value === "" || value === null || value === void 0;
400
+ function toNumber(value) {
401
+ if (isFormulaError(value)) return value;
402
+ if (isBlank(value)) return 0;
403
+ if (typeof value === "boolean") return value ? 1 : 0;
404
+ if (typeof value === "number") return value;
405
+ const trimmed = String(value).trim();
406
+ const num = trimmed === "" ? NaN : Number(trimmed);
407
+ return isNaN(num) ? "#VALUE!" : num;
408
+ }
409
+ function toBoolean(value) {
410
+ if (isFormulaError(value)) return value;
411
+ if (isBlank(value)) return false;
412
+ if (typeof value === "boolean") return value;
413
+ if (typeof value === "number") return value !== 0;
414
+ const upper = String(value).trim().toUpperCase();
415
+ if (upper === "TRUE") return true;
416
+ if (upper === "FALSE") return false;
417
+ return "#VALUE!";
418
+ }
419
+ var toText = (value) => isBlank(value) ? "" : typeof value === "boolean" ? value ? "TRUE" : "FALSE" : String(value);
420
+ function compareValues(left, right) {
421
+ const rank = (v) => typeof v === "boolean" ? 2 : typeof v === "string" ? 1 : 0;
422
+ const l = isBlank(left) ? typeof right === "string" ? "" : 0 : left;
423
+ const r = isBlank(right) ? typeof l === "string" ? "" : 0 : right;
424
+ if (rank(l) !== rank(r)) return rank(l) - rank(r);
425
+ if (typeof l === "string") {
426
+ const a = l.toLowerCase();
427
+ const b = r.toLowerCase();
428
+ return a < b ? -1 : a > b ? 1 : 0;
429
+ }
430
+ return Number(l) - Number(r);
431
+ }
432
+ function roundHalfAwayFromZero(value, decimals) {
433
+ const factor = Math.pow(10, decimals);
434
+ const scaled = Number((Math.abs(value) * factor).toPrecision(15));
435
+ return Math.sign(value) * Math.round(scaled) / factor;
436
+ }
360
437
  function colNameToIndex(col) {
361
438
  let index = 0;
362
439
  const upper = col.toUpperCase();
@@ -407,42 +484,59 @@ var FormulaEvaluator = class {
407
484
  return this.context.resolveProperty(node.property);
408
485
  }
409
486
  case "UnaryOp": {
410
- const val = this.evaluateNode(node.argument);
411
- if (node.op === "-") return -Number(val);
412
- if (node.op === "+") return +Number(val);
487
+ const val = toNumber(this.evaluateNode(node.argument));
488
+ if (isFormulaError(val)) return val;
489
+ if (node.op === "-") return -val;
490
+ if (node.op === "+") return val;
413
491
  throw new Error(`Unsupported unary operator: ${node.op}`);
414
492
  }
415
493
  case "BinaryOp": {
416
494
  const left = this.evaluateNode(node.left);
495
+ if (isFormulaError(left)) return left;
417
496
  const right = this.evaluateNode(node.right);
418
- switch (node.op) {
419
- case "+":
420
- return Number(left) + Number(right);
421
- case "-":
422
- return Number(left) - Number(right);
423
- case "*":
424
- return Number(left) * Number(right);
425
- case "/": {
426
- const denom = Number(right);
427
- if (denom === 0) return "#DIV/0!";
428
- return Number(left) / denom;
497
+ if (isFormulaError(right)) return right;
498
+ if (["+", "-", "*", "/", "^"].includes(node.op)) {
499
+ const a = toNumber(left);
500
+ if (isFormulaError(a)) return a;
501
+ const b = toNumber(right);
502
+ if (isFormulaError(b)) return b;
503
+ const x = a;
504
+ const y = b;
505
+ let result;
506
+ switch (node.op) {
507
+ case "+":
508
+ result = x + y;
509
+ break;
510
+ case "-":
511
+ result = x - y;
512
+ break;
513
+ case "*":
514
+ result = x * y;
515
+ break;
516
+ case "/":
517
+ if (y === 0) return "#DIV/0!";
518
+ result = x / y;
519
+ break;
520
+ default:
521
+ result = Math.pow(x, y);
429
522
  }
430
- case "^":
431
- return Math.pow(Number(left), Number(right));
523
+ return Number.isFinite(result) ? result : "#NUM!";
524
+ }
525
+ switch (node.op) {
432
526
  case "&":
433
- return `${_nullishCoalesce(left, () => ( ""))}${_nullishCoalesce(right, () => ( ""))}`;
527
+ return toText(left) + toText(right);
434
528
  case "=":
435
- return String(left).toLowerCase() === String(right).toLowerCase();
529
+ return compareValues(left, right) === 0;
436
530
  case "<>":
437
- return String(left).toLowerCase() !== String(right).toLowerCase();
531
+ return compareValues(left, right) !== 0;
438
532
  case "<":
439
- return Number(left) < Number(right);
533
+ return compareValues(left, right) < 0;
440
534
  case "<=":
441
- return Number(left) <= Number(right);
535
+ return compareValues(left, right) <= 0;
442
536
  case ">":
443
- return Number(left) > Number(right);
537
+ return compareValues(left, right) > 0;
444
538
  case ">=":
445
- return Number(left) >= Number(right);
539
+ return compareValues(left, right) >= 0;
446
540
  default:
447
541
  throw new Error(`Unsupported binary operator: ${node.op}`);
448
542
  }
@@ -461,25 +555,36 @@ var FormulaEvaluator = class {
461
555
  for (const arg of args) {
462
556
  const val = this.evaluateNode(arg);
463
557
  if (Array.isArray(val)) {
464
- items.push(...val);
558
+ for (const item of val) items.push(item);
465
559
  } else {
466
560
  items.push(val);
467
561
  }
468
562
  }
469
563
  return items;
470
564
  };
565
+ const numericArgs = () => {
566
+ const nums = [];
567
+ for (const item of evaluateAndFlatten()) {
568
+ if (isFormulaError(item)) return item;
569
+ if (isBlank(item) || typeof item === "boolean") continue;
570
+ const num = typeof item === "number" ? item : Number(String(item).trim());
571
+ if (String(item).trim() !== "" && !isNaN(num)) nums.push(num);
572
+ }
573
+ return nums;
574
+ };
575
+ const evaluateArg = (index) => this.evaluateNode(args[index]);
471
576
  switch (fnName) {
472
577
  case "SUM": {
473
- const items = evaluateAndFlatten();
578
+ const nums = numericArgs();
579
+ if (typeof nums === "string") return nums;
474
580
  let total = 0;
475
- for (const item of items) {
476
- const num = Number(item);
477
- if (!isNaN(num)) total += num;
478
- }
581
+ for (const num of nums) total += num;
479
582
  return total;
480
583
  }
481
584
  case "AVERAGE": {
482
585
  const items = evaluateAndFlatten();
586
+ const error = items.find(isFormulaError);
587
+ if (error) return error;
483
588
  let total = 0;
484
589
  let count = 0;
485
590
  for (const item of items) {
@@ -512,17 +617,21 @@ var FormulaEvaluator = class {
512
617
  }
513
618
  return count;
514
619
  }
515
- case "MIN": {
516
- const items = evaluateAndFlatten().map(Number).filter((n) => !isNaN(n));
517
- return items.length === 0 ? 0 : Math.min(...items);
518
- }
620
+ case "MIN":
519
621
  case "MAX": {
520
- const items = evaluateAndFlatten().map(Number).filter((n) => !isNaN(n));
521
- return items.length === 0 ? 0 : Math.max(...items);
622
+ const nums = numericArgs();
623
+ if (typeof nums === "string") return nums;
624
+ if (nums.length === 0) return 0;
625
+ let result = nums[0];
626
+ for (const num of nums) {
627
+ result = fnName === "MIN" ? Math.min(result, num) : Math.max(result, num);
628
+ }
629
+ return result;
522
630
  }
523
631
  case "IF": {
524
632
  if (args.length < 2) throw new Error("IF requires at least 2 arguments");
525
- const condition = Boolean(this.evaluateNode(args[0]));
633
+ const condition = toBoolean(evaluateArg(0));
634
+ if (isFormulaError(condition)) return condition;
526
635
  if (condition) {
527
636
  return this.evaluateNode(args[1]);
528
637
  } else {
@@ -531,41 +640,51 @@ var FormulaEvaluator = class {
531
640
  }
532
641
  case "CONCAT": {
533
642
  const items = evaluateAndFlatten();
534
- return items.map((i) => i === null || i === void 0 ? "" : String(i)).join("");
643
+ const error = items.find(isFormulaError);
644
+ if (error) return error;
645
+ return items.map(toText).join("");
535
646
  }
536
647
  case "UPPER": {
537
648
  if (args.length !== 1) throw new Error("UPPER requires 1 argument");
538
- const val = this.evaluateNode(args[0]);
539
- return String(_nullishCoalesce(val, () => ( ""))).toUpperCase();
649
+ const val = evaluateArg(0);
650
+ if (isFormulaError(val)) return val;
651
+ return toText(val).toUpperCase();
540
652
  }
541
653
  case "LOWER": {
542
654
  if (args.length !== 1) throw new Error("LOWER requires 1 argument");
543
- const val = this.evaluateNode(args[0]);
544
- return String(_nullishCoalesce(val, () => ( ""))).toLowerCase();
655
+ const val = evaluateArg(0);
656
+ if (isFormulaError(val)) return val;
657
+ return toText(val).toLowerCase();
545
658
  }
546
659
  case "TRIM": {
547
660
  if (args.length !== 1) throw new Error("TRIM requires 1 argument");
548
- const val = this.evaluateNode(args[0]);
549
- return String(_nullishCoalesce(val, () => ( ""))).trim();
661
+ const val = evaluateArg(0);
662
+ if (isFormulaError(val)) return val;
663
+ return toText(val).trim();
550
664
  }
551
665
  case "LEN": {
552
666
  if (args.length !== 1) throw new Error("LEN requires 1 argument");
553
- const val = this.evaluateNode(args[0]);
554
- return String(_nullishCoalesce(val, () => ( ""))).length;
667
+ const val = evaluateArg(0);
668
+ if (isFormulaError(val)) return val;
669
+ return toText(val).length;
555
670
  }
556
671
  case "ROUND": {
557
672
  if (args.length < 1) throw new Error("ROUND requires at least 1 argument");
558
- const num = Number(this.evaluateNode(args[0]));
559
- const decimals = args.length > 1 ? Number(this.evaluateNode(args[1])) : 0;
560
- const factor = Math.pow(10, decimals);
561
- return Math.round(num * factor) / factor;
673
+ const num = toNumber(evaluateArg(0));
674
+ if (isFormulaError(num)) return num;
675
+ const decimals = args.length > 1 ? toNumber(evaluateArg(1)) : 0;
676
+ if (isFormulaError(decimals)) return decimals;
677
+ return roundHalfAwayFromZero(num, Math.trunc(decimals));
562
678
  }
563
679
  case "ABS": {
564
680
  if (args.length !== 1) throw new Error("ABS requires 1 argument");
565
- return Math.abs(Number(this.evaluateNode(args[0])));
681
+ const num = toNumber(evaluateArg(0));
682
+ return isFormulaError(num) ? num : Math.abs(num);
566
683
  }
567
684
  case "TODAY": {
568
- return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
685
+ const now = /* @__PURE__ */ new Date();
686
+ const pad = (n) => String(n).padStart(2, "0");
687
+ return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
569
688
  }
570
689
  case "DAYS": {
571
690
  if (args.length !== 2) throw new Error("DAYS requires 2 arguments (end_date, start_date)");
@@ -575,17 +694,24 @@ var FormulaEvaluator = class {
575
694
  return Math.round(diffTime / (1e3 * 60 * 60 * 24));
576
695
  }
577
696
  default:
578
- throw new Error(`Unknown function: ${fnName}`);
697
+ return "#NAME?";
579
698
  }
580
699
  }
581
700
  };
582
701
 
583
702
  // src/spreadsheet/formula/dependency-graph.ts
703
+ var columnDependencyKey = (col) => `${col.toUpperCase()}:`;
584
704
  function extractCellDependencies(node) {
585
705
  const deps = [];
586
706
  const visit = (curr) => {
587
707
  if (curr.type === "CellRef") {
588
708
  deps.push({ col: curr.col, row: curr.row });
709
+ } else if (curr.type === "Range" && curr.endRow >= WHOLE_COLUMN_END_ROW) {
710
+ const startC = colNameToIndex(curr.startCol);
711
+ const endC = colNameToIndex(curr.endCol);
712
+ for (let c = Math.min(startC, endC); c <= Math.max(startC, endC); c++) {
713
+ deps.push({ col: indexToColName(c), row: 0 });
714
+ }
589
715
  } else if (curr.type === "Range") {
590
716
  const startC = colNameToIndex(curr.startCol);
591
717
  const endC = colNameToIndex(curr.endCol);
@@ -630,7 +756,7 @@ var DependencyGraph = (_class2 = class {constructor() { _class2.prototype.__init
630
756
  const deps = extractCellDependencies(ast);
631
757
  const newDeps = /* @__PURE__ */ new Set();
632
758
  for (const d of deps) {
633
- const depKey = `${d.col}${d.row}`.toUpperCase();
759
+ const depKey = d.row === 0 ? columnDependencyKey(d.col) : `${d.col}${d.row}`.toUpperCase();
634
760
  newDeps.add(depKey);
635
761
  if (!this.dependents.has(depKey)) {
636
762
  this.dependents.set(depKey, /* @__PURE__ */ new Set());
@@ -641,6 +767,11 @@ var DependencyGraph = (_class2 = class {constructor() { _class2.prototype.__init
641
767
  } catch (e) {
642
768
  }
643
769
  }
770
+ /** Removes every registered formula and dependency. */
771
+ clear() {
772
+ this.dependencies.clear();
773
+ this.dependents.clear();
774
+ }
644
775
  getDependencies(cellKey) {
645
776
  return Array.from(this.dependencies.get(cellKey.toUpperCase()) || []);
646
777
  }
@@ -714,8 +845,191 @@ var DependencyGraph = (_class2 = class {constructor() { _class2.prototype.__init
714
845
  }
715
846
  return { order, hasCycle: cycleDetected };
716
847
  }
848
+ /**
849
+ * Plans a recalculation over `cellKeys` (and, with `includeDependents`, every
850
+ * cell that transitively depends on them). Returns the keys in evaluation
851
+ * order (dependencies first) and the set of keys that are part of a cycle,
852
+ * which must be marked `#CYCLE!` instead of evaluated. Every member of a
853
+ * cycle is reported, not just the cell that closed it.
854
+ */
855
+ getEvaluationPlan(cellKeys, options = {}) {
856
+ const nodes = /* @__PURE__ */ new Set();
857
+ const queue = [];
858
+ for (const key of cellKeys) {
859
+ const upper = key.toUpperCase();
860
+ if (!nodes.has(upper)) {
861
+ nodes.add(upper);
862
+ queue.push(upper);
863
+ }
864
+ }
865
+ if (options.includeDependents) {
866
+ while (queue.length > 0) {
867
+ const node = queue.pop();
868
+ const direct = [
869
+ ..._nullishCoalesce(this.dependents.get(node), () => ( [])),
870
+ ..._nullishCoalesce(this.dependents.get(columnDependencyKey(node.replace(/\d+$/, ""))), () => ( []))
871
+ ];
872
+ for (const dep of direct) {
873
+ if (!nodes.has(dep)) {
874
+ nodes.add(dep);
875
+ queue.push(dep);
876
+ }
877
+ }
878
+ }
879
+ }
880
+ const neighbors = (node) => {
881
+ const result = [];
882
+ for (const dep of _nullishCoalesce(this.dependencies.get(node), () => ( []))) {
883
+ if (dep.endsWith(":")) {
884
+ const col = dep.slice(0, -1);
885
+ for (const candidate of nodes) {
886
+ if (candidate.replace(/\d+$/, "") === col) result.push(candidate);
887
+ }
888
+ } else if (nodes.has(dep)) {
889
+ result.push(dep);
890
+ }
891
+ }
892
+ return result;
893
+ };
894
+ const order = [];
895
+ const cyclic = /* @__PURE__ */ new Set();
896
+ const index = /* @__PURE__ */ new Map();
897
+ const low = /* @__PURE__ */ new Map();
898
+ const stack = [];
899
+ const onStack = /* @__PURE__ */ new Set();
900
+ let counter = 0;
901
+ for (const root of nodes) {
902
+ if (index.has(root)) continue;
903
+ const work = [];
904
+ const enter = (node) => {
905
+ index.set(node, counter);
906
+ low.set(node, counter);
907
+ counter++;
908
+ stack.push(node);
909
+ onStack.add(node);
910
+ work.push({ node, edges: neighbors(node), next: 0 });
911
+ };
912
+ enter(root);
913
+ while (work.length > 0) {
914
+ const frame = work[work.length - 1];
915
+ if (frame.next < frame.edges.length) {
916
+ const target = frame.edges[frame.next++];
917
+ if (!index.has(target)) {
918
+ enter(target);
919
+ } else if (onStack.has(target)) {
920
+ low.set(frame.node, Math.min(low.get(frame.node), index.get(target)));
921
+ }
922
+ continue;
923
+ }
924
+ work.pop();
925
+ if (work.length > 0) {
926
+ const parent = work[work.length - 1].node;
927
+ low.set(parent, Math.min(low.get(parent), low.get(frame.node)));
928
+ }
929
+ if (low.get(frame.node) === index.get(frame.node)) {
930
+ const component = [];
931
+ let member;
932
+ do {
933
+ member = stack.pop();
934
+ onStack.delete(member);
935
+ component.push(member);
936
+ } while (member !== frame.node);
937
+ const isCycle = component.length > 1 || frame.edges.includes(frame.node);
938
+ for (const key of component) {
939
+ order.push(key);
940
+ if (isCycle) cyclic.add(key);
941
+ }
942
+ }
943
+ }
944
+ }
945
+ return { order, cyclic };
946
+ }
717
947
  }, _class2);
718
948
 
949
+ // src/spreadsheet/formula/references.ts
950
+ var CELL_PART = /^(\$?)([A-Za-z]+)(\$?)(\d+)$/;
951
+ var COLUMN_PART = /^(\$?)([A-Za-z]+)$/;
952
+ var REF_CHAR = /[A-Za-z0-9_$.:]/;
953
+ function parsePart(text) {
954
+ const cell = CELL_PART.exec(text);
955
+ if (cell) {
956
+ return {
957
+ colAbs: cell[1],
958
+ col: colNameToIndex(cell[2]),
959
+ rowAbs: cell[3],
960
+ row: parseInt(cell[4], 10) - 1
961
+ };
962
+ }
963
+ const column = COLUMN_PART.exec(text);
964
+ if (column) {
965
+ return { colAbs: column[1], col: colNameToIndex(column[2]), rowAbs: "", row: null };
966
+ }
967
+ return null;
968
+ }
969
+ function formatPart(part) {
970
+ const col = `${part.colAbs}${indexToColName(part.col)}`;
971
+ return part.row === null ? col : `${col}${part.rowAbs}${part.row + 1}`;
972
+ }
973
+ function shiftCoordinate(value, change) {
974
+ if (change.type === "insert") {
975
+ return value >= change.index ? value + 1 : value;
976
+ }
977
+ if (value === change.index) return null;
978
+ return value > change.index ? value - 1 : value;
979
+ }
980
+ function shiftSpan(lo, hi, change) {
981
+ if (change.type === "insert") {
982
+ return [lo >= change.index ? lo + 1 : lo, hi >= change.index ? hi + 1 : hi];
983
+ }
984
+ if (lo === change.index && hi === change.index) return null;
985
+ return [lo > change.index ? lo - 1 : lo, hi >= change.index ? hi - 1 : hi];
986
+ }
987
+ function rewriteReference(text, change) {
988
+ const [startText, endText] = text.split(":");
989
+ const start = parsePart(startText);
990
+ if (!start) return text;
991
+ const key = change.axis === "row" ? "row" : "col";
992
+ if (endText === void 0) {
993
+ if (start[key] === null) return text;
994
+ const shifted = shiftCoordinate(start[key], change);
995
+ if (shifted === null) return "#REF!";
996
+ return formatPart({ ...start, [key]: shifted });
997
+ }
998
+ const end = parsePart(endText);
999
+ if (!end) return text;
1000
+ if (start[key] === null || end[key] === null) return text;
1001
+ const startValue = start[key];
1002
+ const endValue = end[key];
1003
+ const span = shiftSpan(Math.min(startValue, endValue), Math.max(startValue, endValue), change);
1004
+ if (span === null) return "#REF!";
1005
+ const [lo, hi] = span;
1006
+ const newStart = { ...start, [key]: startValue <= endValue ? lo : hi };
1007
+ const newEnd = { ...end, [key]: startValue <= endValue ? hi : lo };
1008
+ return `${formatPart(newStart)}:${formatPart(newEnd)}`;
1009
+ }
1010
+ function shiftFormulaReferences(formula, change) {
1011
+ if (!formula.startsWith("=")) return formula;
1012
+ let tokens;
1013
+ try {
1014
+ tokens = tokenizeFormula(formula);
1015
+ } catch (e2) {
1016
+ return formula;
1017
+ }
1018
+ let result = "";
1019
+ let cursor = 0;
1020
+ for (let i = 0; i < tokens.length; i++) {
1021
+ const token = tokens[i];
1022
+ if (token.type !== "CELL_REF" && token.type !== "RANGE") continue;
1023
+ if (_optionalChain([tokens, 'access', _7 => _7[i + 1], 'optionalAccess', _8 => _8.type]) === "LPAREN") continue;
1024
+ let end = token.position;
1025
+ while (end < formula.length && REF_CHAR.test(formula[end])) end++;
1026
+ const original = formula.slice(token.position, end);
1027
+ result += formula.slice(cursor, token.position) + rewriteReference(original, change);
1028
+ cursor = end;
1029
+ }
1030
+ return result + formula.slice(cursor);
1031
+ }
1032
+
719
1033
  // src/spreadsheet/datasource/index.ts
720
1034
  var DataSourceBinder = (_class3 = class {constructor() { _class3.prototype.__init5.call(this); }
721
1035
  __init5() {this.dataSources = /* @__PURE__ */ new Map()}
@@ -749,9 +1063,9 @@ var DataSourceBinder = (_class3 = class {constructor() { _class3.prototype.__ini
749
1063
  for (const record of records) {
750
1064
  const existing = existingRecordRowMap.get(record.id);
751
1065
  nextRows.push({
752
- id: _optionalChain([existing, 'optionalAccess', _7 => _7.id]) || `row_${record.id}`,
1066
+ id: _optionalChain([existing, 'optionalAccess', _9 => _9.id]) || `row_${record.id}`,
753
1067
  index: rowIndex++,
754
- height: _optionalChain([existing, 'optionalAccess', _8 => _8.height]) || 32,
1068
+ height: _optionalChain([existing, 'optionalAccess', _10 => _10.height]) || 32,
755
1069
  recordId: record.id,
756
1070
  type: "record"
757
1071
  });
@@ -803,7 +1117,12 @@ var SpreadsheetController = (_class4 = class {
803
1117
  __init12() {this.undoStack = []}
804
1118
  __init13() {this.redoStack = []}
805
1119
  __init14() {this.isApplyingHistory = false}
806
- constructor(options = {}) {;_class4.prototype.__init6.call(this);_class4.prototype.__init7.call(this);_class4.prototype.__init8.call(this);_class4.prototype.__init9.call(this);_class4.prototype.__init10.call(this);_class4.prototype.__init11.call(this);_class4.prototype.__init12.call(this);_class4.prototype.__init13.call(this);_class4.prototype.__init14.call(this);
1120
+ // Set while importing TSV: history and recalculation are done once at the end.
1121
+ __init15() {this.batching = false}
1122
+ __init16() {this.pendingCommits = []}
1123
+ // Row being evaluated, so [property] references resolve per row.
1124
+ __init17() {this.evaluatingRowIndex = null}
1125
+ constructor(options = {}) {;_class4.prototype.__init6.call(this);_class4.prototype.__init7.call(this);_class4.prototype.__init8.call(this);_class4.prototype.__init9.call(this);_class4.prototype.__init10.call(this);_class4.prototype.__init11.call(this);_class4.prototype.__init12.call(this);_class4.prototype.__init13.call(this);_class4.prototype.__init14.call(this);_class4.prototype.__init15.call(this);_class4.prototype.__init16.call(this);_class4.prototype.__init17.call(this);
807
1126
  const defaultCols = [
808
1127
  { id: "col_a", key: "A", title: "A", width: 120, type: "freeform" },
809
1128
  { id: "col_b", key: "B", title: "B", width: 120, type: "freeform" },
@@ -817,13 +1136,13 @@ var SpreadsheetController = (_class4 = class {
817
1136
  type: "freeform"
818
1137
  }));
819
1138
  this.document = {
820
- id: _optionalChain([options, 'access', _9 => _9.document, 'optionalAccess', _10 => _10.id]) || `sheet_${Date.now()}`,
821
- name: _optionalChain([options, 'access', _11 => _11.document, 'optionalAccess', _12 => _12.name]) || "Sheet 1",
822
- columns: _optionalChain([options, 'access', _13 => _13.document, 'optionalAccess', _14 => _14.columns]) || defaultCols,
823
- rows: _optionalChain([options, 'access', _15 => _15.document, 'optionalAccess', _16 => _16.rows]) || defaultRows,
824
- cells: _optionalChain([options, 'access', _17 => _17.document, 'optionalAccess', _18 => _18.cells]) || {},
825
- createdAt: _optionalChain([options, 'access', _19 => _19.document, 'optionalAccess', _20 => _20.createdAt]) || Date.now(),
826
- updatedAt: _optionalChain([options, 'access', _21 => _21.document, 'optionalAccess', _22 => _22.updatedAt]) || Date.now()
1139
+ id: _optionalChain([options, 'access', _11 => _11.document, 'optionalAccess', _12 => _12.id]) || `sheet_${Date.now()}`,
1140
+ name: _optionalChain([options, 'access', _13 => _13.document, 'optionalAccess', _14 => _14.name]) || "Sheet 1",
1141
+ columns: _optionalChain([options, 'access', _15 => _15.document, 'optionalAccess', _16 => _16.columns]) || defaultCols,
1142
+ rows: _optionalChain([options, 'access', _17 => _17.document, 'optionalAccess', _18 => _18.rows]) || defaultRows,
1143
+ cells: _optionalChain([options, 'access', _19 => _19.document, 'optionalAccess', _20 => _20.cells]) || {},
1144
+ createdAt: _optionalChain([options, 'access', _21 => _21.document, 'optionalAccess', _22 => _22.createdAt]) || Date.now(),
1145
+ updatedAt: _optionalChain([options, 'access', _23 => _23.document, 'optionalAccess', _24 => _24.updatedAt]) || Date.now()
827
1146
  };
828
1147
  this.primaryDataSourceId = options.primaryDataSourceId;
829
1148
  this.onDocumentChange = options.onDocumentChange;
@@ -887,10 +1206,14 @@ var SpreadsheetController = (_class4 = class {
887
1206
  return this.document.rows.find((r) => r.id === rowIdOrIndex);
888
1207
  }
889
1208
  getCell(rowId, colId) {
890
- const existing = _optionalChain([this, 'access', _23 => _23.document, 'access', _24 => _24.cells, 'access', _25 => _25[rowId], 'optionalAccess', _26 => _26[colId]]);
1209
+ const existing = _optionalChain([this, 'access', _25 => _25.document, 'access', _26 => _26.cells, 'access', _27 => _27[rowId], 'optionalAccess', _28 => _28[colId]]);
891
1210
  if (existing) return existing;
892
1211
  const col = this.document.columns.find((c) => c.id === colId);
893
1212
  const row = this.document.rows.find((r) => r.id === rowId);
1213
+ return this.getDerivedCell(row, col);
1214
+ }
1215
+ /** Value of a cell with no stored data: bound record value, column formula, or blank. */
1216
+ getDerivedCell(row, col) {
894
1217
  if (col && row && col.type === "bound" && col.binding && row.recordId) {
895
1218
  const boundVal = this.binder.resolveBoundValue(
896
1219
  col.binding.dataSource,
@@ -910,7 +1233,7 @@ var SpreadsheetController = (_class4 = class {
910
1233
  format: col.format
911
1234
  };
912
1235
  }
913
- return { raw: "", value: "", format: _optionalChain([col, 'optionalAccess', _27 => _27.format]) };
1236
+ return { raw: "", value: "", format: _optionalChain([col, 'optionalAccess', _29 => _29.format]) };
914
1237
  }
915
1238
  setCellValue(rowId, colId, raw) {
916
1239
  this.recordSnapshot();
@@ -919,7 +1242,7 @@ var SpreadsheetController = (_class4 = class {
919
1242
  }
920
1243
  const col = this.document.columns.find((c) => c.id === colId);
921
1244
  const row = this.document.rows.find((r) => r.id === rowId);
922
- if (_optionalChain([col, 'optionalAccess', _28 => _28.type]) === "bound" && col.binding && _optionalChain([row, 'optionalAccess', _29 => _29.recordId])) {
1245
+ if (_optionalChain([col, 'optionalAccess', _30 => _30.type]) === "bound" && col.binding && _optionalChain([row, 'optionalAccess', _31 => _31.recordId])) {
923
1246
  this.binder.commitBoundValue(
924
1247
  col.binding.dataSource,
925
1248
  row.recordId,
@@ -932,13 +1255,17 @@ var SpreadsheetController = (_class4 = class {
932
1255
  this.document.cells[rowId][colId] = {
933
1256
  raw,
934
1257
  value: raw,
935
- format: _optionalChain([col, 'optionalAccess', _30 => _30.format])
1258
+ format: _optionalChain([col, 'optionalAccess', _32 => _32.format])
936
1259
  };
937
1260
  this.document.updatedAt = Date.now();
1261
+ if (this.batching) {
1262
+ this.pendingCommits.push([rowId, colId, raw]);
1263
+ return;
1264
+ }
938
1265
  this.recalculateCell(rowId, colId);
939
1266
  this.notifyChange();
940
- const evaluatedVal = _optionalChain([this, 'access', _31 => _31.document, 'access', _32 => _32.cells, 'access', _33 => _33[rowId], 'access', _34 => _34[colId], 'optionalAccess', _35 => _35.value]);
941
- _optionalChain([this, 'access', _36 => _36.onCellCommit, 'optionalCall', _37 => _37(rowId, colId, raw, evaluatedVal)]);
1267
+ const evaluatedVal = _optionalChain([this, 'access', _33 => _33.document, 'access', _34 => _34.cells, 'access', _35 => _35[rowId], 'access', _36 => _36[colId], 'optionalAccess', _37 => _37.value]);
1268
+ _optionalChain([this, 'access', _38 => _38.onCellCommit, 'optionalCall', _39 => _39(rowId, colId, raw, evaluatedVal)]);
942
1269
  }
943
1270
  getCellKey(rowId, colId) {
944
1271
  const row = this.document.rows.find((r) => r.id === rowId);
@@ -946,37 +1273,58 @@ var SpreadsheetController = (_class4 = class {
946
1273
  if (!row || !col) return `${colId}_${rowId}`;
947
1274
  return `${col.key}${row.index + 1}`.toUpperCase();
948
1275
  }
1276
+ /**
1277
+ * Recalculates a cell and everything that depends on it, in dependency
1278
+ * order. Every cell in a reference cycle is marked `#CYCLE!`.
1279
+ */
949
1280
  recalculateCell(rowId, colId) {
950
1281
  const cellKey = this.getCellKey(rowId, colId);
951
- const { order, hasCycle } = this.depGraph.getEvaluationOrder(cellKey);
952
- if (hasCycle) {
953
- if (_optionalChain([this, 'access', _38 => _38.document, 'access', _39 => _39.cells, 'access', _40 => _40[rowId], 'optionalAccess', _41 => _41[colId]])) {
954
- this.document.cells[rowId][colId].error = "#CYCLE!";
955
- this.document.cells[rowId][colId].value = "#CYCLE!";
956
- }
957
- return;
958
- }
959
- for (const key of order) {
960
- this.evalSingleCellByKey(key);
961
- }
1282
+ this.runEvaluationPlan(this.depGraph.getEvaluationPlan([cellKey], { includeDependents: true }));
962
1283
  }
1284
+ /**
1285
+ * Rebuilds the dependency graph from scratch and recalculates every formula
1286
+ * in dependency order.
1287
+ */
963
1288
  recalculateAll() {
1289
+ this.depGraph.clear();
1290
+ const formulaKeys = [];
964
1291
  for (const row of this.document.rows) {
965
1292
  for (const col of this.document.columns) {
966
- const cell = this.getCell(row.id, col.id);
967
- const cellKey = `${col.key}${row.index + 1}`.toUpperCase();
1293
+ const cell = _nullishCoalesce(_optionalChain([this, 'access', _40 => _40.document, 'access', _41 => _41.cells, 'access', _42 => _42[row.id], 'optionalAccess', _43 => _43[col.id]]), () => ( this.getDerivedCell(row, col)));
968
1294
  if (cell.raw.startsWith("=")) {
1295
+ const cellKey = `${col.key}${row.index + 1}`.toUpperCase();
969
1296
  this.depGraph.setCellFormula(cellKey, cell.raw);
1297
+ formulaKeys.push(cellKey);
970
1298
  }
971
1299
  }
972
1300
  }
973
- for (const row of this.document.rows) {
974
- for (const col of this.document.columns) {
975
- const cellKey = `${col.key}${row.index + 1}`.toUpperCase();
976
- this.evalSingleCellByKey(cellKey);
1301
+ this.runEvaluationPlan(this.depGraph.getEvaluationPlan(formulaKeys));
1302
+ }
1303
+ runEvaluationPlan(plan) {
1304
+ for (const key of plan.order) {
1305
+ if (plan.cyclic.has(key)) {
1306
+ this.markCycle(key);
1307
+ } else {
1308
+ this.evalSingleCellByKey(key);
977
1309
  }
978
1310
  }
979
1311
  }
1312
+ locateCellKey(cellKey) {
1313
+ const match = cellKey.match(/^([A-Za-z]+)(\d+)$/);
1314
+ if (!match) return null;
1315
+ const col = this.document.columns.find((c) => c.key.toUpperCase() === match[1].toUpperCase());
1316
+ const row = this.document.rows[parseInt(match[2], 10) - 1];
1317
+ return col && row ? { row, col } : null;
1318
+ }
1319
+ markCycle(cellKey) {
1320
+ const located = this.locateCellKey(cellKey);
1321
+ if (!located) return;
1322
+ const { row, col } = located;
1323
+ const cell = this.getCell(row.id, col.id);
1324
+ if (!cell.raw.startsWith("=")) return;
1325
+ if (!this.document.cells[row.id]) this.document.cells[row.id] = {};
1326
+ this.document.cells[row.id][col.id] = { ...cell, value: "#CYCLE!", error: "#CYCLE!" };
1327
+ }
980
1328
  evalSingleCellByKey(cellKey) {
981
1329
  const match = cellKey.match(/^([A-Za-z]+)(\d+)$/);
982
1330
  if (!match) return;
@@ -993,6 +1341,7 @@ var SpreadsheetController = (_class4 = class {
993
1341
  return;
994
1342
  }
995
1343
  try {
1344
+ this.evaluatingRowIndex = rowNum - 1;
996
1345
  const computed = this.evaluator.evaluate(cell.raw);
997
1346
  if (!this.document.cells[row.id]) {
998
1347
  this.document.cells[row.id] = {};
@@ -1009,16 +1358,17 @@ var SpreadsheetController = (_class4 = class {
1009
1358
  this.document.cells[row.id][col.id] = {
1010
1359
  ...cell,
1011
1360
  value: "#ERROR!",
1012
- error: _optionalChain([err, 'optionalAccess', _42 => _42.message]) || "Formula error"
1361
+ error: _optionalChain([err, 'optionalAccess', _44 => _44.message]) || "Formula error"
1013
1362
  };
1363
+ } finally {
1364
+ this.evaluatingRowIndex = null;
1014
1365
  }
1015
1366
  }
1016
1367
  resolveCellByCoord(colStr, rowNum) {
1017
1368
  const col = this.document.columns.find((c) => c.key.toUpperCase() === colStr.toUpperCase());
1018
1369
  const row = this.document.rows[rowNum - 1];
1019
1370
  if (!col || !row) return 0;
1020
- const cell = this.getCell(row.id, col.id);
1021
- return cell.value !== void 0 && cell.value !== "" ? cell.value : 0;
1371
+ return _nullishCoalesce(this.getCell(row.id, col.id).value, () => ( ""));
1022
1372
  }
1023
1373
  resolveRangeByCoords(sCol, sRow, eCol, eRow) {
1024
1374
  const startC = colNameToIndex(sCol);
@@ -1026,7 +1376,7 @@ var SpreadsheetController = (_class4 = class {
1026
1376
  const minC = Math.min(startC, endC);
1027
1377
  const maxC = Math.max(startC, endC);
1028
1378
  const minR = Math.min(sRow, eRow);
1029
- const maxR = Math.max(sRow, eRow);
1379
+ const maxR = Math.min(Math.max(sRow, eRow), WHOLE_COLUMN_END_ROW, this.document.rows.length);
1030
1380
  const results = [];
1031
1381
  for (let c = minC; c <= maxC; c++) {
1032
1382
  const colName = indexToColName(c);
@@ -1035,15 +1385,15 @@ var SpreadsheetController = (_class4 = class {
1035
1385
  for (let r = minR; r <= maxR; r++) {
1036
1386
  const row = this.document.rows[r - 1];
1037
1387
  if (!row) continue;
1038
- const cell = this.getCell(row.id, col.id);
1039
- results.push(cell.value !== void 0 && cell.value !== "" ? cell.value : 0);
1388
+ results.push(_nullishCoalesce(this.getCell(row.id, col.id).value, () => ( "")));
1040
1389
  }
1041
1390
  }
1042
1391
  return results;
1043
1392
  }
1044
1393
  resolveCurrentRowProperty(prop) {
1045
- if (!this.activeCell) return 0;
1046
- const row = this.document.rows[this.activeCell.row];
1394
+ const rowIndex = _nullishCoalesce(this.evaluatingRowIndex, () => ( _optionalChain([this, 'access', _45 => _45.activeCell, 'optionalAccess', _46 => _46.row])));
1395
+ if (rowIndex === void 0 || rowIndex === null) return 0;
1396
+ const row = this.document.rows[rowIndex];
1047
1397
  if (!row || !row.recordId || !this.primaryDataSourceId) return 0;
1048
1398
  return _nullishCoalesce(this.binder.resolveBoundValue(this.primaryDataSourceId, row.recordId, prop), () => ( 0));
1049
1399
  }
@@ -1056,8 +1406,8 @@ var SpreadsheetController = (_class4 = class {
1056
1406
  this.activeCell = {
1057
1407
  row: clampedRow,
1058
1408
  col: clampedCol,
1059
- rowId: _optionalChain([rowObj, 'optionalAccess', _43 => _43.id]),
1060
- colId: _optionalChain([colObj, 'optionalAccess', _44 => _44.id])
1409
+ rowId: _optionalChain([rowObj, 'optionalAccess', _47 => _47.id]),
1410
+ colId: _optionalChain([colObj, 'optionalAccess', _48 => _48.id])
1061
1411
  };
1062
1412
  if (extendSelection && this.selectedRange) {
1063
1413
  this.selectedRange = {
@@ -1074,15 +1424,15 @@ var SpreadsheetController = (_class4 = class {
1074
1424
  endCol: clampedCol
1075
1425
  };
1076
1426
  }
1077
- _optionalChain([this, 'access', _45 => _45.onSelectionChange, 'optionalCall', _46 => _46(this.activeCell, this.selectedRange)]);
1427
+ _optionalChain([this, 'access', _49 => _49.onSelectionChange, 'optionalCall', _50 => _50(this.activeCell, this.selectedRange)]);
1078
1428
  }
1079
1429
  selectRange(range) {
1080
1430
  this.selectedRange = range;
1081
- _optionalChain([this, 'access', _47 => _47.onSelectionChange, 'optionalCall', _48 => _48(this.activeCell, this.selectedRange)]);
1431
+ _optionalChain([this, 'access', _51 => _51.onSelectionChange, 'optionalCall', _52 => _52(this.activeCell, this.selectedRange)]);
1082
1432
  }
1083
1433
  startEditing(row, col, initialValue) {
1084
- const targetRow = _nullishCoalesce(_nullishCoalesce(row, () => ( _optionalChain([this, 'access', _49 => _49.activeCell, 'optionalAccess', _50 => _50.row]))), () => ( 0));
1085
- const targetCol = _nullishCoalesce(_nullishCoalesce(col, () => ( _optionalChain([this, 'access', _51 => _51.activeCell, 'optionalAccess', _52 => _52.col]))), () => ( 0));
1434
+ const targetRow = _nullishCoalesce(_nullishCoalesce(row, () => ( _optionalChain([this, 'access', _53 => _53.activeCell, 'optionalAccess', _54 => _54.row]))), () => ( 0));
1435
+ const targetCol = _nullishCoalesce(_nullishCoalesce(col, () => ( _optionalChain([this, 'access', _55 => _55.activeCell, 'optionalAccess', _56 => _56.col]))), () => ( 0));
1086
1436
  this.selectCell(targetRow, targetCol);
1087
1437
  const rowObj = this.document.rows[targetRow];
1088
1438
  const colObj = this.document.columns[targetCol];
@@ -1135,18 +1485,19 @@ var SpreadsheetController = (_class4 = class {
1135
1485
  // --- Grid Structure Mutations ---
1136
1486
  insertColumn(index, config) {
1137
1487
  this.recordSnapshot();
1138
- const newId = _optionalChain([config, 'optionalAccess', _53 => _53.id]) || `col_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
1488
+ const newId = _optionalChain([config, 'optionalAccess', _57 => _57.id]) || this.generateId("col");
1139
1489
  const newCol = {
1140
1490
  id: newId,
1141
1491
  key: indexToColName(index),
1142
- title: _optionalChain([config, 'optionalAccess', _54 => _54.title]) || indexToColName(index),
1143
- width: _optionalChain([config, 'optionalAccess', _55 => _55.width]) || 130,
1144
- type: _optionalChain([config, 'optionalAccess', _56 => _56.type]) || "freeform",
1145
- binding: _optionalChain([config, 'optionalAccess', _57 => _57.binding]),
1146
- formula: _optionalChain([config, 'optionalAccess', _58 => _58.formula]),
1147
- format: _optionalChain([config, 'optionalAccess', _59 => _59.format]) || "text",
1148
- readOnly: _optionalChain([config, 'optionalAccess', _60 => _60.readOnly])
1492
+ title: _optionalChain([config, 'optionalAccess', _58 => _58.title]) || indexToColName(index),
1493
+ width: _optionalChain([config, 'optionalAccess', _59 => _59.width]) || 130,
1494
+ type: _optionalChain([config, 'optionalAccess', _60 => _60.type]) || "freeform",
1495
+ binding: _optionalChain([config, 'optionalAccess', _61 => _61.binding]),
1496
+ formula: _optionalChain([config, 'optionalAccess', _62 => _62.formula]),
1497
+ format: _optionalChain([config, 'optionalAccess', _63 => _63.format]) || "text",
1498
+ readOnly: _optionalChain([config, 'optionalAccess', _64 => _64.readOnly])
1149
1499
  };
1500
+ this.shiftReferences({ axis: "col", index, type: "insert" });
1150
1501
  this.document.columns.splice(index, 0, newCol);
1151
1502
  this.recomputeRowIndices();
1152
1503
  this.recalculateAll();
@@ -1154,6 +1505,9 @@ var SpreadsheetController = (_class4 = class {
1154
1505
  }
1155
1506
  deleteColumn(colId) {
1156
1507
  this.recordSnapshot();
1508
+ const colIndex = this.document.columns.findIndex((c) => c.id === colId);
1509
+ if (colIndex === -1) return;
1510
+ this.shiftReferences({ axis: "col", index: colIndex, type: "delete" });
1157
1511
  this.document.columns = this.document.columns.filter((c) => c.id !== colId);
1158
1512
  for (const rowId of Object.keys(this.document.cells)) {
1159
1513
  delete this.document.cells[rowId][colId];
@@ -1164,14 +1518,15 @@ var SpreadsheetController = (_class4 = class {
1164
1518
  }
1165
1519
  insertRow(index, config) {
1166
1520
  this.recordSnapshot();
1167
- const newId = _optionalChain([config, 'optionalAccess', _61 => _61.id]) || `row_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
1521
+ const newId = _optionalChain([config, 'optionalAccess', _65 => _65.id]) || this.generateId("row");
1168
1522
  const newRow = {
1169
1523
  id: newId,
1170
1524
  index,
1171
- height: _optionalChain([config, 'optionalAccess', _62 => _62.height]) || 32,
1172
- recordId: _optionalChain([config, 'optionalAccess', _63 => _63.recordId]),
1173
- type: _optionalChain([config, 'optionalAccess', _64 => _64.type]) || "freeform"
1525
+ height: _optionalChain([config, 'optionalAccess', _66 => _66.height]) || 32,
1526
+ recordId: _optionalChain([config, 'optionalAccess', _67 => _67.recordId]),
1527
+ type: _optionalChain([config, 'optionalAccess', _68 => _68.type]) || "freeform"
1174
1528
  };
1529
+ this.shiftReferences({ axis: "row", index, type: "insert" });
1175
1530
  this.document.rows.splice(index, 0, newRow);
1176
1531
  this.recomputeRowIndices();
1177
1532
  this.recalculateAll();
@@ -1179,12 +1534,38 @@ var SpreadsheetController = (_class4 = class {
1179
1534
  }
1180
1535
  deleteRow(rowId) {
1181
1536
  this.recordSnapshot();
1537
+ const rowIndex = this.document.rows.findIndex((r) => r.id === rowId);
1538
+ if (rowIndex === -1) return;
1539
+ this.shiftReferences({ axis: "row", index: rowIndex, type: "delete" });
1182
1540
  this.document.rows = this.document.rows.filter((r) => r.id !== rowId);
1183
1541
  delete this.document.cells[rowId];
1184
1542
  this.recomputeRowIndices();
1185
1543
  this.recalculateAll();
1186
1544
  this.notifyChange();
1187
1545
  }
1546
+ generateId(prefix) {
1547
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
1548
+ }
1549
+ /**
1550
+ * Rewrites references in every cell formula (and, for column changes, every
1551
+ * column formula) so they keep pointing at the same cells.
1552
+ */
1553
+ shiftReferences(change) {
1554
+ for (const rowCells of Object.values(this.document.cells)) {
1555
+ for (const cell of Object.values(rowCells)) {
1556
+ if (_optionalChain([cell, 'access', _69 => _69.raw, 'optionalAccess', _70 => _70.startsWith, 'call', _71 => _71("=")])) {
1557
+ cell.raw = shiftFormulaReferences(cell.raw, change);
1558
+ }
1559
+ }
1560
+ }
1561
+ if (change.axis === "col") {
1562
+ for (const col of this.document.columns) {
1563
+ if (_optionalChain([col, 'access', _72 => _72.formula, 'optionalAccess', _73 => _73.startsWith, 'call', _74 => _74("=")])) {
1564
+ col.formula = shiftFormulaReferences(col.formula, change);
1565
+ }
1566
+ }
1567
+ }
1568
+ }
1188
1569
  setColumnWidth(colId, width) {
1189
1570
  const col = this.document.columns.find((c) => c.id === colId);
1190
1571
  if (col) {
@@ -1211,39 +1592,74 @@ var SpreadsheetController = (_class4 = class {
1211
1592
  this.setColumnWidth(colId, estimatedWidth);
1212
1593
  }
1213
1594
  // --- Clipboard TSV/CSV ---
1214
- exportToTsv() {
1215
- return this.document.rows.map(
1216
- (row) => this.document.columns.map((col) => {
1595
+ /**
1596
+ * Exports cell values as TSV: the whole sheet, or only `range` (e.g. the
1597
+ * current selection) when given.
1598
+ */
1599
+ exportToTsv(range) {
1600
+ const rows = range ? this.document.rows.slice(
1601
+ Math.min(range.startRow, range.endRow),
1602
+ Math.max(range.startRow, range.endRow) + 1
1603
+ ) : this.document.rows;
1604
+ const columns = range ? this.document.columns.slice(
1605
+ Math.min(range.startCol, range.endCol),
1606
+ Math.max(range.startCol, range.endCol) + 1
1607
+ ) : this.document.columns;
1608
+ return rows.map(
1609
+ (row) => columns.map((col) => {
1217
1610
  const cell = this.getCell(row.id, col.id);
1218
1611
  return String(_nullishCoalesce(cell.value, () => ( ""))).replace(/\t/g, " ");
1219
1612
  }).join(" ")
1220
1613
  ).join("\n");
1221
1614
  }
1615
+ /**
1616
+ * Pastes tab-separated values starting at (startRow, startCol), adding rows
1617
+ * and columns as needed. The whole paste is a single undo step.
1618
+ */
1222
1619
  importFromTsv(tsvData, startRow = 0, startCol = 0) {
1620
+ const normalized = tsvData.replace(/\r\n?/g, "\n").replace(/\n$/, "");
1621
+ if (normalized === "") return;
1223
1622
  this.recordSnapshot();
1224
- const lines = tsvData.trim().split("\n");
1225
- lines.forEach((line, rIdx) => {
1226
- const cells = line.split(" ");
1227
- const targetR = startRow + rIdx;
1228
- while (targetR >= this.document.rows.length) {
1229
- this.insertRow(this.document.rows.length);
1230
- }
1231
- const row = this.document.rows[targetR];
1232
- cells.forEach((val, cIdx) => {
1233
- const targetC = startCol + cIdx;
1234
- while (targetC >= this.document.columns.length) {
1235
- this.insertColumn(this.document.columns.length);
1623
+ this.batching = true;
1624
+ this.pendingCommits = [];
1625
+ try {
1626
+ normalized.split("\n").forEach((line, rIdx) => {
1627
+ const targetR = startRow + rIdx;
1628
+ while (targetR >= this.document.rows.length) {
1629
+ const index = this.document.rows.length;
1630
+ this.document.rows.push({ id: this.generateId("row"), index, height: 32, type: "freeform" });
1236
1631
  }
1237
- const col = this.document.columns[targetC];
1238
- this.setCellValue(row.id, col.id, val);
1632
+ const row = this.document.rows[targetR];
1633
+ line.split(" ").forEach((val, cIdx) => {
1634
+ const targetC = startCol + cIdx;
1635
+ while (targetC >= this.document.columns.length) {
1636
+ const key = indexToColName(this.document.columns.length);
1637
+ this.document.columns.push({
1638
+ id: this.generateId("col"),
1639
+ key,
1640
+ title: key,
1641
+ width: 130,
1642
+ type: "freeform",
1643
+ format: "text"
1644
+ });
1645
+ }
1646
+ this.setCellValue(row.id, this.document.columns[targetC].id, val);
1647
+ });
1239
1648
  });
1240
- });
1649
+ } finally {
1650
+ this.batching = false;
1651
+ }
1241
1652
  this.recalculateAll();
1242
1653
  this.notifyChange();
1654
+ const commits = this.pendingCommits;
1655
+ this.pendingCommits = [];
1656
+ for (const [rowId, colId, raw] of commits) {
1657
+ _optionalChain([this, 'access', _75 => _75.onCellCommit, 'optionalCall', _76 => _76(rowId, colId, raw, _optionalChain([this, 'access', _77 => _77.document, 'access', _78 => _78.cells, 'access', _79 => _79[rowId], 'optionalAccess', _80 => _80[colId], 'optionalAccess', _81 => _81.value]))]);
1658
+ }
1243
1659
  }
1244
1660
  // --- Undo/Redo Snapshots ---
1245
1661
  recordSnapshot() {
1246
- if (this.isApplyingHistory) return;
1662
+ if (this.isApplyingHistory || this.batching) return;
1247
1663
  this.undoStack.push(JSON.stringify(this.document));
1248
1664
  if (this.undoStack.length > 50) this.undoStack.shift();
1249
1665
  this.redoStack = [];
@@ -1251,25 +1667,29 @@ var SpreadsheetController = (_class4 = class {
1251
1667
  undo() {
1252
1668
  if (this.undoStack.length === 0) return;
1253
1669
  this.isApplyingHistory = true;
1254
- this.redoStack.push(JSON.stringify(this.document));
1255
- const previous = JSON.parse(this.undoStack.pop());
1256
- this.document = previous;
1257
- this.recalculateAll();
1258
- this.notifyChange();
1259
- this.isApplyingHistory = false;
1670
+ try {
1671
+ this.redoStack.push(JSON.stringify(this.document));
1672
+ this.document = JSON.parse(this.undoStack.pop());
1673
+ this.recalculateAll();
1674
+ this.notifyChange();
1675
+ } finally {
1676
+ this.isApplyingHistory = false;
1677
+ }
1260
1678
  }
1261
1679
  redo() {
1262
1680
  if (this.redoStack.length === 0) return;
1263
1681
  this.isApplyingHistory = true;
1264
- this.undoStack.push(JSON.stringify(this.document));
1265
- const next = JSON.parse(this.redoStack.pop());
1266
- this.document = next;
1267
- this.recalculateAll();
1268
- this.notifyChange();
1269
- this.isApplyingHistory = false;
1682
+ try {
1683
+ this.undoStack.push(JSON.stringify(this.document));
1684
+ this.document = JSON.parse(this.redoStack.pop());
1685
+ this.recalculateAll();
1686
+ this.notifyChange();
1687
+ } finally {
1688
+ this.isApplyingHistory = false;
1689
+ }
1270
1690
  }
1271
1691
  notifyChange() {
1272
- _optionalChain([this, 'access', _65 => _65.onDocumentChange, 'optionalCall', _66 => _66(this.document)]);
1692
+ _optionalChain([this, 'access', _82 => _82.onDocumentChange, 'optionalCall', _83 => _83(this.document)]);
1273
1693
  }
1274
1694
  }, _class4);
1275
1695
 
@@ -1286,5 +1706,10 @@ var SpreadsheetController = (_class4 = class {
1286
1706
 
1287
1707
 
1288
1708
 
1289
- exports.tokenizeFormula = tokenizeFormula; exports.parseCellReference = parseCellReference; exports.parseRange = parseRange; exports.FormulaParser = FormulaParser; exports.parseFormula = parseFormula; exports.colNameToIndex = colNameToIndex; exports.indexToColName = indexToColName; exports.FormulaEvaluator = FormulaEvaluator; exports.extractCellDependencies = extractCellDependencies; exports.DependencyGraph = DependencyGraph; exports.DataSourceBinder = DataSourceBinder; exports.SpreadsheetController = SpreadsheetController;
1290
- //# sourceMappingURL=chunk-CYKQLDYH.cjs.map
1709
+
1710
+
1711
+
1712
+
1713
+
1714
+ exports.tokenizeFormula = tokenizeFormula; exports.WHOLE_COLUMN_END_ROW = WHOLE_COLUMN_END_ROW; exports.parseCellReference = parseCellReference; exports.parseRange = parseRange; exports.FormulaParser = FormulaParser; exports.parseFormula = parseFormula; exports.FORMULA_ERRORS = FORMULA_ERRORS; exports.isFormulaError = isFormulaError; exports.colNameToIndex = colNameToIndex; exports.indexToColName = indexToColName; exports.FormulaEvaluator = FormulaEvaluator; exports.columnDependencyKey = columnDependencyKey; exports.extractCellDependencies = extractCellDependencies; exports.DependencyGraph = DependencyGraph; exports.shiftFormulaReferences = shiftFormulaReferences; exports.DataSourceBinder = DataSourceBinder; exports.SpreadsheetController = SpreadsheetController;
1715
+ //# sourceMappingURL=chunk-YQCI4CCN.cjs.map