@odoo/o-spreadsheet 16.4.15 → 16.4.17

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.
@@ -297,13 +297,6 @@ const NEWLINE = "\n";
297
297
  const FONT_SIZES = [6, 7, 8, 9, 10, 11, 12, 14, 18, 24, 36];
298
298
 
299
299
  //------------------------------------------------------------------------------
300
- /**
301
- * Stringify an object, like JSON.stringify, except that the first level of keys
302
- * is ordered.
303
- */
304
- function stringify(obj) {
305
- return JSON.stringify(obj, Object.keys(obj).sort());
306
- }
307
300
  /**
308
301
  * Remove quotes from a quoted string
309
302
  * ```js
@@ -523,7 +516,7 @@ function isObjectEmptyRecursive(argument) {
523
516
  */
524
517
  function getItemId(item, itemsDic) {
525
518
  for (let [key, value] of Object.entries(itemsDic)) {
526
- if (stringify(value) === stringify(item)) {
519
+ if (deepEquals(value, item)) {
527
520
  return parseInt(key, 10);
528
521
  }
529
522
  }
@@ -2341,8 +2334,10 @@ function convertInternalFormatToFormat(internalFormat) {
2341
2334
  const cellReference = new RegExp(/\$?([A-Z]{1,3})\$?([0-9]{1,7})/, "i");
2342
2335
  // Same as above, but matches the exact string (nothing before or after)
2343
2336
  const singleCellReference = new RegExp(/^\$?([A-Z]{1,3})\$?([0-9]{1,7})$/, "i");
2344
- /** Reference of a column header (eg. A, AB) */
2345
- const colHeader = new RegExp(/^([A-Z]{1,3})+$/, "i");
2337
+ /** Reference of a column header (eg. A, AB, $A) */
2338
+ const colHeader = new RegExp(/^\$?([A-Z]{1,3})+$/, "i");
2339
+ /** Reference of a row header (eg. 1, $1) */
2340
+ const rowHeader = new RegExp(/^\$?([0-9]{1,7})+$/, "i");
2346
2341
  /** Reference of a column (eg. A, $CA, Sheet1!B) */
2347
2342
  const colReference = new RegExp(/^\s*('.+'!|[^']+!)?\$?([A-Z]{1,3})$/, "i");
2348
2343
  /** Reference of a row (eg. 1, 59, Sheet1!9) */
@@ -2372,6 +2367,9 @@ function isRowReference(xc) {
2372
2367
  function isColHeader(str) {
2373
2368
  return colHeader.test(str);
2374
2369
  }
2370
+ function isRowHeader(str) {
2371
+ return rowHeader.test(str);
2372
+ }
2375
2373
  /**
2376
2374
  * Return true if the given xc is the reference of a single cell,
2377
2375
  * without any specified sheet (e.g. A1)
@@ -3044,18 +3042,10 @@ class RangeImpl {
3044
3042
  if (isFullCol) {
3045
3043
  parts[0].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
3046
3044
  parts[1].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
3047
- if (zone.left === zone.right) {
3048
- parts[0].colFixed = parts[0].colFixed || parts[1].colFixed;
3049
- parts[1].colFixed = parts[0].colFixed || parts[1].colFixed;
3050
- }
3051
3045
  }
3052
3046
  if (isFullRow) {
3053
3047
  parts[0].colFixed = parts[0].colFixed || parts[1].colFixed;
3054
3048
  parts[1].colFixed = parts[0].colFixed || parts[1].colFixed;
3055
- if (zone.top === zone.bottom) {
3056
- parts[0].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
3057
- parts[1].rowFixed = parts[0].rowFixed || parts[1].rowFixed;
3058
- }
3059
3049
  }
3060
3050
  return parts;
3061
3051
  }
@@ -5607,6 +5597,7 @@ const FilterMenuPopoverBuilder = {
5607
5597
  },
5608
5598
  };
5609
5599
 
5600
+ const macRegex = /Mac/i;
5610
5601
  /**
5611
5602
  * Return true if the event was triggered from
5612
5603
  * a child element.
@@ -5644,6 +5635,17 @@ function* iterateChildren(el) {
5644
5635
  }
5645
5636
  function getOpenedMenus() {
5646
5637
  return Array.from(document.querySelectorAll(".o-spreadsheet .o-menu"));
5638
+ }
5639
+ function isMacOS() {
5640
+ return Boolean(macRegex.test(navigator.userAgent));
5641
+ }
5642
+ /**
5643
+ * @param {KeyboardEvent | MouseEvent} ev
5644
+ * @returns Returns true if the event was triggered with the "ctrl" modifier pressed.
5645
+ * On Mac, this is the "meta" or "command" key.
5646
+ */
5647
+ function isCtrlKey(ev) {
5648
+ return isMacOS() ? ev.metaKey : ev.ctrlKey;
5647
5649
  }
5648
5650
 
5649
5651
  /**
@@ -14700,7 +14702,7 @@ const machine = {
14700
14702
  SPACE: goTo(State.RightRef),
14701
14703
  NUMBER: goTo(State.Found),
14702
14704
  REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
14703
- SYMBOL: goTo(State.Found, (token) => isColHeader(token.value)),
14705
+ SYMBOL: goTo(State.Found, (token) => isColHeader(token.value) || isRowHeader(token.value)),
14704
14706
  },
14705
14707
  [State.RightColumnRef]: {
14706
14708
  SPACE: goTo(State.RightColumnRef),
@@ -14711,6 +14713,7 @@ const machine = {
14711
14713
  SPACE: goTo(State.RightRowRef),
14712
14714
  NUMBER: goTo(State.Found),
14713
14715
  REFERENCE: goTo(State.Found, (token) => isSingleCellReference(token.value)),
14716
+ SYMBOL: goTo(State.Found, (token) => isRowHeader(token.value)),
14714
14717
  },
14715
14718
  [State.Found]: {},
14716
14719
  };
@@ -16868,7 +16871,7 @@ function createGaugeChartRuntime(chart, getters) {
16868
16871
  });
16869
16872
  return {
16870
16873
  chartJsConfig: config,
16871
- background: getters.getBackgroundOfSingleCellChart(chart.background, dataRange),
16874
+ background: getters.getStyleOfSingleCellChart(chart.background, dataRange).background,
16872
16875
  };
16873
16876
  }
16874
16877
 
@@ -17592,7 +17595,7 @@ function createScorecardChartRuntime(chart, getters) {
17592
17595
  };
17593
17596
  baselineCell = getters.getEvaluatedCell(baselinePosition);
17594
17597
  }
17595
- const background = getters.getBackgroundOfSingleCellChart(chart.background, chart.keyValue);
17598
+ const { background, fontColor } = getters.getStyleOfSingleCellChart(chart.background, chart.keyValue);
17596
17599
  const locale = getters.getLocale();
17597
17600
  return {
17598
17601
  title: _t(chart.title),
@@ -17601,7 +17604,7 @@ function createScorecardChartRuntime(chart, getters) {
17601
17604
  baselineArrow: getBaselineArrowDirection(baselineCell, keyValueCell, chart.baselineMode),
17602
17605
  baselineColor: getBaselineColor(baselineCell, chart.baselineMode, keyValueCell, chart.baselineColorUp, chart.baselineColorDown),
17603
17606
  baselineDescr: chart.baselineDescr ? _t(chart.baselineDescr) : "",
17604
- fontColor: chartFontColor(background),
17607
+ fontColor,
17605
17608
  background,
17606
17609
  baselineStyle: chart.baselineMode !== "percentage" && baseline
17607
17610
  ? getters.getCellStyle({
@@ -20496,10 +20499,10 @@ const arrowMap = {
20496
20499
  function updateSelectionWithArrowKeys(ev, selection) {
20497
20500
  const direction = arrowMap[ev.key];
20498
20501
  if (ev.shiftKey) {
20499
- selection.resizeAnchorZone(direction, ev.ctrlKey ? "end" : 1);
20502
+ selection.resizeAnchorZone(direction, isCtrlKey(ev) ? "end" : 1);
20500
20503
  }
20501
20504
  else {
20502
- selection.moveAnchorCell(direction, ev.ctrlKey ? "end" : 1);
20505
+ selection.moveAnchorCell(direction, isCtrlKey(ev) ? "end" : 1);
20503
20506
  }
20504
20507
  }
20505
20508
 
@@ -25544,7 +25547,10 @@ class GridOverlay extends Component {
25544
25547
  return;
25545
25548
  }
25546
25549
  const [col, row] = this.getCartesianCoordinates(ev);
25547
- this.props.onCellClicked(col, row, { shiftKey: ev.shiftKey, ctrlKey: ev.ctrlKey });
25550
+ this.props.onCellClicked(col, row, {
25551
+ expandZone: ev.shiftKey,
25552
+ addZone: isCtrlKey(ev),
25553
+ });
25548
25554
  }
25549
25555
  onDoubleClick(ev) {
25550
25556
  const [col, row] = this.getCartesianCoordinates(ev);
@@ -25617,6 +25623,7 @@ class AbstractResizer extends Component {
25617
25623
  draggerShadowThickness: 0,
25618
25624
  delta: 0,
25619
25625
  base: 0,
25626
+ position: "before",
25620
25627
  });
25621
25628
  _computeHandleDisplay(ev) {
25622
25629
  const position = this._getEvOffset(ev);
@@ -25741,11 +25748,13 @@ class AbstractResizer extends Component {
25741
25748
  this.state.draggerLinePosition = dimensions.start;
25742
25749
  this.state.draggerShadowPosition = dimensions.start;
25743
25750
  this.state.base = elementIndex;
25751
+ this.state.position = "before";
25744
25752
  }
25745
25753
  else if (this._getSelectedZoneEnd() < elementIndex) {
25746
25754
  this.state.draggerLinePosition = dimensions.end;
25747
25755
  this.state.draggerShadowPosition = dimensions.end - this.state.draggerShadowThickness;
25748
- this.state.base = elementIndex + 1;
25756
+ this.state.base = elementIndex;
25757
+ this.state.position = "after";
25749
25758
  }
25750
25759
  else {
25751
25760
  this.state.draggerLinePosition = startDimensions.start;
@@ -25769,7 +25778,7 @@ class AbstractResizer extends Component {
25769
25778
  this._increaseSelection(index);
25770
25779
  }
25771
25780
  else {
25772
- this._selectElement(index, ev.ctrlKey);
25781
+ this._selectElement(index, isCtrlKey(ev));
25773
25782
  }
25774
25783
  this.lastSelectedElementIndex = index;
25775
25784
  const mouseMoveSelect = (col, row) => {
@@ -25782,7 +25791,7 @@ class AbstractResizer extends Component {
25782
25791
  const mouseUpSelect = () => {
25783
25792
  this.state.isSelecting = false;
25784
25793
  this.lastSelectedElementIndex = null;
25785
- this.env.model.dispatch(ev.ctrlKey ? "PREPARE_SELECTION_INPUT_EXPANSION" : "STOP_SELECTION_INPUT");
25794
+ this.env.model.dispatch(isCtrlKey(ev) ? "PREPARE_SELECTION_INPUT_EXPANSION" : "STOP_SELECTION_INPUT");
25786
25795
  this._computeGrabDisplay(ev);
25787
25796
  };
25788
25797
  dragAndDropBeyondTheViewport(this.env, mouseMoveSelect, mouseUpSelect);
@@ -25928,13 +25937,14 @@ class ColResizer extends AbstractResizer {
25928
25937
  dimension: "COL",
25929
25938
  base: this.state.base,
25930
25939
  elements,
25940
+ position: this.state.position,
25931
25941
  });
25932
25942
  if (!result.isSuccessful && result.reasons.includes(2 /* CommandResult.WillRemoveExistingMerge */)) {
25933
25943
  this.env.raiseError(MergeErrorMessage);
25934
25944
  }
25935
25945
  }
25936
- _selectElement(index, ctrlKey) {
25937
- this.env.model.selection.selectColumn(index, ctrlKey ? "newAnchor" : "overrideSelection");
25946
+ _selectElement(index, addDistinctHeader) {
25947
+ this.env.model.selection.selectColumn(index, addDistinctHeader ? "newAnchor" : "overrideSelection");
25938
25948
  }
25939
25949
  _increaseSelection(index) {
25940
25950
  this.env.model.selection.selectColumn(index, "updateAnchor");
@@ -26101,13 +26111,14 @@ class RowResizer extends AbstractResizer {
26101
26111
  dimension: "ROW",
26102
26112
  base: this.state.base,
26103
26113
  elements,
26114
+ position: this.state.position,
26104
26115
  });
26105
26116
  if (!result.isSuccessful && result.reasons.includes(2 /* CommandResult.WillRemoveExistingMerge */)) {
26106
26117
  this.env.raiseError(MergeErrorMessage);
26107
26118
  }
26108
26119
  }
26109
- _selectElement(index, ctrlKey) {
26110
- this.env.model.selection.selectRow(index, ctrlKey ? "newAnchor" : "overrideSelection");
26120
+ _selectElement(index, addDistinctHeader) {
26121
+ this.env.model.selection.selectRow(index, addDistinctHeader ? "newAnchor" : "overrideSelection");
26111
26122
  }
26112
26123
  _increaseSelection(index) {
26113
26124
  this.env.model.selection.selectRow(index, "updateAnchor");
@@ -26208,8 +26219,8 @@ function useWheelHandler(handler) {
26208
26219
  return val * (deltaMode === 0 ? 1 : DEFAULT_CELL_HEIGHT);
26209
26220
  }
26210
26221
  const onMouseWheel = (ev) => {
26211
- const deltaX = normalize(ev.shiftKey ? ev.deltaY : ev.deltaX, ev.deltaMode);
26212
- const deltaY = normalize(ev.shiftKey ? ev.deltaX : ev.deltaY, ev.deltaMode);
26222
+ const deltaX = normalize(ev.shiftKey && !isMacOS() ? ev.deltaY : ev.deltaX, ev.deltaMode);
26223
+ const deltaY = normalize(ev.shiftKey && !isMacOS() ? ev.deltaX : ev.deltaY, ev.deltaMode);
26213
26224
  handler(deltaX, deltaY);
26214
26225
  };
26215
26226
  return onMouseWheel;
@@ -26913,8 +26924,8 @@ class Grid extends Component {
26913
26924
  // ---------------------------------------------------------------------------
26914
26925
  // Zone selection with mouse
26915
26926
  // ---------------------------------------------------------------------------
26916
- onCellClicked(col, row, { ctrlKey, shiftKey }) {
26917
- if (ctrlKey) {
26927
+ onCellClicked(col, row, { addZone, expandZone }) {
26928
+ if (addZone) {
26918
26929
  this.env.model.dispatch("PREPARE_SELECTION_INPUT_EXPANSION");
26919
26930
  }
26920
26931
  if (this.env.model.getters.hasOpenedPopover()) {
@@ -26923,10 +26934,10 @@ class Grid extends Component {
26923
26934
  if (this.env.model.getters.getEditionMode() === "editing") {
26924
26935
  this.env.model.dispatch("STOP_EDITION");
26925
26936
  }
26926
- if (shiftKey) {
26937
+ if (expandZone) {
26927
26938
  this.env.model.selection.setAnchorCorner(col, row);
26928
26939
  }
26929
- else if (ctrlKey) {
26940
+ else if (addZone) {
26930
26941
  this.env.model.selection.addCellToSelection(col, row);
26931
26942
  }
26932
26943
  else {
@@ -26988,9 +26999,7 @@ class Grid extends Component {
26988
26999
  return;
26989
27000
  }
26990
27001
  let keyDownString = "";
26991
- if (ev.ctrlKey)
26992
- keyDownString += "CTRL+";
26993
- if (ev.metaKey)
27002
+ if (isCtrlKey(ev))
26994
27003
  keyDownString += "CTRL+";
26995
27004
  if (ev.altKey)
26996
27005
  keyDownString += "ALT+";
@@ -31638,7 +31647,7 @@ class BordersPlugin extends CorePlugin {
31638
31647
  */
31639
31648
  function getBorderId(border) {
31640
31649
  for (let [key, value] of Object.entries(borders)) {
31641
- if (stringify(value) === stringify(border)) {
31650
+ if (deepEquals(value, border)) {
31642
31651
  return parseInt(key, 10);
31643
31652
  }
31644
31653
  }
@@ -34547,7 +34556,7 @@ class SheetPlugin extends CorePlugin {
34547
34556
  const elements = cmd.dimension === "COL"
34548
34557
  ? this.getNumberCols(cmd.sheetId)
34549
34558
  : this.getNumberRows(cmd.sheetId);
34550
- if (cmd.base < 0 || cmd.base > elements) {
34559
+ if (cmd.base < 0 || cmd.base >= elements) {
34551
34560
  return 88 /* CommandResult.InvalidHeaderIndex */;
34552
34561
  }
34553
34562
  else if (cmd.quantity <= 0) {
@@ -35488,10 +35497,13 @@ class CompilationParametersBuilder {
35488
35497
  if (evaluatedCell === undefined) {
35489
35498
  return { value: null, format: this.getters.getCell(position)?.format };
35490
35499
  }
35500
+ if (evaluatedCell.type === CellValueType.error) {
35501
+ throw evaluatedCell.error;
35502
+ }
35491
35503
  return evaluatedCell;
35492
35504
  }
35493
35505
  getEvaluatedCellIfNotEmpty(position) {
35494
- const evaluatedCell = this.getEvaluatedCell(position);
35506
+ const evaluatedCell = this.computeCell(position);
35495
35507
  if (evaluatedCell.type === CellValueType.empty) {
35496
35508
  const cell = this.getters.getCell(position);
35497
35509
  if (!cell || (!cell.isFormula && cell.content === "")) {
@@ -35500,13 +35512,6 @@ class CompilationParametersBuilder {
35500
35512
  }
35501
35513
  return evaluatedCell;
35502
35514
  }
35503
- getEvaluatedCell(position) {
35504
- const evaluatedCell = this.computeCell(position);
35505
- if (evaluatedCell.type === CellValueType.error) {
35506
- throw evaluatedCell.error;
35507
- }
35508
- return evaluatedCell;
35509
- }
35510
35515
  /**
35511
35516
  * Return the values of the cell(s) used in reference, but always in the format of a range even
35512
35517
  * if a single cell is referenced. It is a list of col values. This is useful for the formulas that describe parameters as
@@ -35529,7 +35534,11 @@ class CompilationParametersBuilder {
35529
35534
  const { top, left, bottom, right } = zone;
35530
35535
  const cacheKey = `${sheetId}-${top}-${left}-${bottom}-${right}`;
35531
35536
  if (cacheKey in this.rangeCache) {
35532
- return this.rangeCache[cacheKey];
35537
+ const result = this.rangeCache[cacheKey];
35538
+ if (result instanceof EvaluationError) {
35539
+ throw result;
35540
+ }
35541
+ return result;
35533
35542
  }
35534
35543
  const height = _zone.bottom - _zone.top + 1;
35535
35544
  const width = _zone.right - _zone.left + 1;
@@ -35542,6 +35551,10 @@ class CompilationParametersBuilder {
35542
35551
  format[colIndex] = new Array(height);
35543
35552
  for (let row = _zone.top; row <= _zone.bottom; row++) {
35544
35553
  const evaluatedCell = this.getEvaluatedCellIfNotEmpty({ sheetId, col, row });
35554
+ if (evaluatedCell?.type === CellValueType.error) {
35555
+ this.rangeCache[cacheKey] = evaluatedCell.error;
35556
+ throw evaluatedCell.error;
35557
+ }
35545
35558
  const rowIndex = row - _zone.top;
35546
35559
  value[colIndex][rowIndex] = evaluatedCell?.value;
35547
35560
  if (evaluatedCell?.format !== undefined) {
@@ -35555,56 +35568,758 @@ class CompilationParametersBuilder {
35555
35568
  }
35556
35569
  }
35557
35570
 
35571
+ function quickselect(arr, k, left, right, compare) {
35572
+ quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare);
35573
+ }
35574
+
35575
+ function quickselectStep(arr, k, left, right, compare) {
35576
+
35577
+ while (right > left) {
35578
+ if (right - left > 600) {
35579
+ var n = right - left + 1;
35580
+ var m = k - left + 1;
35581
+ var z = Math.log(n);
35582
+ var s = 0.5 * Math.exp(2 * z / 3);
35583
+ var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
35584
+ var newLeft = Math.max(left, Math.floor(k - m * s / n + sd));
35585
+ var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));
35586
+ quickselectStep(arr, k, newLeft, newRight, compare);
35587
+ }
35588
+
35589
+ var t = arr[k];
35590
+ var i = left;
35591
+ var j = right;
35592
+
35593
+ swap(arr, left, k);
35594
+ if (compare(arr[right], t) > 0) swap(arr, left, right);
35595
+
35596
+ while (i < j) {
35597
+ swap(arr, i, j);
35598
+ i++;
35599
+ j--;
35600
+ while (compare(arr[i], t) < 0) i++;
35601
+ while (compare(arr[j], t) > 0) j--;
35602
+ }
35603
+
35604
+ if (compare(arr[left], t) === 0) swap(arr, left, j);
35605
+ else {
35606
+ j++;
35607
+ swap(arr, j, right);
35608
+ }
35609
+
35610
+ if (j <= k) left = j + 1;
35611
+ if (k <= j) right = j - 1;
35612
+ }
35613
+ }
35614
+
35615
+ function swap(arr, i, j) {
35616
+ var tmp = arr[i];
35617
+ arr[i] = arr[j];
35618
+ arr[j] = tmp;
35619
+ }
35620
+
35621
+ function defaultCompare(a, b) {
35622
+ return a < b ? -1 : a > b ? 1 : 0;
35623
+ }
35624
+
35625
+ class RBush {
35626
+ constructor(maxEntries = 9) {
35627
+ // max entries in a node is 9 by default; min node fill is 40% for best performance
35628
+ this._maxEntries = Math.max(4, maxEntries);
35629
+ this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));
35630
+ this.clear();
35631
+ }
35632
+
35633
+ all() {
35634
+ return this._all(this.data, []);
35635
+ }
35636
+
35637
+ search(bbox) {
35638
+ let node = this.data;
35639
+ const result = [];
35640
+
35641
+ if (!intersects(bbox, node)) return result;
35642
+
35643
+ const toBBox = this.toBBox;
35644
+ const nodesToSearch = [];
35645
+
35646
+ while (node) {
35647
+ for (let i = 0; i < node.children.length; i++) {
35648
+ const child = node.children[i];
35649
+ const childBBox = node.leaf ? toBBox(child) : child;
35650
+
35651
+ if (intersects(bbox, childBBox)) {
35652
+ if (node.leaf) result.push(child);
35653
+ else if (contains(bbox, childBBox)) this._all(child, result);
35654
+ else nodesToSearch.push(child);
35655
+ }
35656
+ }
35657
+ node = nodesToSearch.pop();
35658
+ }
35659
+
35660
+ return result;
35661
+ }
35662
+
35663
+ collides(bbox) {
35664
+ let node = this.data;
35665
+
35666
+ if (!intersects(bbox, node)) return false;
35667
+
35668
+ const nodesToSearch = [];
35669
+ while (node) {
35670
+ for (let i = 0; i < node.children.length; i++) {
35671
+ const child = node.children[i];
35672
+ const childBBox = node.leaf ? this.toBBox(child) : child;
35673
+
35674
+ if (intersects(bbox, childBBox)) {
35675
+ if (node.leaf || contains(bbox, childBBox)) return true;
35676
+ nodesToSearch.push(child);
35677
+ }
35678
+ }
35679
+ node = nodesToSearch.pop();
35680
+ }
35681
+
35682
+ return false;
35683
+ }
35684
+
35685
+ load(data) {
35686
+ if (!(data && data.length)) return this;
35687
+
35688
+ if (data.length < this._minEntries) {
35689
+ for (let i = 0; i < data.length; i++) {
35690
+ this.insert(data[i]);
35691
+ }
35692
+ return this;
35693
+ }
35694
+
35695
+ // recursively build the tree with the given data from scratch using OMT algorithm
35696
+ let node = this._build(data.slice(), 0, data.length - 1, 0);
35697
+
35698
+ if (!this.data.children.length) {
35699
+ // save as is if tree is empty
35700
+ this.data = node;
35701
+
35702
+ } else if (this.data.height === node.height) {
35703
+ // split root if trees have the same height
35704
+ this._splitRoot(this.data, node);
35705
+
35706
+ } else {
35707
+ if (this.data.height < node.height) {
35708
+ // swap trees if inserted one is bigger
35709
+ const tmpNode = this.data;
35710
+ this.data = node;
35711
+ node = tmpNode;
35712
+ }
35713
+
35714
+ // insert the small tree into the large tree at appropriate level
35715
+ this._insert(node, this.data.height - node.height - 1, true);
35716
+ }
35717
+
35718
+ return this;
35719
+ }
35720
+
35721
+ insert(item) {
35722
+ if (item) this._insert(item, this.data.height - 1);
35723
+ return this;
35724
+ }
35725
+
35726
+ clear() {
35727
+ this.data = createNode([]);
35728
+ return this;
35729
+ }
35730
+
35731
+ remove(item, equalsFn) {
35732
+ if (!item) return this;
35733
+
35734
+ let node = this.data;
35735
+ const bbox = this.toBBox(item);
35736
+ const path = [];
35737
+ const indexes = [];
35738
+ let i, parent, goingUp;
35739
+
35740
+ // depth-first iterative tree traversal
35741
+ while (node || path.length) {
35742
+
35743
+ if (!node) { // go up
35744
+ node = path.pop();
35745
+ parent = path[path.length - 1];
35746
+ i = indexes.pop();
35747
+ goingUp = true;
35748
+ }
35749
+
35750
+ if (node.leaf) { // check current node
35751
+ const index = findItem(item, node.children, equalsFn);
35752
+
35753
+ if (index !== -1) {
35754
+ // item found, remove the item and condense tree upwards
35755
+ node.children.splice(index, 1);
35756
+ path.push(node);
35757
+ this._condense(path);
35758
+ return this;
35759
+ }
35760
+ }
35761
+
35762
+ if (!goingUp && !node.leaf && contains(node, bbox)) { // go down
35763
+ path.push(node);
35764
+ indexes.push(i);
35765
+ i = 0;
35766
+ parent = node;
35767
+ node = node.children[0];
35768
+
35769
+ } else if (parent) { // go right
35770
+ i++;
35771
+ node = parent.children[i];
35772
+ goingUp = false;
35773
+
35774
+ } else node = null; // nothing found
35775
+ }
35776
+
35777
+ return this;
35778
+ }
35779
+
35780
+ toBBox(item) { return item; }
35781
+
35782
+ compareMinX(a, b) { return a.minX - b.minX; }
35783
+ compareMinY(a, b) { return a.minY - b.minY; }
35784
+
35785
+ toJSON() { return this.data; }
35786
+
35787
+ fromJSON(data) {
35788
+ this.data = data;
35789
+ return this;
35790
+ }
35791
+
35792
+ _all(node, result) {
35793
+ const nodesToSearch = [];
35794
+ while (node) {
35795
+ if (node.leaf) result.push(...node.children);
35796
+ else nodesToSearch.push(...node.children);
35797
+
35798
+ node = nodesToSearch.pop();
35799
+ }
35800
+ return result;
35801
+ }
35802
+
35803
+ _build(items, left, right, height) {
35804
+
35805
+ const N = right - left + 1;
35806
+ let M = this._maxEntries;
35807
+ let node;
35808
+
35809
+ if (N <= M) {
35810
+ // reached leaf level; return leaf
35811
+ node = createNode(items.slice(left, right + 1));
35812
+ calcBBox(node, this.toBBox);
35813
+ return node;
35814
+ }
35815
+
35816
+ if (!height) {
35817
+ // target height of the bulk-loaded tree
35818
+ height = Math.ceil(Math.log(N) / Math.log(M));
35819
+
35820
+ // target number of root entries to maximize storage utilization
35821
+ M = Math.ceil(N / Math.pow(M, height - 1));
35822
+ }
35823
+
35824
+ node = createNode([]);
35825
+ node.leaf = false;
35826
+ node.height = height;
35827
+
35828
+ // split the items into M mostly square tiles
35829
+
35830
+ const N2 = Math.ceil(N / M);
35831
+ const N1 = N2 * Math.ceil(Math.sqrt(M));
35832
+
35833
+ multiSelect(items, left, right, N1, this.compareMinX);
35834
+
35835
+ for (let i = left; i <= right; i += N1) {
35836
+
35837
+ const right2 = Math.min(i + N1 - 1, right);
35838
+
35839
+ multiSelect(items, i, right2, N2, this.compareMinY);
35840
+
35841
+ for (let j = i; j <= right2; j += N2) {
35842
+
35843
+ const right3 = Math.min(j + N2 - 1, right2);
35844
+
35845
+ // pack each entry recursively
35846
+ node.children.push(this._build(items, j, right3, height - 1));
35847
+ }
35848
+ }
35849
+
35850
+ calcBBox(node, this.toBBox);
35851
+
35852
+ return node;
35853
+ }
35854
+
35855
+ _chooseSubtree(bbox, node, level, path) {
35856
+ while (true) {
35857
+ path.push(node);
35858
+
35859
+ if (node.leaf || path.length - 1 === level) break;
35860
+
35861
+ let minArea = Infinity;
35862
+ let minEnlargement = Infinity;
35863
+ let targetNode;
35864
+
35865
+ for (let i = 0; i < node.children.length; i++) {
35866
+ const child = node.children[i];
35867
+ const area = bboxArea(child);
35868
+ const enlargement = enlargedArea(bbox, child) - area;
35869
+
35870
+ // choose entry with the least area enlargement
35871
+ if (enlargement < minEnlargement) {
35872
+ minEnlargement = enlargement;
35873
+ minArea = area < minArea ? area : minArea;
35874
+ targetNode = child;
35875
+
35876
+ } else if (enlargement === minEnlargement) {
35877
+ // otherwise choose one with the smallest area
35878
+ if (area < minArea) {
35879
+ minArea = area;
35880
+ targetNode = child;
35881
+ }
35882
+ }
35883
+ }
35884
+
35885
+ node = targetNode || node.children[0];
35886
+ }
35887
+
35888
+ return node;
35889
+ }
35890
+
35891
+ _insert(item, level, isNode) {
35892
+ const bbox = isNode ? item : this.toBBox(item);
35893
+ const insertPath = [];
35894
+
35895
+ // find the best node for accommodating the item, saving all nodes along the path too
35896
+ const node = this._chooseSubtree(bbox, this.data, level, insertPath);
35897
+
35898
+ // put the item into the node
35899
+ node.children.push(item);
35900
+ extend(node, bbox);
35901
+
35902
+ // split on node overflow; propagate upwards if necessary
35903
+ while (level >= 0) {
35904
+ if (insertPath[level].children.length > this._maxEntries) {
35905
+ this._split(insertPath, level);
35906
+ level--;
35907
+ } else break;
35908
+ }
35909
+
35910
+ // adjust bboxes along the insertion path
35911
+ this._adjustParentBBoxes(bbox, insertPath, level);
35912
+ }
35913
+
35914
+ // split overflowed node into two
35915
+ _split(insertPath, level) {
35916
+ const node = insertPath[level];
35917
+ const M = node.children.length;
35918
+ const m = this._minEntries;
35919
+
35920
+ this._chooseSplitAxis(node, m, M);
35921
+
35922
+ const splitIndex = this._chooseSplitIndex(node, m, M);
35923
+
35924
+ const newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));
35925
+ newNode.height = node.height;
35926
+ newNode.leaf = node.leaf;
35927
+
35928
+ calcBBox(node, this.toBBox);
35929
+ calcBBox(newNode, this.toBBox);
35930
+
35931
+ if (level) insertPath[level - 1].children.push(newNode);
35932
+ else this._splitRoot(node, newNode);
35933
+ }
35934
+
35935
+ _splitRoot(node, newNode) {
35936
+ // split root node
35937
+ this.data = createNode([node, newNode]);
35938
+ this.data.height = node.height + 1;
35939
+ this.data.leaf = false;
35940
+ calcBBox(this.data, this.toBBox);
35941
+ }
35942
+
35943
+ _chooseSplitIndex(node, m, M) {
35944
+ let index;
35945
+ let minOverlap = Infinity;
35946
+ let minArea = Infinity;
35947
+
35948
+ for (let i = m; i <= M - m; i++) {
35949
+ const bbox1 = distBBox(node, 0, i, this.toBBox);
35950
+ const bbox2 = distBBox(node, i, M, this.toBBox);
35951
+
35952
+ const overlap = intersectionArea(bbox1, bbox2);
35953
+ const area = bboxArea(bbox1) + bboxArea(bbox2);
35954
+
35955
+ // choose distribution with minimum overlap
35956
+ if (overlap < minOverlap) {
35957
+ minOverlap = overlap;
35958
+ index = i;
35959
+
35960
+ minArea = area < minArea ? area : minArea;
35961
+
35962
+ } else if (overlap === minOverlap) {
35963
+ // otherwise choose distribution with minimum area
35964
+ if (area < minArea) {
35965
+ minArea = area;
35966
+ index = i;
35967
+ }
35968
+ }
35969
+ }
35970
+
35971
+ return index || M - m;
35972
+ }
35973
+
35974
+ // sorts node children by the best axis for split
35975
+ _chooseSplitAxis(node, m, M) {
35976
+ const compareMinX = node.leaf ? this.compareMinX : compareNodeMinX;
35977
+ const compareMinY = node.leaf ? this.compareMinY : compareNodeMinY;
35978
+ const xMargin = this._allDistMargin(node, m, M, compareMinX);
35979
+ const yMargin = this._allDistMargin(node, m, M, compareMinY);
35980
+
35981
+ // if total distributions margin value is minimal for x, sort by minX,
35982
+ // otherwise it's already sorted by minY
35983
+ if (xMargin < yMargin) node.children.sort(compareMinX);
35984
+ }
35985
+
35986
+ // total margin of all possible split distributions where each node is at least m full
35987
+ _allDistMargin(node, m, M, compare) {
35988
+ node.children.sort(compare);
35989
+
35990
+ const toBBox = this.toBBox;
35991
+ const leftBBox = distBBox(node, 0, m, toBBox);
35992
+ const rightBBox = distBBox(node, M - m, M, toBBox);
35993
+ let margin = bboxMargin(leftBBox) + bboxMargin(rightBBox);
35994
+
35995
+ for (let i = m; i < M - m; i++) {
35996
+ const child = node.children[i];
35997
+ extend(leftBBox, node.leaf ? toBBox(child) : child);
35998
+ margin += bboxMargin(leftBBox);
35999
+ }
36000
+
36001
+ for (let i = M - m - 1; i >= m; i--) {
36002
+ const child = node.children[i];
36003
+ extend(rightBBox, node.leaf ? toBBox(child) : child);
36004
+ margin += bboxMargin(rightBBox);
36005
+ }
36006
+
36007
+ return margin;
36008
+ }
36009
+
36010
+ _adjustParentBBoxes(bbox, path, level) {
36011
+ // adjust bboxes along the given tree path
36012
+ for (let i = level; i >= 0; i--) {
36013
+ extend(path[i], bbox);
36014
+ }
36015
+ }
36016
+
36017
+ _condense(path) {
36018
+ // go through the path, removing empty nodes and updating bboxes
36019
+ for (let i = path.length - 1, siblings; i >= 0; i--) {
36020
+ if (path[i].children.length === 0) {
36021
+ if (i > 0) {
36022
+ siblings = path[i - 1].children;
36023
+ siblings.splice(siblings.indexOf(path[i]), 1);
36024
+
36025
+ } else this.clear();
36026
+
36027
+ } else calcBBox(path[i], this.toBBox);
36028
+ }
36029
+ }
36030
+ }
36031
+
36032
+ function findItem(item, items, equalsFn) {
36033
+ if (!equalsFn) return items.indexOf(item);
36034
+
36035
+ for (let i = 0; i < items.length; i++) {
36036
+ if (equalsFn(item, items[i])) return i;
36037
+ }
36038
+ return -1;
36039
+ }
36040
+
36041
+ // calculate node's bbox from bboxes of its children
36042
+ function calcBBox(node, toBBox) {
36043
+ distBBox(node, 0, node.children.length, toBBox, node);
36044
+ }
36045
+
36046
+ // min bounding rectangle of node children from k to p-1
36047
+ function distBBox(node, k, p, toBBox, destNode) {
36048
+ if (!destNode) destNode = createNode(null);
36049
+ destNode.minX = Infinity;
36050
+ destNode.minY = Infinity;
36051
+ destNode.maxX = -Infinity;
36052
+ destNode.maxY = -Infinity;
36053
+
36054
+ for (let i = k; i < p; i++) {
36055
+ const child = node.children[i];
36056
+ extend(destNode, node.leaf ? toBBox(child) : child);
36057
+ }
36058
+
36059
+ return destNode;
36060
+ }
36061
+
36062
+ function extend(a, b) {
36063
+ a.minX = Math.min(a.minX, b.minX);
36064
+ a.minY = Math.min(a.minY, b.minY);
36065
+ a.maxX = Math.max(a.maxX, b.maxX);
36066
+ a.maxY = Math.max(a.maxY, b.maxY);
36067
+ return a;
36068
+ }
36069
+
36070
+ function compareNodeMinX(a, b) { return a.minX - b.minX; }
36071
+ function compareNodeMinY(a, b) { return a.minY - b.minY; }
36072
+
36073
+ function bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); }
36074
+ function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }
36075
+
36076
+ function enlargedArea(a, b) {
36077
+ return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *
36078
+ (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));
36079
+ }
36080
+
36081
+ function intersectionArea(a, b) {
36082
+ const minX = Math.max(a.minX, b.minX);
36083
+ const minY = Math.max(a.minY, b.minY);
36084
+ const maxX = Math.min(a.maxX, b.maxX);
36085
+ const maxY = Math.min(a.maxY, b.maxY);
36086
+
36087
+ return Math.max(0, maxX - minX) *
36088
+ Math.max(0, maxY - minY);
36089
+ }
36090
+
36091
+ function contains(a, b) {
36092
+ return a.minX <= b.minX &&
36093
+ a.minY <= b.minY &&
36094
+ b.maxX <= a.maxX &&
36095
+ b.maxY <= a.maxY;
36096
+ }
36097
+
36098
+ function intersects(a, b) {
36099
+ return b.minX <= a.maxX &&
36100
+ b.minY <= a.maxY &&
36101
+ b.maxX >= a.minX &&
36102
+ b.maxY >= a.minY;
36103
+ }
36104
+
36105
+ function createNode(children) {
36106
+ return {
36107
+ children,
36108
+ height: 1,
36109
+ leaf: true,
36110
+ minX: Infinity,
36111
+ minY: Infinity,
36112
+ maxX: -Infinity,
36113
+ maxY: -Infinity
36114
+ };
36115
+ }
36116
+
36117
+ // sort an array so that items come in groups of n unsorted items, with groups sorted between each other;
36118
+ // combines selection algorithm with binary divide & conquer approach
36119
+
36120
+ function multiSelect(arr, left, right, n, compare) {
36121
+ const stack = [left, right];
36122
+
36123
+ while (stack.length) {
36124
+ right = stack.pop();
36125
+ left = stack.pop();
36126
+
36127
+ if (right - left <= n) continue;
36128
+
36129
+ const mid = left + Math.ceil((right - left) / n / 2) * n;
36130
+ quickselect(arr, mid, left, right, compare);
36131
+
36132
+ stack.push(left, mid, mid, right);
36133
+ }
36134
+ }
36135
+
36136
+ /**
36137
+ * R-Tree Data Structure
36138
+ *
36139
+ * R-Tree is a spatial data structure used for efficient indexing and querying
36140
+ * of multi-dimensional objects, particularly in geometric and spatial applications.
36141
+ *
36142
+ * It organizes objects into a tree hierarchy, grouping nearby objects together
36143
+ * in bounding boxes. Each node in the tree represents a bounding box that
36144
+ * contains its child nodes or leaf objects. This hierarchical structure allows
36145
+ * for faster spatial queries.
36146
+ *
36147
+ * @see https://en.wikipedia.org/wiki/R-tree
36148
+ *
36149
+ * Consider a 2D Space with four zones: A, B, C, D
36150
+ * +--------------------------+
36151
+ * | |
36152
+ * | +---+ +-------+ |
36153
+ * | | A | | B | |
36154
+ * | +---+ +-------+ |
36155
+ * | |
36156
+ * | |
36157
+ * | +---+ |
36158
+ * | | C | |
36159
+ * | +---+ |
36160
+ * | +-----------+ |
36161
+ * | | D | |
36162
+ * | +-----------+ |
36163
+ * | |
36164
+ * +--------------------------+
36165
+ *
36166
+ * It groups together zones that are spatially close into a minimum bounding box.
36167
+ * For example, A and B are grouped together in rectangle R1, and C and D are grouped
36168
+ * in R2.
36169
+ *
36170
+ * R0
36171
+ * +--------------------------+
36172
+ * | R1 |
36173
+ * | +-----------------+ |
36174
+ * | | A | | B | |
36175
+ * | +-----------------+ |
36176
+ * | |
36177
+ * | R2 |
36178
+ * | +---+---+---+ |
36179
+ * | | | C | | |
36180
+ * | | +---+ | |
36181
+ * | +-----------+ |
36182
+ * | | D | |
36183
+ * | +-----------+ |
36184
+ * | |
36185
+ * +--------------------------+
36186
+ *
36187
+ * The tree would look like this:
36188
+ * R0
36189
+ * / \
36190
+ * / \
36191
+ * R1 R2
36192
+ * | |
36193
+ * A,B C,D
36194
+
36195
+ * Choosing how to group the zones is crucial for the performance of the tree.
36196
+ * Key considerations include avoiding excessive empty space coverage and minimizing overlap
36197
+ * to reduce the number of subtrees processed during searches.
36198
+ *
36199
+ * Various heuristics exist for determining the optimal grouping strategy, such as "least enlargement"
36200
+ * which prioritizes grouping nodes resulting in the smallest increase in bounding box size. In cases where
36201
+ * the choice cannot be made based on this criterion due to the same enlargement for different groupings,
36202
+ * we then evaluate "least area," aiming to minimize the overall area of bounding boxes.
36203
+ *
36204
+ * This implementation is tailored for spreadsheet use, indexing objects associated
36205
+ * with a zone and a sheet.
36206
+ *
36207
+ * It uses the RBush library under the hood. One 2D RBush R-tree per sheet.
36208
+ * @see https://github.com/mourner/rbush
36209
+ */
36210
+ class SpreadsheetRTree {
36211
+ /**
36212
+ * One 2D R-tree per sheet
36213
+ */
36214
+ rTrees = {};
36215
+ /**
36216
+ * Bulk-inserts the given items into the tree. Bulk insertion is usually ~2-3 times
36217
+ * faster than inserting items one by one. After bulk loading (bulk insertion into
36218
+ * an empty tree), subsequent query performance is also ~20-30% better.
36219
+ */
36220
+ constructor(items = []) {
36221
+ const rangesPerSheet = {};
36222
+ for (const item of items) {
36223
+ const sheetId = item.boundingBox.sheetId;
36224
+ if (!rangesPerSheet[sheetId]) {
36225
+ rangesPerSheet[sheetId] = [];
36226
+ }
36227
+ rangesPerSheet[sheetId].push(item);
36228
+ }
36229
+ for (const sheetId in rangesPerSheet) {
36230
+ this.rTrees[sheetId] = new ZoneRBush();
36231
+ this.rTrees[sheetId].load(rangesPerSheet[sheetId]); // bulk-insert
36232
+ }
36233
+ }
36234
+ insert(item) {
36235
+ const sheetId = item.boundingBox.sheetId;
36236
+ if (!this.rTrees[sheetId]) {
36237
+ this.rTrees[sheetId] = new ZoneRBush();
36238
+ }
36239
+ this.rTrees[sheetId].insert(item);
36240
+ }
36241
+ search({ zone, sheetId }) {
36242
+ if (!this.rTrees[sheetId]) {
36243
+ return [];
36244
+ }
36245
+ return this.rTrees[sheetId].search({
36246
+ minX: zone.left,
36247
+ minY: zone.top,
36248
+ maxX: zone.right,
36249
+ maxY: zone.bottom,
36250
+ });
36251
+ }
36252
+ remove(item) {
36253
+ const sheetId = item.boundingBox.sheetId;
36254
+ if (!this.rTrees[sheetId]) {
36255
+ return;
36256
+ }
36257
+ this.rTrees[sheetId].remove(item, deepEquals);
36258
+ }
36259
+ }
36260
+ /**
36261
+ * RBush extension to use zones as bounding boxes
36262
+ */
36263
+ class ZoneRBush extends RBush {
36264
+ toBBox({ boundingBox }) {
36265
+ const zone = boundingBox.zone;
36266
+ return {
36267
+ minX: zone.left,
36268
+ minY: zone.top,
36269
+ maxX: zone.right,
36270
+ maxY: zone.bottom,
36271
+ };
36272
+ }
36273
+ compareMinX(a, b) {
36274
+ return a.boundingBox.zone.left - b.boundingBox.zone.left;
36275
+ }
36276
+ compareMinY(a, b) {
36277
+ return a.boundingBox.zone.top - b.boundingBox.zone.top;
36278
+ }
36279
+ }
36280
+
35558
36281
  /**
35559
- * This class is an implementation of a dependency Graph.
36282
+ * Implementation of a dependency Graph.
35560
36283
  * The graph is used to evaluate the cells in the correct
35561
36284
  * order, and should be updated each time a cell's content is modified
35562
36285
  *
36286
+ * It uses an R-Tree data structure to efficiently find dependent cells.
35563
36287
  */
35564
36288
  class FormulaDependencyGraph {
35565
- /**
35566
- * Internal structure:
35567
- * - key: a cell position (encoded as an integer)
35568
- * - value: a set of cell positions that depends on the key
35569
- *
35570
- * Given
35571
- * - A1:"= B1 + SQRT(B2)"
35572
- * - C1:"= B1";
35573
- * - C2:"= C1"
35574
- *
35575
- * we will have something like:
35576
- * - B1 ---> (A1, C1) meaning A1 and C1 depends on B1
35577
- * - B2 ---> (A1) meaning A1 depends on B2
35578
- * - C1 ---> (C2) meaning C2 depends on C1
35579
- */
35580
- inverseDependencies = new Map();
36289
+ encoder;
35581
36290
  dependencies = new Map();
36291
+ rTree;
36292
+ constructor(encoder, data = []) {
36293
+ this.encoder = encoder;
36294
+ this.rTree = new SpreadsheetRTree(data);
36295
+ }
35582
36296
  removeAllDependencies(formulaPositionId) {
35583
- const dependencies = this.dependencies.get(formulaPositionId);
35584
- if (!dependencies) {
36297
+ const ranges = this.dependencies.get(formulaPositionId);
36298
+ if (!ranges) {
35585
36299
  return;
35586
36300
  }
35587
- for (const dependency of dependencies) {
35588
- this.inverseDependencies.get(dependency)?.delete(formulaPositionId);
36301
+ for (const range of ranges) {
36302
+ this.rTree.remove(range);
35589
36303
  }
35590
36304
  this.dependencies.delete(formulaPositionId);
35591
36305
  }
35592
36306
  addDependencies(formulaPositionId, dependencies) {
35593
- for (const dependency of dependencies) {
35594
- const inverseDependencies = this.inverseDependencies.get(dependency);
35595
- if (inverseDependencies) {
35596
- inverseDependencies.add(formulaPositionId);
35597
- }
35598
- else {
35599
- this.inverseDependencies.set(dependency, new Set([formulaPositionId]));
35600
- }
36307
+ const rTreeItems = dependencies.map(({ sheetId, zone }) => ({
36308
+ data: formulaPositionId,
36309
+ boundingBox: {
36310
+ zone,
36311
+ sheetId,
36312
+ },
36313
+ }));
36314
+ for (const item of rTreeItems) {
36315
+ this.rTree.insert(item);
35601
36316
  }
35602
36317
  const existingDependencies = this.dependencies.get(formulaPositionId);
35603
36318
  if (existingDependencies) {
35604
- existingDependencies.push(...dependencies);
36319
+ existingDependencies.push(...rTreeItems);
35605
36320
  }
35606
36321
  else {
35607
- this.dependencies.set(formulaPositionId, dependencies);
36322
+ this.dependencies.set(formulaPositionId, rTreeItems);
35608
36323
  }
35609
36324
  }
35610
36325
  /**
@@ -35612,20 +36327,20 @@ class FormulaDependencyGraph {
35612
36327
  * in the correct order they should be evaluated.
35613
36328
  * This is called a topological ordering (excluding cycles)
35614
36329
  */
35615
- getCellsDependingOn(positionIds) {
36330
+ getCellsDependingOn(ranges) {
35616
36331
  const visited = new JetSet();
35617
- const queue = Array.from(positionIds).reverse();
36332
+ const queue = Array.from(ranges).reverse();
35618
36333
  while (queue.length > 0) {
35619
- const node = queue.pop();
35620
- visited.add(node);
35621
- const adjacentNodes = this.inverseDependencies.get(node) || new Set();
35622
- for (const adjacentNode of adjacentNodes) {
35623
- if (!visited.has(adjacentNode)) {
35624
- queue.push(adjacentNode);
36334
+ const range = queue.pop();
36335
+ visited.add(...this.encoder.encodeBoundingBox(range));
36336
+ const impactedPositionIds = this.rTree.search(range).map((dep) => dep.data);
36337
+ for (const positionId of impactedPositionIds) {
36338
+ if (!visited.has(positionId)) {
36339
+ queue.push(this.encoder.decodeToBoundingBox(positionId));
35625
36340
  }
35626
36341
  }
35627
36342
  }
35628
- visited.delete(...positionIds);
36343
+ visited.delete(...ranges.flatMap((r) => this.encoder.encodeBoundingBox(r)));
35629
36344
  return visited;
35630
36345
  }
35631
36346
  }
@@ -35713,9 +36428,9 @@ class Evaluator {
35713
36428
  context;
35714
36429
  getters;
35715
36430
  compilationParams;
35716
- positionEncoder = new PositionBitsEncoder();
36431
+ encoder = new PositionBitsEncoder();
35717
36432
  evaluatedCells = new Map();
35718
- formulaDependencies = lazy(new FormulaDependencyGraph());
36433
+ formulaDependencies = lazy(new FormulaDependencyGraph(this.encoder));
35719
36434
  blockedArrayFormulas = new Set();
35720
36435
  spreadingRelations = new SpreadingRelation();
35721
36436
  constructor(context, getters) {
@@ -35724,16 +36439,16 @@ class Evaluator {
35724
36439
  this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
35725
36440
  }
35726
36441
  getEvaluatedCell(position) {
35727
- return (this.evaluatedCells.get(this.encodePosition(position)) ||
36442
+ return (this.evaluatedCells.get(this.encoder.encode(position)) ||
35728
36443
  createEvaluatedCell("", { locale: this.getters.getLocale() }));
35729
36444
  }
35730
36445
  getArrayFormulaSpreadingOn(position) {
35731
- const positionId = this.encodePosition(position);
36446
+ const positionId = this.encoder.encode(position);
35732
36447
  const formulaPosition = this.getArrayFormulaSpreadingOnId(positionId);
35733
- return formulaPosition !== undefined ? this.decodePosition(formulaPosition) : undefined;
36448
+ return formulaPosition !== undefined ? this.encoder.decode(formulaPosition) : undefined;
35734
36449
  }
35735
36450
  getEvaluatedPositions() {
35736
- return [...this.evaluatedCells.keys()].map(this.decodePosition.bind(this));
36451
+ return [...this.evaluatedCells.keys()].map((p) => this.encoder.decode(p));
35737
36452
  }
35738
36453
  getArrayFormulaSpreadingOnId(positionId) {
35739
36454
  if (!this.spreadingRelations.hasArrayFormulaResult(positionId)) {
@@ -35743,7 +36458,7 @@ class Evaluator {
35743
36458
  return Array.from(arrayFormulas).find((positionId) => !this.blockedArrayFormulas.has(positionId));
35744
36459
  }
35745
36460
  updateDependencies(position) {
35746
- const positionId = this.encodePosition(position);
36461
+ const positionId = this.encoder.encode(position);
35747
36462
  this.formulaDependencies().removeAllDependencies(positionId);
35748
36463
  const dependencies = this.getDirectDependencies(positionId);
35749
36464
  this.formulaDependencies().addDependencies(positionId, dependencies);
@@ -35753,12 +36468,12 @@ class Evaluator {
35753
36468
  this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
35754
36469
  }
35755
36470
  evaluateCells(positions) {
35756
- const cells = positions.map(this.encodePosition.bind(this));
36471
+ const cells = positions.map((p) => this.encoder.encode(p));
35757
36472
  const cellsToCompute = new JetSet(cells);
35758
- const arrayFormulas = this.getArrayFormulasImpactedByChangesOf(cells);
36473
+ const arrayFormulasPositionIds = this.getArrayFormulasImpactedByChangesOf(cells);
35759
36474
  cellsToCompute.add(...this.getCellsDependingOn(cells));
35760
- cellsToCompute.add(...arrayFormulas);
35761
- cellsToCompute.add(...this.getCellsDependingOn(arrayFormulas));
36475
+ cellsToCompute.add(...arrayFormulasPositionIds);
36476
+ cellsToCompute.add(...this.getCellsDependingOn(arrayFormulasPositionIds));
35762
36477
  this.evaluate(cellsToCompute);
35763
36478
  }
35764
36479
  getArrayFormulasImpactedByChangesOf(positionIds) {
@@ -35781,12 +36496,14 @@ class Evaluator {
35781
36496
  this.blockedArrayFormulas = new Set();
35782
36497
  this.spreadingRelations = new SpreadingRelation();
35783
36498
  this.formulaDependencies = lazy(() => {
35784
- const dependencyGraph = new FormulaDependencyGraph();
35785
- for (const positionId of this.getAllCells()) {
35786
- const dependencies = this.getDirectDependencies(positionId);
35787
- dependencyGraph.addDependencies(positionId, dependencies);
35788
- }
35789
- return dependencyGraph;
36499
+ const dependencies = [...this.getAllCells()].flatMap((positionId) => this.getDirectDependencies(positionId).map((range) => ({
36500
+ data: positionId,
36501
+ boundingBox: {
36502
+ zone: range.zone,
36503
+ sheetId: range.sheetId,
36504
+ },
36505
+ })));
36506
+ return new FormulaDependencyGraph(this.encoder, dependencies);
35790
36507
  });
35791
36508
  }
35792
36509
  evaluateAllCells() {
@@ -35807,7 +36524,7 @@ class Evaluator {
35807
36524
  for (const sheetId of this.getters.getSheetIds()) {
35808
36525
  const cellIds = this.getters.getCells(sheetId);
35809
36526
  for (const cellId in cellIds) {
35810
- positionIds.add(this.encodePosition(this.getters.getCellPosition(cellId)));
36527
+ positionIds.add(this.encoder.encode(this.getters.getCellPosition(cellId)));
35811
36528
  }
35812
36529
  }
35813
36530
  return positionIds;
@@ -35874,7 +36591,7 @@ class Evaluator {
35874
36591
  }
35875
36592
  }
35876
36593
  computeAndSave(position) {
35877
- const positionId = this.encodePosition(position);
36594
+ const positionId = this.encoder.encode(position);
35878
36595
  const evaluatedCell = this.computeCell(positionId);
35879
36596
  if (!this.evaluatedCells.has(positionId)) {
35880
36597
  this.setEvaluatedCell(positionId, evaluatedCell);
@@ -35935,15 +36652,15 @@ class Evaluator {
35935
36652
  throw new Error(_lt("Result couldn't be automatically expanded. Please insert more columns and rows."));
35936
36653
  }
35937
36654
  updateSpreadRelation({ sheetId, col, row, }) {
35938
- const arrayFormulaPositionId = this.encodePosition({ sheetId, col, row });
36655
+ const arrayFormulaPositionId = this.encoder.encode({ sheetId, col, row });
35939
36656
  return (i, j) => {
35940
36657
  const position = { sheetId, col: i + col, row: j + row };
35941
- const resultPositionId = this.encodePosition(position);
36658
+ const resultPositionId = this.encoder.encode(position);
35942
36659
  this.spreadingRelations.addRelation({ resultPositionId, arrayFormulaPositionId });
35943
36660
  };
35944
36661
  }
35945
36662
  checkCollision({ sheetId, col, row }) {
35946
- const formulaPositionId = this.encodePosition({ sheetId, col, row });
36663
+ const formulaPositionId = this.encoder.encode({ sheetId, col, row });
35947
36664
  return (i, j) => {
35948
36665
  const position = { sheetId: sheetId, col: i + col, row: j + row };
35949
36666
  const rawCell = this.getters.getCell(position);
@@ -35965,7 +36682,7 @@ class Evaluator {
35965
36682
  format: format || formatFromPosition(i, j),
35966
36683
  locale: this.getters.getLocale(),
35967
36684
  });
35968
- const positionId = this.encodePosition(position);
36685
+ const positionId = this.encoder.encode(position);
35969
36686
  this.setEvaluatedCell(positionId, evaluatedCell);
35970
36687
  // check if formula dependencies present in the spread zone
35971
36688
  // if so, they need to be recomputed
@@ -35997,29 +36714,17 @@ class Evaluator {
35997
36714
  if (!cell?.isFormula) {
35998
36715
  return [];
35999
36716
  }
36000
- const dependencies = [];
36001
- for (const range of cell.dependencies) {
36002
- if (range.invalidSheetName || range.invalidXc) {
36003
- continue;
36004
- }
36005
- const sheetId = range.sheetId;
36006
- forEachPositionsInZone(range.zone, (col, row) => {
36007
- dependencies.push(this.encodePosition({ sheetId, col, row }));
36008
- });
36009
- }
36010
- return dependencies;
36717
+ return cell.dependencies;
36011
36718
  }
36012
36719
  getCellsDependingOn(positionIds) {
36013
- return this.formulaDependencies().getCellsDependingOn(positionIds);
36720
+ const ranges = [];
36721
+ for (const positionId of positionIds) {
36722
+ ranges.push(this.encoder.decodeToBoundingBox(positionId));
36723
+ }
36724
+ return this.formulaDependencies().getCellsDependingOn(ranges);
36014
36725
  }
36015
36726
  getCell(positionId) {
36016
- return this.getters.getCell(this.decodePosition(positionId));
36017
- }
36018
- encodePosition(position) {
36019
- return this.positionEncoder.encode(position);
36020
- }
36021
- decodePosition(positionId) {
36022
- return this.positionEncoder.decode(positionId);
36727
+ return this.getters.getCell(this.encoder.decode(positionId));
36023
36728
  }
36024
36729
  }
36025
36730
  function forEachSpreadPositionInMatrix(matrix, callback) {
@@ -36094,6 +36799,13 @@ class PositionBitsEncoder {
36094
36799
  encode({ sheetId, col, row }) {
36095
36800
  return (this.encodeSheet(sheetId) << 42n) | (BigInt(col) << 21n) | BigInt(row);
36096
36801
  }
36802
+ encodeBoundingBox({ sheetId, zone }) {
36803
+ const positions = [];
36804
+ forEachPositionsInZone(zone, (col, row) => {
36805
+ positions.push(this.encode({ sheetId, col, row }));
36806
+ });
36807
+ return positions;
36808
+ }
36097
36809
  decode(id) {
36098
36810
  // keep only the last 21 bits by AND-ing the bit sequence with 21 ones
36099
36811
  const row = Number(id & 2097151n);
@@ -36101,6 +36813,10 @@ class PositionBitsEncoder {
36101
36813
  const sheetId = this.decodeSheet(id >> 42n);
36102
36814
  return { sheetId, col, row };
36103
36815
  }
36816
+ decodeToBoundingBox(id) {
36817
+ const { sheetId, col, row } = this.decode(id);
36818
+ return { sheetId, zone: { left: col, top: row, right: col, bottom: row } };
36819
+ }
36104
36820
  encodeSheet(sheetId) {
36105
36821
  const sheetKey = this.sheetMapping[sheetId];
36106
36822
  if (sheetKey === undefined) {
@@ -36215,6 +36931,7 @@ class PositionBitsEncoder {
36215
36931
  class EvaluationPlugin extends UIPlugin {
36216
36932
  static getters = [
36217
36933
  "evaluateFormula",
36934
+ "getCorrespondingFormulaCell",
36218
36935
  "getRangeFormattedValues",
36219
36936
  "getRangeValues",
36220
36937
  "getRangeFormats",
@@ -36268,7 +36985,12 @@ class EvaluationPlugin extends UIPlugin {
36268
36985
  // Getters
36269
36986
  // ---------------------------------------------------------------------------
36270
36987
  evaluateFormula(sheetId, formulaString) {
36271
- return this.evaluator.evaluateFormula(sheetId, formulaString);
36988
+ try {
36989
+ return this.evaluator.evaluateFormula(sheetId, formulaString);
36990
+ }
36991
+ catch (error) {
36992
+ return error instanceof EvaluationError ? error.errorType : CellErrorType.GenericError;
36993
+ }
36272
36994
  }
36273
36995
  /**
36274
36996
  * Return the value of each cell in the range as they are displayed in the grid.
@@ -36531,7 +37253,7 @@ class CustomColorsPlugin extends UIPlugin {
36531
37253
  }
36532
37254
 
36533
37255
  class EvaluationChartPlugin extends UIPlugin {
36534
- static getters = ["getChartRuntime", "getBackgroundOfSingleCellChart"];
37256
+ static getters = ["getChartRuntime", "getStyleOfSingleCellChart"];
36535
37257
  charts = {};
36536
37258
  createRuntimeChart = chartRuntimeFactory(this.getters);
36537
37259
  handle(cmd) {
@@ -36569,25 +37291,26 @@ class EvaluationChartPlugin extends UIPlugin {
36569
37291
  return this.charts[figureId];
36570
37292
  }
36571
37293
  /**
36572
- * Get the background color of a chart based on the color of the first cell of the main range
36573
- * of the chart. In order of priority, it will return :
36574
- *
36575
- * - the chart background color if one is defined
36576
- * - the fill color of the cell if one is defined
36577
- * - the fill color of the cell from conditional formats if one is defined
36578
- * - the default chart color if no other color is defined
37294
+ * Get the background and textColor of a chart based on the color of the first cell of the main range of the chart.
36579
37295
  */
36580
- getBackgroundOfSingleCellChart(chartBackground, mainRange) {
37296
+ getStyleOfSingleCellChart(chartBackground, mainRange) {
36581
37297
  if (chartBackground)
36582
- return chartBackground;
37298
+ return { background: chartBackground, fontColor: chartFontColor(chartBackground) };
36583
37299
  if (!mainRange) {
36584
- return BACKGROUND_CHART_COLOR;
37300
+ return {
37301
+ background: BACKGROUND_CHART_COLOR,
37302
+ fontColor: chartFontColor(BACKGROUND_CHART_COLOR),
37303
+ };
36585
37304
  }
36586
37305
  const col = mainRange.zone.left;
36587
37306
  const row = mainRange.zone.top;
36588
37307
  const sheetId = mainRange.sheetId;
36589
37308
  const style = this.getters.getCellComputedStyle({ sheetId, col, row });
36590
- return style.fillColor || BACKGROUND_CHART_COLOR;
37309
+ const background = style.fillColor || BACKGROUND_CHART_COLOR;
37310
+ return {
37311
+ background,
37312
+ fontColor: style.textColor || chartFontColor(background),
37313
+ };
36591
37314
  }
36592
37315
  }
36593
37316
 
@@ -36657,34 +37380,29 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
36657
37380
  getComputedStyles(sheetId) {
36658
37381
  const computedStyle = {};
36659
37382
  for (let cf of this.getters.getConditionalFormats(sheetId).reverse()) {
36660
- try {
36661
- switch (cf.rule.type) {
36662
- case "ColorScaleRule":
36663
- for (let range of cf.ranges) {
36664
- this.applyColorScale(sheetId, range, cf.rule, computedStyle);
36665
- }
36666
- break;
36667
- case "CellIsRule":
36668
- for (let ref of cf.ranges) {
36669
- const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
36670
- for (let row = zone.top; row <= zone.bottom; row++) {
36671
- for (let col = zone.left; col <= zone.right; col++) {
36672
- const pr = this.rulePredicate[cf.rule.type];
36673
- let cell = this.getters.getEvaluatedCell({ sheetId, col, row });
36674
- if (pr && pr(cell, cf.rule)) {
36675
- if (!computedStyle[col])
36676
- computedStyle[col] = [];
36677
- // we must combine all the properties of all the CF rules applied to the given cell
36678
- computedStyle[col][row] = Object.assign(computedStyle[col]?.[row] || {}, cf.rule.style);
36679
- }
37383
+ switch (cf.rule.type) {
37384
+ case "ColorScaleRule":
37385
+ for (let range of cf.ranges) {
37386
+ this.applyColorScale(sheetId, range, cf.rule, computedStyle);
37387
+ }
37388
+ break;
37389
+ case "CellIsRule":
37390
+ for (let ref of cf.ranges) {
37391
+ const zone = this.getters.getRangeFromSheetXC(sheetId, ref).zone;
37392
+ for (let row = zone.top; row <= zone.bottom; row++) {
37393
+ for (let col = zone.left; col <= zone.right; col++) {
37394
+ const pr = this.rulePredicate[cf.rule.type];
37395
+ let cell = this.getters.getEvaluatedCell({ sheetId, col, row });
37396
+ if (pr && pr(cell, cf.rule)) {
37397
+ if (!computedStyle[col])
37398
+ computedStyle[col] = [];
37399
+ // we must combine all the properties of all the CF rules applied to the given cell
37400
+ computedStyle[col][row] = Object.assign(computedStyle[col]?.[row] || {}, cf.rule.style);
36680
37401
  }
36681
37402
  }
36682
37403
  }
36683
- break;
36684
- }
36685
- }
36686
- catch (_) {
36687
- // we don't care about the errors within the evaluation of a rule
37404
+ }
37405
+ break;
36688
37406
  }
36689
37407
  }
36690
37408
  return computedStyle;
@@ -36720,7 +37438,7 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
36720
37438
  return percentile(rangeValues, Number(threshold.value) / 100, true);
36721
37439
  case "formula":
36722
37440
  const value = threshold.value && this.getters.evaluateFormula(sheetId, threshold.value);
36723
- return !(value instanceof Promise) ? value : null;
37441
+ return typeof value === "number" ? value : null;
36724
37442
  default:
36725
37443
  return null;
36726
37444
  }
@@ -37286,19 +38004,21 @@ class AutofillPlugin extends UIPlugin {
37286
38004
  let col = zone.left;
37287
38005
  let row = zone.bottom;
37288
38006
  if (col > 0) {
37289
- let left = this.getters.getEvaluatedCell({ sheetId, col: col - 1, row });
37290
- while (left.type !== CellValueType.empty) {
38007
+ let leftPosition = { sheetId, col: col - 1, row };
38008
+ while (this.getters.getCorrespondingFormulaCell(leftPosition) ||
38009
+ this.getters.getCell(leftPosition)?.content) {
37291
38010
  row += 1;
37292
- left = this.getters.getEvaluatedCell({ sheetId, col: col - 1, row });
38011
+ leftPosition = { sheetId, col: col - 1, row };
37293
38012
  }
37294
38013
  }
37295
38014
  if (row === zone.bottom) {
37296
38015
  col = zone.right;
37297
38016
  if (col <= this.getters.getNumberCols(sheetId)) {
37298
- let right = this.getters.getEvaluatedCell({ sheetId, col: col + 1, row });
37299
- while (right.type !== CellValueType.empty) {
38017
+ let rightPosition = { sheetId, col: col + 1, row };
38018
+ while (this.getters.getCorrespondingFormulaCell(rightPosition) ||
38019
+ this.getters.getCell(rightPosition)?.content) {
37300
38020
  row += 1;
37301
- right = this.getters.getEvaluatedCell({ sheetId, col: col + 1, row });
38021
+ rightPosition = { sheetId, col: col + 1, row };
37302
38022
  }
37303
38023
  }
37304
38024
  }
@@ -40216,6 +40936,9 @@ class SelectionInputsManagerPlugin extends UIPlugin {
40216
40936
  // Other
40217
40937
  // ---------------------------------------------------------------------------
40218
40938
  initInput(id, initialRanges, inputHasSingleRange = false) {
40939
+ if (this.inputs[id]) {
40940
+ this.unfocus();
40941
+ }
40219
40942
  this.inputs[id] = new SelectionInputPlugin(this.config, initialRanges, inputHasSingleRange);
40220
40943
  if (initialRanges.length === 0) {
40221
40944
  const input = this.inputs[id];
@@ -42331,6 +43054,10 @@ function setXcToReferenceType(xc, referenceType) {
42331
43054
  return xc.slice(0, indexOfNumber) + "$" + xc.slice(indexOfNumber);
42332
43055
  case "colrow":
42333
43056
  indexOfNumber = xc.search(/[0-9]/);
43057
+ if (indexOfNumber === -1 || indexOfNumber === 0) {
43058
+ // no row number (eg. A) or no column (eg. 1)
43059
+ return "$" + xc;
43060
+ }
42334
43061
  xc = xc.slice(0, indexOfNumber) + "$" + xc.slice(indexOfNumber);
42335
43062
  return "$" + xc;
42336
43063
  case "none":
@@ -43746,7 +44473,7 @@ class GridSelectionPlugin extends UIPlugin {
43746
44473
  sheetId: cmd.sheetId,
43747
44474
  base: cmd.base,
43748
44475
  quantity: thickness,
43749
- position: "before",
44476
+ position: cmd.position,
43750
44477
  });
43751
44478
  const isCol = cmd.dimension === "COL";
43752
44479
  const start = cmd.elements[0];
@@ -43763,12 +44490,13 @@ class GridSelectionPlugin extends UIPlugin {
43763
44490
  },
43764
44491
  ];
43765
44492
  const state = new ClipboardCellsState(target, "CUT", this.getters, this.dispatch, this.selection);
44493
+ const base = isBasedBefore ? cmd.base : cmd.base + 1;
43766
44494
  const pasteTarget = [
43767
44495
  {
43768
- left: isCol ? cmd.base : 0,
43769
- right: isCol ? cmd.base + thickness - 1 : this.getters.getNumberCols(cmd.sheetId) - 1,
43770
- top: !isCol ? cmd.base : 0,
43771
- bottom: !isCol ? cmd.base + thickness - 1 : this.getters.getNumberRows(cmd.sheetId) - 1,
44496
+ left: isCol ? base : 0,
44497
+ right: isCol ? base + thickness - 1 : this.getters.getNumberCols(cmd.sheetId) - 1,
44498
+ top: !isCol ? base : 0,
44499
+ bottom: !isCol ? base + thickness - 1 : this.getters.getNumberRows(cmd.sheetId) - 1,
43772
44500
  },
43773
44501
  ];
43774
44502
  state.paste(pasteTarget, { selectTarget: true });
@@ -43805,6 +44533,11 @@ class GridSelectionPlugin extends UIPlugin {
43805
44533
  doesElementsHaveCommonMerges(id, cmd.base - 1, cmd.base)) {
43806
44534
  return 2 /* CommandResult.WillRemoveExistingMerge */;
43807
44535
  }
44536
+ const headers = [cmd.base, ...cmd.elements];
44537
+ const maxHeaderValue = isCol ? this.getters.getNumberCols(id) : this.getters.getNumberRows(id);
44538
+ if (headers.some((h) => h < 0 || h >= maxHeaderValue)) {
44539
+ return 88 /* CommandResult.InvalidHeaderIndex */;
44540
+ }
43808
44541
  return 0 /* CommandResult.Success */;
43809
44542
  }
43810
44543
  //-------------------------------------------
@@ -47030,7 +47763,7 @@ class Spreadsheet extends Component {
47030
47763
  }
47031
47764
  onKeydown(ev) {
47032
47765
  let keyDownString = "";
47033
- if (ev.ctrlKey || ev.metaKey) {
47766
+ if (isCtrlKey(ev)) {
47034
47767
  keyDownString += "CTRL+";
47035
47768
  }
47036
47769
  keyDownString += ev.key.toUpperCase();
@@ -50757,6 +51490,6 @@ const constants = {
50757
51490
  export { AbstractChart, CommandResult, CorePlugin, DispatchResult, EvaluationError, Model, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, UIPlugin, __info__, addFunction, astToFormula, compile, components, constants, convertAstNodes, coreTypes, findCellInNewZone, functionCache, helpers, invalidateCFEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, iterateAstNodes, links, load, parse, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, tokenize };
50758
51491
 
50759
51492
 
50760
- __info__.version = '16.4.15';
50761
- __info__.date = '2023-11-24T13:12:26.608Z';
50762
- __info__.hash = '141943d';
51493
+ __info__.version = '16.4.17';
51494
+ __info__.date = '2024-01-04T12:22:53.125Z';
51495
+ __info__.hash = '769f8a3';